Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ nav:
- Migrating to V6: tutorials/migrating-from-v5-to-v6.md
- Concepts:
- Overview: concepts/overview.md
- Plugins:
- Overview: plugins/index.md
- Object Signing: plugins/object-signing.md
47 changes: 47 additions & 0 deletions src/plugins/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
layout: default
title: Plugins
---
# Plugins

`go-git` ships a lightweight, type-safe plugin registry (package `github.com/go-git/go-git/v6/x/plugin`) that lets callers replace internal implementations without modifying the core library.

Each plugin is identified by a typed **key** — a value that carries the Go type it manages at compile time. This means the compiler rejects any attempt to register an incompatible factory: a factory for type `A` cannot be stored under a key declared for type `B`.

## Lifecycle

The registry follows a strict, three-phase lifecycle:

1. **Register** — Provide a factory function early in program initialization, before any library code reads the plugin. The idiomatic place is an `init()` function or `main()` before calling any `go-git` APIs.
2. **Freeze** — The first call to `plugin.Get(key)` internally freezes that entry. Any subsequent `Register` call for the same key returns `plugin.ErrFrozen`.
3. **Get** — `plugin.Get(key)` invokes the registered factory and returns a fresh value of the plugin type.

```go
func init() {
err := plugin.Register(plugin.ObjectSigner(), func() plugin.Signer {
// return your signer implementation
return mySigner
})
if err != nil {
log.Fatal(err)
}
}
```

## Available plugins

| Key | Plugin type | Description |
|-----|-------------|-------------|
| `plugin.ObjectSigner()` | `plugin.Signer` | Defines the default signer for Git objects (commits and tags). When set, go-git will use it to sign new tags or commits, based on the respective config entries `tag.gpgSign=true` and `commit.gpgSign=true`. |

## Errors

| Error | Returned by | Cause |
|-------|-------------|-------|
| `plugin.ErrFrozen` | `Register` | `Get` has already been called for that key |
| `plugin.ErrNotFound` | `Get` | No factory has been registered for the key |
| `plugin.ErrNilFactory` | `Register` | A `nil` factory was passed |

## Thread safety

`Register` and `Get` are safe for concurrent use. The freeze is an atomic transition: once `Get` marks a key as frozen, no concurrent `Register` can succeed for that key.
269 changes: 269 additions & 0 deletions src/plugins/object-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
---
layout: default
title: Object Signing
---
# Object Signing

The `plugin.ObjectSigner` plugin lets applications provide a cryptographic signer for Git objects. When registered and the config settings `tag.gpgSign=true` or `commit.gpgSign=true` are set, `go-git` will automatically sign every new commit and tag using the configured signer.

A signer is any value that satisfies `plugin.Signer`:

```go
type Signer interface {
Sign(message io.Reader) ([]byte, error)
}
```

The extended repository (`github.com/go-git/x`) ships two ready-to-use implementations:

| Package | Format | Protocol |
|---------|--------|----------|
| `github.com/go-git/x/plugin/objectsigner/gpg` | OpenPGP / GPG | ASCII-armored detached signature |
| `github.com/go-git/x/plugin/objectsigner/ssh` | SSH | [sshsig] armored signature |

## Application-level vs per-operation signing

There are two ways to attach a signer.

**Application-level** — Register once at startup. Every subsequent commit and tag is signed automatically without changing any call sites.

```go
func init() {
plugin.Register(plugin.ObjectSigner(), func() plugin.Signer {
return mySigner
})
}
```

**Per-operation** — Pass a `Signer` directly in `CommitOptions` or `CreateTagOptions`. This takes precedence over the application-level plugin for that single call.

```go
w.Commit("message", &git.CommitOptions{
Signer: mySigner,
Author: &object.Signature{ /* ... */ },
})
```

## GPG / OpenPGP signing

Install the package:

```
go get github.com/go-git/x/plugin/objectsigner/gpg
```

`gpg.FromKey` wraps an `*openpgp.Entity` from [`github.com/ProtonMail/go-crypto`] and produces ASCII-armored detached signatures.

The following example registers a freshly generated RSA-2048 / SHA-256 key at application level, initialises an in-memory repository, commits a file, and prints both the commit object and its raw signature:

