Skip to content

Verify plugin#1883

Draft
Bluebugs wants to merge 15 commits intogo-git:mainfrom
Bluebugs:verify-plugin
Draft

Verify plugin#1883
Bluebugs wants to merge 15 commits intogo-git:mainfrom
Bluebugs:verify-plugin

Conversation

@Bluebugs
Copy link
Copy Markdown
Contributor

@Bluebugs Bluebugs commented Mar 6, 2026

Added on top of #1860 a verify plugin that could be used like so:

import (
    "errors"
    "fmt"
    "os"

    git "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing/object"
    "github.com/go-git/go-git/v6/x/plugin"

    // Out-of-tree verifier implementations (e.g. from github.com/go-git/x)
    gossh "github.com/go-git/x/plugin/objectverifier/ssh"
    // "github.com/go-git/x/plugin/objectverifier/gpg"
)

func main() {
    // Register a verifier plugin globally — once at startup.
    // All commits/tags fetched via Repository methods will auto-verify.
    plugin.Register(plugin.ObjectVerifier(),
        func() plugin.Verifier {
            v, _ := gossh.FromAllowedSignersFile("~/.ssh/allowed_signers")
            return v
            // return gpg.FromArmoredKeyRing(keyring)
        })

    r, _ := git.PlainOpen("/path/to/repo")

    // Commits retrieved via the repo auto-carry the verifier
    commit, _ := r.CommitObject(someHash)

    result, err := commit.Verify()
    switch {
    case err == nil:
        fmt.Printf("Verified: %s (signed by %s)\n", result.KeyID, result.Signer)
    case errors.Is(err, object.ErrKeyNotTrusted):
        fmt.Printf("Valid signature from unknown key: %s\n", result.KeyID)
    case errors.Is(err, object.ErrSignatureInvalid):
        fmt.Println("Signature is invalid — content may have been tampered with")
    case errors.Is(err, object.ErrNoSignature):
        fmt.Println("Commit is unsigned")
    default:
        fmt.Printf("Verification failed: %v\n", err)
    }
}

This is trying to address the discussion from #1869 . I couldn't figure out a way to do this PR against PR #1860, so it does have all the commit from it too :-(

pjbgf and others added 15 commits February 27, 2026 11:16
Signed-off-by: Paulo Gomes <pjbgf@linux.com>
The plugin package provides a generic, thread-safe registry for plugin
factory functions. It enables off-tree implementations to be registered and
retrieved at runtime.

The first plugin type introduced is ObjectSigner. When registered, the plugin
will define a function factory which will be used when creating commits or tags.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
When no CommitOptions.Signer is provided, buildCommitObject falls back to
the globally registered ObjectSigner plugin. An explicit CommitOptions.Signer
always takes precedence over the plugin.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
These changes align the user experience, so that both Tags and Commits
share the same API.

The implementation details are now pushed back into the out-of-tree
plugins, removing external dependencies from the core go-git logic.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Add support for GPG configuration in git config, following existing
patterns for User, Author, and Committer sections.

New configuration options:
- gpg.format: signature format ("openpgp" or "ssh")
- gpg.ssh.allowedSignersFile: path to SSH allowed signers file

This enables reading Git's gpg.ssh.allowedSignersFile setting for
SSH signature verification workflows.
Expand on sections/keys exposed by config.Config so that go-git can
implement heuristics to automatically GPG/SSH sign objects.

Added fields: tag.gpgSign, commit.gpgSign and user.signingKey.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Auto-signing objects now respect the config values for tag.gpgSign and commit.gpgSign,
meaning that when an ObjectSigner is registered, it will only be used when either setting
is enabled.

Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Signed-off-by: Paulo Gomes <pjbgf@linux.com>
Export signatureType as SignatureType along with its constants
(SignatureTypeUnknown, SignatureTypeOpenPGP, SignatureTypeX509,
SignatureTypeSSH) so they can be used by verification interfaces.

Add DetectSignatureType() public function and String() method.
Also add "-----BEGIN CERTIFICATE-----" to x509 signature formats.
Introduce the core verification abstractions:

