Skip to content

plumbing: object, add scheme-agnostic object verification API#2235

Open
pjbgf wants to merge 4 commits into
go-git:mainfrom
pjbgf:verifier
Open

plumbing: object, add scheme-agnostic object verification API#2235
pjbgf wants to merge 4 commits into
go-git:mainfrom
pjbgf:verifier

Conversation

@pjbgf

@pjbgf pjbgf commented Jun 26, 2026

Copy link
Copy Markdown
Member

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.
  • 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.

This results in:

  • By default, go-git has no default Verifier in-tree, which aligns with the approach taken for Signer.
  • The ObjectVerifier implementations will be placed in go-git/x so that users of go-git that don't need the third-party dependencies won't have them in their projects.

Global verifier

func init() {
    plugin.Register(plugin.ObjectVerifier(), func() plugin.Verifier {
        v, _ := gpg.FromKeyRing(myKeyRing)
        return v
    })
}

v, err := commit.Verify(ctx)

Explicit verifier

verifier, _ := gpg.FromKeyRing(myKeyRing)
v, err := tag.Verify(ctx, object.WithVerifier(verifier))

Proposed implementation for gpg and ssh can be seen here.

Supersedes #1883.
Relates to #1869.

Copilot AI review requested due to automatic review settings June 26, 2026 13:19

Copilot AI left a comment

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.

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.Verification and the plugin.ObjectVerifier() registry key to support out-of-tree verification implementations.
  • Updated Commit.Verify / Tag.Verify to take context.Context + VerifyOptions and delegate to a configured (or registered) Verifier.
  • Refactored signature storage to []byte and changed EncodeWithoutSignature to return a streaming io.Reader (via io.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.

Comment thread plumbing/object/verify.go
Comment thread plumbing/object/verify_test.go
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@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.

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.

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 ).

@pjbgf

pjbgf commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

A discussion point around the future of Tag/Commits and the introduction of "read-only" alternatives:

BenchmarkReadCommitVerify/body=200-24         	 2338753	       459.2 ns/op	     304 B/op	       7 allocs/op
BenchmarkReadCommitVerify/body=4096-24        	  579508	      2097 ns/op	     304 B/op	       7 allocs/op
BenchmarkReadCommitVerify/body=262144-24      	   10000	    112914 ns/op	     304 B/op	       7 allocs/op
BenchmarkReadTagVerify/body=200-24            	 2180050	       582.5 ns/op	     376 B/op	       9 allocs/op
BenchmarkReadTagVerify/body=4096-24           	  497545	      2438 ns/op	     376 B/op	       9 allocs/op
BenchmarkReadTagVerify/body=262144-24         	   10000	    120918 ns/op	     377 B/op	       9 allocs/op


BenchmarkCommitVerify/body=200-24         	  662184	      1893 ns/op	 178.03 MB/s	    2156 B/op	      39 allocs/op
BenchmarkCommitVerify/body=4096-24        	  172166	      7959 ns/op	 531.82 MB/s	   20273 B/op	      41 allocs/op
BenchmarkCommitVerify/body=262144-24      	    3795	    354616 ns/op	 739.62 MB/s	 1079762 B/op	     110 allocs/op
BenchmarkTagVerify/body=200-24            	  516109	      1938 ns/op	 160.99 MB/s	    2316 B/op	      37 allocs/op
BenchmarkTagVerify/body=4096-24           	  175252	      6664 ns/op	 631.48 MB/s	   20017 B/op	      38 allocs/op
BenchmarkTagVerify/body=262144-24         	    3724	    377877 ns/op	 694.02 MB/s	 1079485 B/op	     107 allocs/op

Comment thread tests/objectverify/tag_test.go Outdated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These tests are for the actual implementation, which are now moved to go-git/x as per go-git/x#13.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 28 out of 29 changed files in this pull request and generated 3 comments.

Comment thread x/plumbing/object/readobject.go Outdated
Comment thread x/plumbing/object/readobject.go Outdated
Comment thread x/plumbing/object/readobject.go Outdated
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

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.

Comment thread plumbing/object/commit.go
Comment on lines +508 to +513
v, err := Verify(ctx, attestedPayload(c.src, c.Hash), sig, opts...)
if err != nil {
return nil, err
}
v.Object = c.Hash
return v, nil
Comment thread plumbing/object/tag.go
Comment on lines +359 to +364
v, err := Verify(ctx, attestedPayload(t.src, t.Hash), sig, opts...)
if err != nil {
return nil, err
}
v.Object = t.Hash
return v, nil
Comment on lines 165 to 169
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)
}

pjbgf added 2 commits July 15, 2026 23:36
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
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.

3 participants