```go
package main

import (
"crypto"
"fmt"
"os"
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/go-git/go-billy/v6/memfs"
"github.com/go-git/x/plugin/objectsigner/gpg"

"github.com/go-git/go-git/v6"
. "github.com/go-git/go-git/v6/_examples"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/go-git/go-git/v6/storage/memory"
"github.com/go-git/go-git/v6/x/plugin"
)

func main() {
key, err := getKey()
CheckIfError(err)

// Register a static GPG signer at application level. All new Tags and Commits
// will be signed by this key, unless overwritten on a "per-operation" basis.
err = plugin.Register(plugin.ObjectSigner(), func() plugin.Signer { return gpg.FromKey(key) })
CheckIfError(err)

fs := memfs.New()
st := memory.NewStorage()
r, err := git.Init(st, git.WithWorkTree(fs))
CheckIfError(err)

w, err := r.Worktree()
CheckIfError(err)

// Create a new file inside the worktree.
Info("echo \"hello world!\" > example-git-file")
f, err := fs.OpenFile("example-git-file", os.O_CREATE|os.O_RDWR, 0o644)
CheckIfError(err)

_, err = f.Write([]byte("hello world!"))
CheckIfError(err)
CheckIfError(f.Close())

// Add the new file to the staging area.
Info("git add example-git-file")
_, err = w.Add("example-git-file")
CheckIfError(err)

Info("git commit -m \"example go-git commit\"")
commit, err := w.Commit("example go-git commit", &git.CommitOptions{
// A signer can be defined on a per-operation basis, instead of the
// application-level as per plugin.Register.
// Signer: gpg.FromKey(key),

Author: &object.Signature{
Name: "John Doe",
Email: "john@doe.org",
When: time.Now(),
},
})
CheckIfError(err)

// Print the commit.
Info("git show -s")
obj, err := r.CommitObject(commit)
CheckIfError(err)

fmt.Println(obj)

// Print raw signature.
fmt.Println(obj.Signature)
}

func getKey() (*openpgp.Entity, error) {
cfg := &packet.Config{
RSABits: 2048,
Time: func() time.Time { return time.Now() },
DefaultHash: crypto.SHA256,
}

return openpgp.NewEntity(
"Test User", // name
"Test Key", // comment
"test@example.com", // email
cfg,
)
}
```

## SSH signing

Install the package:

```
go get github.com/go-git/x/plugin/objectsigner/ssh
```

`ssh.FromKey` wraps a `golang.org/x/crypto/ssh.Signer` and produces [sshsig]-format armored signatures. Two hash algorithms are supported: `ssh.SHA256` and `ssh.SHA512` (the default).

Keys can come from an SSH agent, a key file, or be generated in memory (useful for testing). The example below tries the SSH agent first and falls back to the other helpers:

```go
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net"
"os"
"time"

"github.com/go-git/go-billy/v6/memfs"
"github.com/go-git/x/plugin/objectsigner/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"

"github.com/go-git/go-git/v6"
. "github.com/go-git/go-git/v6/_examples"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/go-git/go-git/v6/storage/memory"
"github.com/go-git/go-git/v6/x/plugin"
)

func main() {
// Generally this key would come from a file or SSH agent.
key, err := getKeyFromAgent()
CheckIfError(err)

// Register a static SSH signer at application level. All new Tags and Commits
// will be signed by this key, unless overwritten on a "per-operation" basis.
err = plugin.Register(plugin.ObjectSigner(),
func() plugin.Signer { return ssh.FromKey(key) })
CheckIfError(err)

fs := memfs.New()
st := memory.NewStorage()
r, err := git.Init(st, git.WithWorkTree(fs))
CheckIfError(err)

w, err := r.Worktree()
CheckIfError(err)

// Create a new file inside the worktree.
Info("echo \"hello world!\" > example-git-file")
f, err := fs.OpenFile("example-git-file", os.O_CREATE|os.O_RDWR, 0o644)
CheckIfError(err)

_, err = f.Write([]byte("hello world!"))
CheckIfError(err)
CheckIfError(f.Close())

// Add the new file to the staging area.
Info("git add example-git-file")
_, err = w.Add("example-git-file")
CheckIfError(err)

Info("git commit -m \"example go-git commit\"")
commit, err := w.Commit("example go-git commit", &git.CommitOptions{
// A signer can be defined on a per-operation basis, instead of the
// application-level as per plugin.Register.
// Signer: ssh.FromKey(key, ssh.SHA512),

Author: &object.Signature{
Name: "John Doe",
Email: "john@doe.org",
When: time.Now(),
},
})
CheckIfError(err)

// Print the commit.
Info("git show -s")
obj, err := r.CommitObject(commit)
CheckIfError(err)

fmt.Println(obj)

// Print raw signature.
fmt.Println(obj.Signature)
}

// getKey generates a fresh ECDSA P-256 key (useful for testing).
func getKey() (gossh.Signer, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
return gossh.NewSignerFromKey(key)
}

// getKeyFromFile loads an SSH private key from disk with an optional passphrase.
func getKeyFromFile(file, passphrase string) (gossh.Signer, error) {
keyBytes, err := os.ReadFile(file)
CheckIfError(err)

if len(passphrase) > 0 {
return gossh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(passphrase))
}

return gossh.ParsePrivateKey(keyBytes)
}
```

[sshsig]: https://github.com/openssh/openssh-portable/blob/V_10_2/PROTOCOL.sshsig
[`github.com/ProtonMail/go-crypto`]: https://github.com/ProtonMail/go-crypto