Conversation
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-----"), |
There was a problem hiding this comment.
Is this supported by git, or implemented by any other git implementation? https://github.com/git/git/blob/795c338de725e13bd361214c6b768019fc45a2c1/gpg-interface.c#L65
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It may have come back because I started working on this PR month ago. Will make sure to remove it.
|
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 |
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? |
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
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. |
| func TestTrustLevel_String(t *testing.T) { | ||
| t.Parallel() |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
| // 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 |
There was a problem hiding this comment.
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?
|
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):
@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.
@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 |
Added on top of #1860 a verify plugin that could be used like so:
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 :-(