Skip to content

Commit

Permalink
Add offline verification
Browse files Browse the repository at this point in the history
This adds the ability for gitsign commits to include rekor log entry
details to enable offline verification of commits.

- Adds new unauthenticated attributes corresponding to Rekor log entry
  values.
- Adds support for offline commit verification along side existing SHA
  based online verification.
- Adds config option for users to select offline or online storage
  options. Defaults to existing online behavior to allow users to
  opt-in, this will be changed to offline after a release.
- Adds e2e test for offline verification.

Signed-off-by: Billy Lynch <billy@chainguard.dev>
  • Loading branch information
wlynch committed Jan 17, 2023
1 parent f9c532b commit c309c66
Show file tree
Hide file tree
Showing 18 changed files with 801 additions and 237 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ jobs:
# Verify commit
git verify-commit HEAD
# Extra debug info
git cat-file commit HEAD | sed -n '/BEGIN/, /END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print -print_certs -text
- name: Test Sign and Verify commit - offline verification
env:
GITSIGN_REKOR_MODE: "offline"
run: |
set -e
# Sign commit
git commit --allow-empty -S --message="Signed commit"
# Verify commit
git verify-commit HEAD
# Extra debug info
git cat-file commit HEAD | sed -n '/BEGIN/, /END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print -print_certs -text
- name: Debug log
Expand Down
395 changes: 179 additions & 216 deletions README.md

Large diffs are not rendered by default.

174 changes: 174 additions & 0 deletions docs/verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Verification

## Offline Verification

In offline Rekor storage mode Gitsign will store a HashedRekord in Rekor
corresponding to the commit content.

Unfortunately this is a bit complex to query manually. Roughly this is:

```
sha256(der(sort(system time | commit data | content type)))
```

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.

```
unauth_attr:
object: undefined (1.3.6.1.4.1.57264.3.8)
value.set:
INTEGER:6954358
object: undefined (1.3.6.1.4.1.57264.3.9)
value.set:
INTEGER:6954357
object: undefined (1.3.6.1.4.1.57264.3.1)
value.set:
INTEGER:1673643613
object: undefined (1.3.6.1.4.1.57264.3.3)
value.set:
INTEGER:11117788
object: undefined (1.3.6.1.4.1.57264.3.2)
value.set:
PRINTABLESTRING:c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d
object: undefined (1.3.6.1.4.1.57264.3.7)
value.set:
PRINTABLESTRING:373443ac6ee5e01d4bfa00666f79d5c7cee0380684ebe571fc98bdffea82f972
object: undefined (1.3.6.1.4.1.57264.3.4)
value.set:
OCTET STRING:
0000 - 30 45 02 20 00 d0 88 ff-91 18 75 1c 90 0E. ......u..
000d - 4c aa f3 37 94 45 a8 ca-1e a4 de 60 10 L..7.E.....`.
001a - 0a 22 69 03 c9 2d d2 0e-1a 9f 02 21 00 ."i..-.....!.
0027 - af cd 78 85 f2 66 5f 22-c5 d3 a2 5c fc ..x..f_"...\.
0034 - e2 c1 fe 0c f2 27 aa f0-fa fd 73 ca 5d .....'....s.]
0041 - 58 98 9c 00 df 5c X....\
object: undefined (1.3.6.1.4.1.57264.3.5)
value.set:
UTF8STRING:rekor.sigstore.dev - 2605736670972794746
```

These OIDs 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.

## 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
explains how verification used to work.

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.

We can manually validate that the commit exists in the transparency log by
running:

```sh
$ uuid=$(rekor-cli search --artifact <(git rev-parse HEAD | tr -d '\n') | tail -n 1)
$ rekor-cli get --uuid=$uuid --format=json | jq .
LogID: c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d
Index: 2212633
IntegratedTime: 2022-05-02T20:51:49Z
UUID: d0444ed9897f31fefc820ade9a706188a3bb030055421c91e64475a8c955ae2c
Body: {
"HashedRekordObj": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "05b4f02a24d1c4c2c95dacaee30de2a6ce4b5b88fa981f4e7b456b76ea103141"
}
},
"signature": {
"content": "MEYCIQCeZwhnq9dgS7ZvU2K5m785V6PqqWAsmkNzAOsf8F++gAIhAKfW2qReBZL34Xrzd7r4JzUlJbf5eoeUZvKT+qsbbskL",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNGVENDQVp1Z0F3SUJBZ0lVQUxZY1ZSbUZTcG05VnhJTjdIVzdtaHBPeSs4d0NnWUlLb1pJemowRUF3TXcKS2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweQpNakExTURJeU1EVXhORGRhRncweU1qQTFNREl5TVRBeE5EWmFNQUF3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPClBRTUJCd05DQUFUc1lFdG5xaWpaTlBPRG5CZWx5S1dIWHQ3YndtWElpK2JjeEcrY2gyQUZRaGozdHcyUEJ2RmkKenBwWm5YRVNWUnZEMU1lUXBmWUt0QnF6RHFjOVRoSTRvNEhJTUlIRk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBVApCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBekFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCU2dzZW9ECnhRaEtjSk1oMnFPZ0MweFZTZE1HUFRBZkJnTlZIU01FR0RBV2dCUll3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYKK2pBaUJnTlZIUkVCQWY4RUdEQVdnUlJpYVd4c2VVQmphR0ZwYm1kMVlYSmtMbVJsZGpBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dDZ1lJS29aSXpqMEVBd01ECmFBQXdaUUl4QUsrKzliL25CZlVWNGdlRlNBRE9nUjQrdW5zaDArU2tpdWJsT0o4QmloWnNUTk9VcjNmd2ZXNngKblBrcCtTeTFFd0l3ZE91bFdvcDNvSlYvUW83ZmF1MG1sc3kwTUNtM2xCZ3l4bzJscEFhSTRnRlJ4R0UyR2hwVgo3TitrQ29TMUEyNFMKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
}
}
}
}

