plumbing: object, add scheme-agnostic object verification API#2235
plumbing: object, add scheme-agnostic object verification API#2235pjbgf wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a scheme-agnostic, plugin-based Git object signature verification API (paralleling the existing signing plugin approach), while also refactoring commit/tag signature handling to use []byte and enabling streaming “encode-without-signature” payload generation for signing/verifying.
Changes:
- Added
plugin.Verifier/plugin.Verificationand theplugin.ObjectVerifier()registry key to support out-of-tree verification implementations. - Updated
Commit.Verify/Tag.Verifyto takecontext.Context+VerifyOptions and delegate to a configured (or registered)Verifier. - Refactored signature storage to
[]byteand changedEncodeWithoutSignatureto return a streamingio.Reader(viaio.WriterTo) to avoid full-payload buffering.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| x/plugin/plugin_verifier.go | Adds the verifier plugin contract and the ObjectVerifier plugin key. |
| worktree_commit.go | Stores generated commit signatures as []byte. |
| worktree_commit_test.go | Adjusts signature assertions for []byte commit signatures. |
| tests/objectverify/tag_test.go | Removes in-tree tag verification conformance tests (moved off-tree). |
| tests/objectverify/main_test.go | Removes in-tree object verification harness (moved off-tree). |
| tests/objectverify/commit_test.go | Removes in-tree commit verification conformance tests (moved off-tree). |
| signer.go | Switches signing to consume a streaming EncodeWithoutSignature() reader. |
| signer_test.go | Updates example output for []byte signatures. |
| repository.go | Updates tag signing path to return/store []byte signatures. |
| plumbing/object/verify.go | Introduces core scheme-agnostic verify helper + options. |
| plumbing/object/verify_test.go | Adds tests for verifier selection and registered default verifier behavior. |
| plumbing/object/verify_bench_test.go | Adds benchmarks focusing on streaming verification behavior. |
| plumbing/object/tag.go | Converts tag signatures to []byte, adds streaming EncodeWithoutSignature, and switches Verify to plugin-based verifier. |
| plumbing/object/tag_test.go | Updates tag tests for []byte signatures and new EncodeWithoutSignature shape; removes OpenPGP Verify tests. |
| plumbing/object/tag_scanner.go | Stores tag signature header data into []byte. |
| plumbing/object/signature.go | Adds streaming signedReader, refactors signature parsing helpers, and makes signature stripping stream-oriented. |
| plumbing/object/signature_test.go | Updates tests/fuzzing for the refactored signature parsing helpers. |
| plumbing/object/commit.go | Converts commit signatures to []byte, adds streaming EncodeWithoutSignature, and switches Verify to plugin-based verifier. |
| plumbing/object/commit_test.go | Updates commit tests for []byte signatures and new EncodeWithoutSignature shape; removes OpenPGP Verify tests. |
| plumbing/object/commit_scanner.go | Stores commit signature header data into []byte. |
| go.sum | Removes ProtonMail/go-crypto and related indirect sums. |
| go.mod | Removes ProtonMail/go-crypto and related indirect dependencies. |
| EXTENDING.md | Documents the new verification plugin API and usage patterns. |
| Method SignatureType | ||
| // Details carries scheme-specific data, for example an *openpgp.Entity | ||
| // for OpenPGP signatures. Callers type-assert on it based on Method. | ||
| Details any |
There was a problem hiding this comment.
@Bluebugs This should enable any additional scheme-specific information to be shared with users (as per separate thread).
Trust levels becomes an implementation detail of the verifier, as opposed to the API first class citizen. I haven't added them to the initial GPG implementation, but that is something we should be able to easily add as a follow-up.
There was a problem hiding this comment.
Nice. I haven't worked much on go-git lately as I don't want to overwhelm more the PR queue :-) But I might try to implement support for allowed_signers on top of your PR for go-git/x ( https://man7.org/linux/man-pages/man1/ssh-keygen.1.html#ALLOWED_SIGNERS ).
|
A discussion point around the future of Tag/Commits and the introduction of "read-only" alternatives: |
There was a problem hiding this comment.
These tests are for the actual implementation, which are now moved to go-git/x as per go-git/x#13.
Introduce a plugin-based, scheme-agnostic API for verifying Git object signatures, replacing the OpenPGP-only Commit.Verify/Tag.Verify. - Add plugin.Verifier, plugin.Verification and the plugin.ObjectVerifier() key (x/plugin), mirroring the existing Signer plugin. - Commit.Verify / Tag.Verify now take a context and VerifyOption(s), using the verifier from WithVerifier or, by default, the registered ObjectVerifier. Core ships only the contract; concrete verifiers live off-tree in go-git/x. - Resolve the default verifier with plugin.Has before plugin.Get, so a Verify before registration does not freeze the plugin entry and block a later plugin.Register. - EncodeWithoutSignature returns a streaming io.Reader whose WriteTo feeds the verifier's hash directly, avoiding a full-payload buffer. - Signature and SignatureSHA256 are now []byte, removing string<->[]byte conversions in the sign/verify paths. - Multi-signature rejection moves to the OpenPGP verifier; core stays scheme-agnostic and no longer depends on ProtonMail/go-crypto. - Move the gpg/git conformance suite to go-git/x. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Entire-Checkpoint: 91381cbf1e8e
| v, err := Verify(ctx, attestedPayload(c.src, c.Hash), sig, opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| v.Object = c.Hash | ||
| return v, nil |
| v, err := Verify(ctx, attestedPayload(t.src, t.Hash), sig, opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| v.Object = t.Hash | ||
| return v, nil |
| func FuzzParseSignedBytes(f *testing.F) { | ||
| f.Add([]byte(openPGPSignatureFormat[0])) | ||
| f.Add([]byte(x509SignatureFormat[0])) | ||
| f.Add([]byte(sshSignatureFormat[0])) | ||
| for _, begin := range signatureBegins { | ||
| f.Add(begin) | ||
| } | ||
|
|
Make commit and tag signing and verification O(1) in memory, and give signing and verification the payloads they each need: - EncodeWithoutSignature returns an io.Reader that streams the canonical encoding of the current fields via io.WriterTo — the exact bytes Encode stores — so a signature computed over it stays verifiable once the object is encoded and stored. Signature stripping for both commits (header-based) and tags (inline trailing block) streams without holding the body in memory. - Expose two composable primitives: object.SignedPayload, which returns the signature-stripped bytes of a stored object, and object.Verify, which checks a detached signature over an io.Reader payload. - Commit.Verify and Tag.Verify prefer the stored source bytes — the bytes an existing signature actually covers, even when the stored form is not byte-identical to go-git's canonical encoding — and prove the exported fields still mirror them against a snapshot captured by Decode (signed objects only). Mutated or in-memory objects fall back to the canonical encoding of the current fields, the same bytes signing covers, so a tampered struct can never verify against the stored object's signature. - While the payload streams to the verifier, the raw source bytes are teed through an object hasher in the same pass: ErrObjectIntegrity is returned when they no longer hash to the object id, and on success Verification.Object attests the verified id. A zero Object means the payload was re-encoded from in-memory state. - sha256 commits are verified with the signature from their gpgsig-sha256 header, selected by the object's hash format via the new ObjectID.Format, mirroring upstream's per-algorithm signature headers (commit.c:gpg_sig_headers). Tags are always verified with their inline trailing signature: Git appends a tag's own signature inline regardless of hash format, and a gpgsig or gpgsig-sha256 header on a tag carries the other rendition's signature in hash compatibility mode, never its own (builtin/tag.c:do_sign, gpg-interface.c:parse_signature). Add benchmarks for the sign and verify paths, and document in EXTENDING.md that signature-block policy — such as rejecting multiple concatenated signatures — is the verifier's responsibility. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Entire-Checkpoint: f15bfe3e3aac
FromObjectFormat and NewHasher built SHA1/SHA256 hashers via crypto.SHA1.New() / crypto.SHA256.New(), which resolve through Go's global crypto hash registry. sha1cd v0.6.0 no longer registers crypto.SHA1 there from its init, so a binary that links plumbing/object without also linking crypto/sha1 — such as the go-118-fuzz-build fuzz harness — panics with "crypto: requested hash function #3 is unavailable" the first time an object is hashed. Route both constructors through plumbing/hash.New, whose registry maps SHA1 to sha1cd.New directly and never consults the global registry, so object hashing is self-contained regardless of what else the binary links. Behaviour is unchanged for normal builds, where crypto.SHA1 already resolved to sha1cd. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Entire-Checkpoint: 51582cf8f76d
A plugin.Verifier reporting success with a nil Verification would panic in the stored-bytes path of Tag.Verify and Commit.Verify when attesting the object id. Verify now returns ErrNilVerification instead. Also add a differential fuzz test for lastSignatureBlockOffset against parseSignedBytes. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Entire-Checkpoint: 092c6f0892e9
Introduce a plugin-based, scheme-agnostic API for verifying Git object signatures, replacing the OpenPGP-only
Commit.Verify/Tag.Verify.This results in:
Verifierin-tree, which aligns with the approach taken forSigner.ObjectVerifierimplementations will be placed ingo-git/xso that users of go-git that don't need the third-party dependencies won't have them in their projects.Global verifier
Explicit verifier
Proposed implementation for gpg and ssh can be seen here.
Supersedes #1883.
Relates to #1869.