- Verifier interface with a single Verify(signature, message) method
- VerificationResult carrying validity, trust level, key info and errors
- TrustLevel enum (Undefined, Never, Marginal, Full, Ultimate)
- ObjectOption functional options for injecting a Verifier at decode time

These types are signature-format agnostic and designed to be implemented
by out-of-tree plugins for OpenPGP, SSH, or other verification backends.
Set the storer field when constructing Commit and Tag objects in
ObjectIter.toObject. Without this, objects obtained through ObjectIter
cannot navigate to related objects (e.g. commit.Tree(), commit.Parents()).

Tree already had this set correctly.
Replace the openpgp-specific Verify(armoredKeyRing) methods on Commit
and Tag with format-agnostic Verify() and VerifyWith(Verifier) methods.

Verify() uses the verifier injected at construction time via
WithVerifier. VerifyWith() accepts an explicit verifier, useful for
one-off verification without plugin registration.

The verifier propagates through object navigation: Parent(), Parents(),
Commit(), and Object() all forward the verifier to decoded children.

GetCommit, DecodeCommit, GetTag, DecodeTag, GetObject and DecodeObject
now accept variadic ObjectOption to support verifier injection.
Register an ObjectVerifier plugin key following the same pattern as
ObjectSigner. When a verification plugin is registered, Repository
methods (CommitObject, TagObject, Object) automatically inject it
into decoded objects via WithVerifier.

Callers can still use VerifyWith() directly without any plugin.
Replace the TrustLevel enum and Valid/Error fields in VerificationResult
with a set of sentinel errors returned by Verify. The error return value
now carries the full verification outcome:

- nil: signature is valid and the key is trusted.
- ErrSignatureInvalid: cryptographic check failed.
- ErrKeyNotTrusted: valid signature, key not in any trust store.
- ErrKeyExpired: signing key has expired.
- ErrKeyRevoked: signing key has been revoked.
- ErrSignatureFormatInvalid: signature cannot be parsed.
- ErrUnknownSignatureType: no verifier for this format.

This is more flexible than a linear trust scale and lets consumers
map verification results to arbitrary trust schemas (e.g. GitHub
Verified / Partially verified / Untrusted) using errors.Is().

The Verifier interface signature is unchanged; only the contract and
the result struct change.
Add a Details any field so that verifier plugins can attach
implementation-specific metadata to the result (e.g. key expiry,
trust chain, key source URI).