$ sig=$(rekor-cli get --uuid=$uuid --format=json | jq -r .Body.HashedRekordObj.signature.content)
$ cert=$(rekor-cli get --uuid=$uuid --format=json | jq -r .Body.HashedRekordObj.signature.publicKey.content)
$ cosign verify-blob --cert <(echo $cert | base64 --decode) --signature <(echo $sig | base64 --decode) <(git rev-parse HEAD | tr -d '\n')
tlog entry verified with uuid: d0444ed9897f31fefc820ade9a706188a3bb030055421c91e64475a8c955ae2c index: 2212633
Verified OK
$ echo $cert | base64 --decode | openssl x509 -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
b6:1c:55:19:85:4a:99:bd:57:12:0d:ec:75:bb:9a:1a:4e:cb:ef
Signature Algorithm: ecdsa-with-SHA384
Issuer: O=sigstore.dev, CN=sigstore
Validity
Not Before: May 2 20:51:47 2022 GMT
Not After : May 2 21:01:46 2022 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:ec:60:4b:67:aa:28:d9:34:f3:83:9c:17:a5:c8:
a5:87:5e:de:db:c2:65:c8:8b:e6:dc:c4:6f:9c:87:
60:05:42:18:f7:b7:0d:8f:06:f1:62:ce:9a:59:9d:
71:12:55:1b:c3:d4:c7:90:a5:f6:0a:b4:1a:b3:0e:
a7:3d:4e:12:38
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
A0:B1:EA:03:C5:08:4A:70:93:21:DA:A3:A0:0B:4C:55:49:D3:06:3D
X509v3 Authority Key Identifier:
keyid:58:C0:1E:5F:91:45:A5:66:A9:7A:CC:90:A1:93:22:D0:2A:C5:C5:FA

