Skip to content

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

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

plumbing: object, add scheme-agnostic object verification API#2235
pjbgf wants to merge 3 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.

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.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
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 on lines +47 to +53
v := cfg.verifier
if v == nil {
var err error
if v, err = plugin.Get(plugin.ObjectVerifier()); err != nil {
return nil, err
}
}
Comment on lines +32 to +37
func (f *fakeVerifier) Verify(_ context.Context, message io.Reader, signature []byte) (*plugin.Verification, error) {
b, _ := io.ReadAll(message)
f.gotMessage = b
f.gotSignature = signature
return f.result, f.err
}
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 added 2 commits June 26, 2026 16:33
Verify resolved the verifier via plugin.Get, which freezes the plugin
entry even when nothing is registered: a single Verify before
registration would make a later plugin.Register(ObjectVerifier) fail with
ErrFrozen. Check plugin.Has first and return plugin.ErrNotFound without
freezing, mirroring the auto-signer path.

Also propagate the read error in the test verifier double so a mid-stream
EncodeWithoutSignature failure cannot be masked as a successful verify.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
…views

Make commit and tag signing and verification O(1) in memory: the payload a
signature is computed over is now produced by streaming the object through
io.WriterTo with the signature stripped, instead of buffering a re-encoded
copy. EncodeWithoutSignature returns an io.Reader, and 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 — and re-express Commit.Verify
and Tag.Verify on top of them.

Add x/plumbing/object with immutable ReadCommit / ReadTag views. Their fields
are exposed only through accessors (slice accessors return copies), so they
cannot be mutated after decoding; this lets Verify reproduce the signed
payload directly from the stored source bytes, in constant memory regardless
of object size.

Add benchmarks for the sign and verify paths.

Assisted-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
@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

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