Skip to content

Commit c5a1f43

Browse files
authored
Offline verification: refactor to make it clear no signature checks are happening. (#319)
- Rename rekor.VerifyOffline -> Rekor.VerifyInclusion to make it clear that is is not checking the signature content. - Require callers to pass in the certificate instead of trying to infer it from the signature to make it more likely that users have verified the commit already. Signed-off-by: Billy Lynch <billy@chainguard.dev>
1 parent 8a76ba2 commit c5a1f43

File tree

3 files changed

+23
-19
lines changed

3 files changed

+23
-19
lines changed

internal/gitsign/gitsign_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func (fakeRekor) Verify(ctx context.Context, commitSHA string, cert *x509.Certif
154154
return nil, nil
155155
}
156156

157-
func (fakeRekor) VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error) {
157+
func (fakeRekor) VerifyInclusion(ctx context.Context, sig []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) {
158158
return nil, nil
159159
}
160160

pkg/git/verify.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func Verify(ctx context.Context, git Verifier, rekor rekor.Verifier, data, sig [
7474
}
7575
claims = append(claims, NewClaim(ClaimValidatedSignature, true))
7676

77-
if tlog, err := rekor.VerifyOffline(ctx, sig); err == nil {
77+
if tlog, err := rekor.VerifyInclusion(ctx, sig, cert); err == nil {
7878
return &VerificationSummary{
7979
Cert: cert,
8080
LogEntry: tlog,

pkg/rekor/rekor.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import (
4949
// Verifier represents a mechanism to get and verify Rekor entries for the given Git data.
5050
type Verifier interface {
5151
Verify(ctx context.Context, commitSHA string, cert *x509.Certificate) (*models.LogEntryAnon, error)
52-
VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error)
52+
VerifyInclusion(ctx context.Context, sig []byte, cert *x509.Certificate) (*models.LogEntryAnon, error)
5353
}
5454

5555
// Writer represents a mechanism to write content to Rekor.
@@ -179,7 +179,7 @@ func rekorPubsFromClient(rekorClient *client.Rekor) (*cosign.TrustedTransparency
179179
// 1. Searching Rekor for an entry matching the commit SHA + cert.
180180
// 2. Use the same cert to verify the commit content.
181181
//
182-
// Note: While not truly deprecated, Client.VerifyOffline is generally preferred.
182+
// Note: While not truly deprecated, using offline verification is generally preferred.
183183
// This function relies on non-GA behavior of Rekor, and remains for backwards
184184
// compatibility with older signatures.
185185
func (c *Client) Verify(ctx context.Context, commitSHA string, cert *x509.Certificate) (*models.LogEntryAnon, error) {
@@ -245,9 +245,10 @@ func (c *Client) PublicKeys() *cosign.TrustedTransparencyLogPubKeys {
245245
return c.publicKeys
246246
}
247247

248-
// VerifyOffline verifies a signature using offline verification.
249-
// Unlike Client.Verify, only the commit content is considered during verification.
250-
func (c *Client) VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error) {
248+
// VerifyInclusion verifies a signature's inclusion in Rekor using offline verification.
249+
// NOTE: This does **not** verify the correctness of the signature against the content.
250+
// Prefer using [git.Verify] instead for complete verification.
251+
func (c *Client) VerifyInclusion(ctx context.Context, sig []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) {
251252
// Try decoding as PEM
252253
var der []byte
253254
if blk, _ := pem.Decode(sig); blk != nil {
@@ -261,27 +262,30 @@ func (c *Client) VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntr
261262
return nil, fmt.Errorf("failed to parse signature: %w", err)
262263
}
263264

264-
// Generate verification options.
265-
certs, err := sd.GetCertificates()
266-
if err != nil {
267-
return nil, fmt.Errorf("error getting signature certs: %w", err)
268-
}
269-
if len(certs) == 0 {
270-
return nil, fmt.Errorf("no certificates found in signature")
271-
}
272-
cert := certs[0]
273-
274-
// Recompute HashedRekord body by rehashing the authenticated attributes.
265+
// Extract signer from signature. The spec allows for multiple signers, but in
266+
// practice for Git commits there's typically only ever 1 signer (the committer).
275267
r := sd.Raw()
276268
if len(r.SignerInfos) == 0 {
277269
return nil, fmt.Errorf("no signers found in signature")
278270
}
279271
si := r.SignerInfos[0]
280-
message, err := si.SignedAttrs.MarshaledForVerification()
272+
273+
// Double check cert matches the signer. This technically isn't needed
274+
// since if this didn't match then the verify below should also fail,
275+
// but this helps us distinguish the error in the unlikely event this does happen.
276+
if _, err := si.FindCertificate([]*x509.Certificate{cert}); err != nil {
277+
return nil, fmt.Errorf("signer certificate mismatch: %w", err)
278+
}
279+
280+
// Get HashedRekord body from the signature.
281+
// We are assuming here that the signature has already been authenticated against the
282+
// cert, so it is okay to rely the precomputed checksum in the SignerInfo.
283+
message, err := si.GetMessageDigestAttribute()
281284
if err != nil {
282285
return nil, err
283286
}
284287

288+
// Reassemble the tlog entry from the signature pieces.
285289
tlog, err := rekoroid.ToLogEntry(ctx, message, si.Signature, cert, si.UnsignedAttrs)
286290
if err != nil {
287291
return nil, err

0 commit comments

Comments
 (0)