Skip to content

Commit

Permalink
Refactor Sign func into 2 separate functions, rework verification doc.
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Lynch <billy@chainguard.dev>
  • Loading branch information
wlynch committed May 18, 2023
1 parent 6f4bc13 commit 2b31102
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 26 deletions.
71 changes: 67 additions & 4 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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 <tag>`), **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)
6 changes: 4 additions & 2 deletions internal/commands/root/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 14 additions & 7 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 8 additions & 13 deletions internal/signature/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pkg/rekor/rekor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2b31102

Please sign in to comment.