diff --git a/docs/verification.md b/docs/verification.md index 0fb65efd..99d70bf0 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -2,6 +2,8 @@ ## Offline Verification +### How we sign + In offline Rekor storage mode Gitsign will store a HashedRekord in Rekor corresponding to the commit content. @@ -15,6 +17,23 @@ The resulting Rekor log entry fields and inclusion proof will be stored in the PKCS7 object as unauthenticated (i.e. not included in the cryptographic signature) attributes. +### How we verify + +1. Recompute and compare commit content checksum from commit. +2. Get Rekor LogEntry from signature. +3. Verify Certificate against commit content checksum (ignoring cert NotAfter time). +4. (if present) Verify signature against TSA cert. +5. Verify Rekor LogEntry inclusion (offline). + +### What's stored in the commit signature + +- Commit content checksum (sha256) +- Commit signing time (untrusted system time) +- Protobuf encoded [Rekor TransparencyLogEntry](https://github.com/sigstore/protobuf-specs/blob/91485b44360d343dadd98fb7297a500f05e0b5b1/protos/sigstore_rekor.proto#L91) +- (optional) TSA signature + cert + +Sample encoded TransparencyLogEntry: + ``` unauth_attr: object: Rekor TransparencyLogEntry proto (1.3.6.1.4.1.57264.3.1) @@ -83,21 +102,50 @@ unauth_attr: 030c - 69 51 75 4d 53 49 59 3d-0a iQuMSIY=. ``` -These OIDs are defined by [Rekor](https://github.com/sigstore/rekor) and are +This OID are defined by [Rekor](https://github.com/sigstore/rekor) and are used during verification to reconstruct the Rekor log entry and verify the commit signature. +### What's stored in Rekor + +HashedRekord containing: + +- Commit content checksum +- Fulcio certificate + - Public Key + - [Signer Identity info](https://github.com/sigstore/fulcio/blob/main/docs/oidc.md) + ## Online Verification -In online Rekor storage mode Gitsign will store the Git commit SHA in rekor -rather that persisting the Rekor log details in the commit itself. Gitsign is in -the process of migrating clients to offline verification, but this section +Note: Gitsign is in the process of migrating clients to offline verification, but this section explains how verification used to work. +### How we sign + +In online Rekor storage mode Gitsign will store the Git commit SHA in Rekor +rather that persisting the Rekor log details in the commit itself. This works by: + +1. Get Fulcio Cert +2. Sign the commit body using cert +3. Generate commit SHA (commit doesn't actually exist yet because the commit includes the signature) +4. Sign the commit SHA using the same cert +5. Upload HashedRekord of commit SHA to Rekor +6. Store the signed commit body signature in commit + +### How we verify + As part of signature verification, `gitsign` not only checks that the given signature matches the commit, but also that the commit exists within the Rekor transparency log. +This is done by: + +1. Recompute and compare commit content checksum from commit. +2. Validate the checksum signature using the public key in the signature's cert (ignoring cert NotAfter time). +3. (if present) Verify signature against TSA cert. +4. Search Rekor for an entry matching the commit SHA + cert. (this is what makes the process online) +5. Verify Rekor LogEntry inclusion (offline). + We can manually validate that the commit exists in the transparency log by running: @@ -199,3 +247,18 @@ though they signed different content! Note that for Git tags, the annotated tag object SHA is what is used (i.e. the output of `git rev-parse `), **not** the SHA of the underlying tagged commit. + +### What's stored in the commit signature + +- Commit content checksum (sha256) +- Commit signing time (untrusted system time) +- (optional) TSA signature + cert + +### What's stored in Rekor + +HashedRekord containing: + +- Commit SHA checksum +- Fulcio certificate + - Public Key + - [Signer Identity info](https://github.com/sigstore/fulcio/blob/main/docs/oidc.md) \ No newline at end of file diff --git a/internal/commands/root/sign.go b/internal/commands/root/sign.go index 288f6df8..d1ff55a4 100644 --- a/internal/commands/root/sign.go +++ b/internal/commands/root/sign.go @@ -88,10 +88,12 @@ func commandSign(o *options, s *gsio.Streams, args ...string) error { opts.UserName = o.Config.CommitterName opts.UserEmail = o.Config.CommitterEmail } + + var fn git.SignFunc = git.LegacySHASign if o.Config.RekorMode == "offline" { - opts.RekorAddr = o.Config.Rekor + fn = git.Sign } - resp, err := git.Sign(ctx, rekor, userIdent, dataBuf.Bytes(), opts) + resp, err := fn(ctx, rekor, userIdent, dataBuf.Bytes(), opts) if err != nil { return fmt.Errorf("failed to sign message: %w", err) } diff --git a/internal/git/git.go b/internal/git/git.go index 5b9e5d28..2aa9294d 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -26,19 +26,26 @@ import ( "github.com/sigstore/gitsign/pkg/rekor" ) +type SignFunc func(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) + +// Sign signs the commit, uploading a HashedRekord of the commit content to Rekor +// and embedding the Rekor log entry in the signature. +// This is suitable for offline verification. func Sign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) { + opts.Rekor = rekor + return signature.Sign(ctx, ident, data, opts) +} + +// LegacySHASign is the old-style signing that signs the commit content, but uploads a signed SHA to Rekor. +// Verification for this style of signing relies on the Rekor Search API to match the signed SHA + commit content certs, +// and cannot be done offline. +// This may be removed in the future. +func LegacySHASign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) (*signature.SignResponse, error) { resp, err := signature.Sign(ctx, ident, data, opts) if err != nil { return nil, fmt.Errorf("failed to sign message: %w", err) } - // We're using offline verification style signing - nothing more to do. - if resp.LogEntry != nil { - return resp, nil - } - - // Legacy SHA based signing - only do if we didn't get a tlog entry back. - // This uploads the commit SHA + sig(commit SHA) to the tlog using the same // key used to sign the commit data itself. // Since the commit SHA ~= hash(commit data + sig(commit data)) and we're diff --git a/internal/signature/sign.go b/internal/signature/sign.go index dda37efb..eb6033a5 100644 --- a/internal/signature/sign.go +++ b/internal/signature/sign.go @@ -54,9 +54,9 @@ type SignOptions struct { // will fail if the Fulcio identity SAN email does not match the git committer email. UserEmail string - // Rekor address - if specified, Rekor details are embedded directly in the + // Rekor client - if specified, Rekor details are embedded directly in the // signature output. - RekorAddr string + Rekor rekor.Writer } // Identity is a copy of smimesign.Identity to allow for compatibility without @@ -80,7 +80,7 @@ type SignResponse struct { Signature []byte Cert *x509.Certificate // LogEntry is the Rekor tlog entry from the signing operation. - // This is only populated if offline signing mode was used. + // This is only populated if offline signing mode was used (e.g. SignOpts.Rekor was passed in) LogEntry *models.LogEntryAnon } @@ -89,7 +89,7 @@ type SignResponse struct { func Sign(ctx context.Context, ident Identity, body []byte, opts SignOptions) (*SignResponse, error) { cert, err := ident.Certificate() if err != nil { - return nil, fmt.Errorf("failed to get idenity certificate: %w", err) + return nil, fmt.Errorf("failed to get identity certificate: %w", err) } // If specified, check if retrieved identity matches the expected identity. @@ -108,7 +108,7 @@ func Sign(ctx context.Context, ident Identity, body []byte, opts SignOptions) (* signer, err := ident.Signer() if err != nil { - return nil, fmt.Errorf("failed to get idenity signer: %w", err) + return nil, fmt.Errorf("failed to get identity signer: %w", err) } sd, err := cms.NewSignedData(body) @@ -144,9 +144,9 @@ func Sign(ctx context.Context, ident Identity, body []byte, opts SignOptions) (* } var lea *models.LogEntryAnon - if opts.RekorAddr != "" { + if opts.Rekor != nil { var err error - lea, err = attachRekorLogEntry(ctx, sd, cert, opts.RekorAddr) + lea, err = attachRekorLogEntry(ctx, sd, cert, opts.Rekor) if err != nil { return nil, err } @@ -243,12 +243,7 @@ func matchSAN(cert *x509.Certificate, name, email string) bool { return false } -func attachRekorLogEntry(ctx context.Context, sd *cms.SignedData, cert *x509.Certificate, addr string) (*models.LogEntryAnon, error) { - rekor, err := rekor.New(addr) - if err != nil { - return nil, err - } - +func attachRekorLogEntry(ctx context.Context, sd *cms.SignedData, cert *x509.Certificate, rekor rekor.Writer) (*models.LogEntryAnon, error) { // Marshal commit attributes as it was signed. raw := sd.Raw() // We're creating a new signature, so this should generally always be len 1. diff --git a/pkg/rekor/rekor.go b/pkg/rekor/rekor.go index ced631ff..6fdb8b2f 100644 --- a/pkg/rekor/rekor.go +++ b/pkg/rekor/rekor.go @@ -55,6 +55,7 @@ type Verifier interface { // Writer represents a mechanism to write content to Rekor. type Writer interface { Write(ctx context.Context, commitSHA string, sig []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) + WriteMessage(ctx context.Context, message, signature []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) } // Client implements a basic rekor implementation for writing and verifying Rekor data.