X509v3 Subject Alternative Name: critical
email:billy@chainguard.dev
1.3.6.1.4.1.57264.1.1:
https://github.com/login/oauth
Signature Algorithm: ecdsa-with-SHA384
30:65:02:31:00:af:be:f5:bf:e7:05:f5:15:e2:07:85:48:00:
ce:81:1e:3e:ba:7b:21:d3:e4:a4:8a:e6:e5:38:9f:01:8a:16:
6c:4c:d3:94:af:77:f0:7d:6e:b1:9c:f9:29:f9:2c:b5:13:02:
30:74:eb:a5:5a:8a:77:a0:95:7f:42:8e:df:6a:ed:26:96:cc:
b4:30:29:b7:94:18:32:c6:8d:a5:a4:06:88:e2:01:51:c4:61:
36:1a:1a:55:ec:df:a4:0a:84:b5:03:6e:12
-----BEGIN CERTIFICATE-----
MIICFTCCAZugAwIBAgIUALYcVRmFSpm9VxIN7HW7mhpOy+8wCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MjA1MDIyMDUxNDdaFw0yMjA1MDIyMTAxNDZaMAAwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAATsYEtnqijZNPODnBelyKWHXt7bwmXIi+bcxG+ch2AFQhj3tw2PBvFi
zppZnXESVRvD1MeQpfYKtBqzDqc9ThI4o4HIMIHFMA4GA1UdDwEB/wQEAwIHgDAT
BgNVHSUEDDAKBggrBgEFBQcDAzAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSgseoD
xQhKcJMh2qOgC0xVSdMGPTAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF
+jAiBgNVHREBAf8EGDAWgRRiaWxseUBjaGFpbmd1YXJkLmRldjAsBgorBgEEAYO/
MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwCgYIKoZIzj0EAwMD
aAAwZQIxAK++9b/nBfUV4geFSADOgR4+unsh0+SkiublOJ8BihZsTNOUr3fwfW6x
nPkp+Sy1EwIwdOulWop3oJV/Qo7fau0mlsy0MCm3lBgyxo2lpAaI4gFRxGE2GhpV
7N+kCoS1A24S
-----END CERTIFICATE-----
```

Notice that **the Rekor entry uses the same cert that was used to generate the
git commit signature**. This can be used to correlate the 2 messages, even
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.
8 changes: 6 additions & 2 deletions internal/commands/root/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,16 @@ func commandSign(o *options, s *gsio.Streams, args ...string) error {
return fmt.Errorf("failed to create rekor client: %w", err)
}

sig, cert, tlog, err := git.Sign(ctx, rekor, userIdent, dataBuf.Bytes(), signature.SignOptions{
opts := signature.SignOptions{
Detached: o.FlagDetachedSignature,
TimestampAuthority: o.Config.TimestampURL,
Armor: o.FlagArmor,
IncludeCerts: o.FlagIncludeCerts,
})
}
if o.Config.RekorMode == "offline" {
opts.RekorAddr = o.Config.Rekor
}
sig, cert, tlog, err := git.Sign(ctx, rekor, userIdent, dataBuf.Bytes(), opts)
if err != nil {
return fmt.Errorf("failed to sign message: %w", err)
}
Expand Down
11 changes: 11 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ type Config struct {

// Address of Rekor server
Rekor string
// Rekor storage mode to operate in. One of [online, offline] (default: online)
// online - Commit SHAs are stored in Rekor, requiring online verification for all commit objects.
// offline - Hashed commit content is stored in Rekor, with Rekor attributes
// necessary for offline verification being stored in the commit itself.
// Note: online verification will be deprecated in favor of offline in the future.
RekorMode string

// OIDC client ID for application
ClientID string
Expand Down Expand Up @@ -75,6 +81,8 @@ func Get() (*Config, error) {
Rekor: "https://rekor.sigstore.dev",
ClientID: "sigstore",
Issuer: "https://oauth2.sigstore.dev/auth",
// TODO: default to offline
RekorMode: "online",
}

// Get values from config file.
Expand Down Expand Up @@ -102,6 +110,7 @@ func Get() (*Config, error) {
}

out.LogPath = envOrValue("GITSIGN_LOG", out.LogPath)
out.RekorMode = envOrValue("GITSIGN_REKOR_MODE", out.RekorMode)

return out, nil
}
Expand Down Expand Up @@ -153,6 +162,8 @@ func applyGitOptions(out *Config, cfg map[string]string) {
out.FulcioRoot = v
case strings.EqualFold(k, "gitsign.rekor"):
out.Rekor = v
case strings.EqualFold(k, "gitsign.rekorMode"):
out.RekorMode = v
case strings.EqualFold(k, "gitsign.clientID"):
out.ClientID = v
case strings.EqualFold(k, "gitsign.redirectURL"):
Expand Down
1 change: 1 addition & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func TestGet(t *testing.T) {
Issuer: "tacocat",
RedirectURL: "example.com",
ConnectorID: "bar",
RekorMode: "online",
}

execFn = func() (io.Reader, error) {
Expand Down
5 changes: 5 additions & 0 deletions internal/fork/ietf-cms/signed_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@ func (sd *SignedData) IsDetached() bool {
func (sd *SignedData) ToDER() ([]byte, error) {
return sd.psd.ContentInfoDER()
}

// Raw returns the underlying CMS SignedData struct.
func (sd *SignedData) Raw() *protocol.SignedData {
return sd.psd
}
11 changes: 9 additions & 2 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@ import (
)

func Sign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data []byte, opts signature.SignOptions) ([]byte, *x509.Certificate, *models.LogEntryAnon, error) {
sig, cert, err := signature.Sign(ident, data, opts)
sig, cert, tlog, err := signature.Sign(ctx, ident, data, opts)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to sign message: %w", err)
}

// We're using offline verification style signing - nothing more to do.
if tlog != nil {
return sig, cert, tlog, 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 All @@ -53,7 +60,7 @@ func Sign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data
if err != nil {
return nil, nil, nil, fmt.Errorf("error signing commit hash: %w", err)
}
tlog, err := rekor.Write(ctx, commit, commitSig, cert)
tlog, err = rekor.Write(ctx, commit, commitSig, cert)
if err != nil {
return nil, nil, nil, fmt.Errorf("error uploading tlog (commit): %w", err)
}
Expand Down
Loading

0 comments on commit c309c66

Please sign in to comment.