Consumers type-assert to the concrete type provided by the verifier
they are using. The core library never inspects this field.
// a PKCS#7 (S/MIME) signature.
x509SignatureFormat = signatureFormat{
[]byte("-----BEGIN SIGNED MESSAGE-----"),
[]byte("-----BEGIN CERTIFICATE-----"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supported by git, or implemented by any other git implementation? https://github.com/git/git/blob/795c338de725e13bd361214c6b768019fc45a2c1/gpg-interface.c#L65

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had that in the past, we no longer have - I just don't recall if my PR removed it or if that is already in main.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may have come back because I started working on this PR month ago. Will make sure to remove it.

@bb-Ricardo
Copy link
Copy Markdown

I had a quick look at it. From the signer part it makes sense to use a configured signer. But from the verifier it seems to make sense to be able to iterated over a list from preconfigured verifiers. As git commits in a tree can have different types of signatures I would like to be able to apply a preconfigured verifier based on the signature type.

Or the verifiers would implement a certain type of signature and then be based on the type.

@Bluebugs
Copy link
Copy Markdown
Contributor Author

Bluebugs commented Mar 8, 2026

I had a quick look at it. From the signer part it makes sense to use a configured signer. But from the verifier it seems to make sense to be able to iterated over a list from preconfigured verifiers. As git commits in a tree can have different types of signatures I would like to be able to apply a preconfigured verifier based on the signature type.

Or the verifiers would implement a certain type of signature and then be based on the type.

I was thinking of implementing chaining of verifiers out of tree following this pattern, but that would be in go-git/x/. I can see pro and cons to having it in tree. Pros is that the mechanism is always there included directly in the verifiers API. The cons is that now you aren't quite necessarily sure of the chain of plugin registered for doing verification. That was my reasoning for moving it out of tree, it make it explicit when you have a chain.

@bb-Ricardo
Copy link
Copy Markdown

bb-Ricardo commented Mar 10, 2026

I was thinking of implementing chaining of verifiers out of tree following this pattern, but that would be in go-git/x/. I can see pro and cons to having it in tree. Pros is that the mechanism is always there included directly in the verifiers API. The cons is that now you aren't quite necessarily sure of the chain of plugin registered for doing verification. That was my reasoning for moving it out of tree, it make it explicit when you have a chain.

You don't really need chaining as the type of signatures requires a certain verifier. And as we currently only have three types of signatures this would result in a GPG verifier, SSH verifier and a x509 verifier. And you would leave it to the go-git user to setup the verifier with the desired set of verification config.

// simple example
plugin.Register(plugin.ObjectSSHVerifier(),
    func() plugin.Verifier {
        v, _ := gossh.FromAllowedSignersFile("~/.ssh/allowed_signers")
        return v
    }
)

plugin.Register(plugin.ObjectGPGVerifier(),
    func() plugin.Verifier {
        v, _ := plugin.GPGVerifierWithKeyring("my-trusted-keyring.asc")
        return v
    }
)

I might have missed it but in order to verify for example a trust level, the verifier needs access to the commit/tag attributes like author, committer, timestamp, id without parsing it from the signed message again. Or is this intentional?

@cedric-appdirect
Copy link
Copy Markdown
Contributor

You don't really need chaining as the type of signatures requires a certain verifier. And as we currently only have three types of signatures this would result in a GPG verifier, SSH verifier and a x509 verifier. And you would leave it to the go-git user to setup the verifier with the desired set of verification config.

Indeed, that simplify things in a way. For my use case, that would work. Ideally, you would want the interface to not be interchangeable to avoid the user to make mistake that don't get caught at compilation time. Do you see some difference between this verifier that I could codify in the API?

Otherwise I can add just a SupportsSignatureType(object.SignatureType) bool to the plugin verifier API and this avoid the problem as go-git can ensure to call the right plugin for the right type.

I might have missed it but in order to verify for example a trust level, the verifier needs access to the commit/tag attributes like author, committer, timestamp, id without parsing it from the signed message again. Or is this intentional?

That's a good point. I could pass the commit itself, not just the []byte. My interest being mostly on ssh, it is good to have additional review indeed. I will add that to the Verifier API.

Comment on lines +7 to +8
func TestTrustLevel_String(t *testing.T) {
t.Parallel()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether trust levels are required for this level of abstraction. For core-tree, I'd rather we keep it agnostic to the verification method and trust level is a concept from GPG - IIRC.

}
}

func TestDetectSignatureType(t *testing.T) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the abstraction level of the core, I'd proactively not use actual verification methods the important thing is:

  • Is the new API flexible enough to account for any verification method?
  • Do we provide ways to handle failure scenarios in a sensible manner?

Comment on lines +71 to +78
// KeyID is the identifier of the signing key.
// For OpenPGP: the key ID (last 16 hex chars of fingerprint)
// For SSH: the key fingerprint (SHA256:...)
KeyID string

// PrimaryKeyFingerprint is the full fingerprint of the primary key.
// For OpenPGP subkeys, this differs from the signing key fingerprint.
PrimaryKeyFingerprint string
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like implementation details. Would a X.509 verifier require either of this?

The question is, how to enable that use case without hard-coding it on our exposed types?

@pjbgf
Copy link
Copy Markdown
Member

pjbgf commented Mar 20, 2026

Thank you both for looking at this. I should get the other PR ready soonish. Here's my initial feedback on the discussions I saw here (as well as the changes):

I was thinking of implementing chaining of verifiers out of tree following this pattern, but that would be in go-git/x/

@Bluebugs +1 on this approach. In theory any verifier can be a chain in itself. Meaning - we could have ssh/gpg static verifiers, each as different implementation. And a third one that is a chain that goes through whatever verifiers you pass at it.

we currently only have three types of signatures

@bb-Ricardo ideally the in-tree code would be oblivious to verifier implementation. If someone believes that they have a better way of signing/verifying commits, I'd like to enable that use case without having to change go-git's code base - pretty much along the lines of OCP. I think we may have achieved that for Signers, it would be great to do the same for verifiers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants