diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index 2910a63e..a14e59a0 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -84,6 +84,28 @@ jobs:
echo "========== git verify-commit =========="
git verify-commit HEAD
+ echo "========== gitsign verify =========="
+ gitsign verify \
+ --certificate-github-workflow-repository=${{ github.repository }} \
+ --certificate-github-workflow-sha=${{ github.sha }} \
+ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
+ --certificate-identity="https://github.com/${{ github.workflow_ref }}"
+
+ # 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
+ echo "========== git verify-commit =========="
+ git verify-commit HEAD
+
echo "========== gitsign verify =========="
gitsign verify \
--certificate-github-workflow-repository=${{ github.repository }} \
diff --git a/README.md b/README.md
index 66a5d409..ad03ff0c 100644
--- a/README.md
+++ b/README.md
@@ -77,19 +77,20 @@ The following config options are supported:
### Environment Variables
-| Environment Variable | Sigstore
Prefix | Default | Description |
-| ---------------------------- | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| GITSIGN_CREDENTIAL_CACHE | ❌ | | Optional path to [gitsign-credential-cache](cmd/gitsign-credential-cache/README.md) socket. |
-| GITSIGN_CONNECTOR_ID | ✅ | | Optional Connector ID to auto-select to pre-select auth flow to use. For the public sigstore instance, valid values are:
- `https://github.com/login/oauth`
- `https://accounts.google.com`
- `https://login.microsoftonline.com` |
-| GITSIGN_FULCIO_URL | ✅ | https://fulcio.sigstore.dev | Address of Fulcio server |
-| GITSIGN_LOG | ❌ | | Path to log status output. Helpful for debugging when no TTY is available in the environment. |
-| GITSIGN_OIDC_CLIENT_ID | ✅ | sigstore | OIDC client ID for application |
-| GITSIGN_OIDC_ISSUER | ✅ | https://oauth2.sigstore.dev/auth | OIDC provider to be used to issue ID token |
-| GITSIGN_OIDC_REDIRECT_URL | ✅ | | OIDC Redirect URL |
-| GITSIGN_REKOR_URL | ✅ | https://rekor.sigstore.dev | Address of Rekor server |
-| GITSIGN_TIMESTAMP_SERVER_URL | ✅ | | Address of timestamping authority. If set, a trusted timestamp will be included in the signature. |
-| GITSIGN_TIMESTAMP_CERT_CHAIN | ✅ | | Path to PEM encoded certificate chain for RFC3161 Timestamp Authority verification. |
-| GITSIGN_FULCIO_ROOT | ✅ | | Path to PEM encoded certificate for Fulcio CA (additional alias: SIGSTORE_ROOT_FILE) |
+| Environment Variable | Sigstore
Prefix | Default | Description |
+| ---------------------------- | ------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| GITSIGN_CREDENTIAL_CACHE | | | Optional path to [gitsign-credential-cache](cmd/gitsign-credential-cache/README.md) socket. |
+| GITSIGN_CONNECTOR_ID | ✅ | | Optional Connector ID to auto-select to pre-select auth flow to use. For the public sigstore instance, valid values are:
- `https://github.com/login/oauth`
- `https://accounts.google.com`
- `https://login.microsoftonline.com` |
+| GITSIGN_FULCIO_URL | ✅ | https://fulcio.sigstore.dev | Address of Fulcio server |
+| GITSIGN_LOG | ❌ | | Path to log status output. Helpful for debugging when no TTY is available in the environment. |
+| GITSIGN_OIDC_CLIENT_ID | ✅ | sigstore | OIDC client ID for application |
+| GITSIGN_OIDC_ISSUER | ✅ | https://oauth2.sigstore.dev/auth | OIDC provider to be used to issue ID token |
+| GITSIGN_OIDC_REDIRECT_URL | ✅ | | OIDC Redirect URL |
+| GITSIGN_REKOR_URL | ✅ | https://rekor.sigstore.dev | Address of Rekor server |
+| GITSIGN_TIMESTAMP_SERVER_URL | ✅ | | Address of timestamping authority. If set, a trusted timestamp will be included in the signature. |
+| GITSIGN_TIMESTAMP_CERT_CHAIN | ✅ | | Path to PEM encoded certificate chain for RFC3161 Timestamp Authority verification. |
+| GITSIGN_FULCIO_ROOT | ✅ | | Path to PEM encoded certificate for Fulcio CA (additional alias: SIGSTORE_ROOT_FILE) |
+| GITSIGN_REKOR_MODE | ❌ | online | 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. |
For environment variables that support `Sigstore Prefix`, the values may be
provided with either a `GITSIGN_` or `SIGSTORE_` prefix - e.g.
@@ -279,13 +280,19 @@ Gitsign stores data in 2 places:
To be able to verify signatures for ephemeral certs past their `Not After`
time, Gitsign records commits and the code signing certificates to
- [Rekor](https://docs.sigstore.dev/rekor/overview/). This data is a
+ [Rekor](https://docs.sigstore.dev/rekor/overview/).
+
+ - If `rekorMode = online` (default)
+
+ This data is a
[HashedRekord](https://github.com/sigstore/rekor/blob/e375eb461cae524270889b57a249ff086bea6c05/types.md#hashed-rekord)
containing a SHA256 hash of the commit SHA, as well as the code signing
certificate. See
[Verifying the Transparency Log](#verifying-the-transparency-log) for more
details.
+ - If `rekorMode = offline`
+
By default, data is written to the
[public Rekor instance](https://docs.sigstore.dev/rekor/public-instance). In
particular, users and organizations may be sensitive to the data contained
@@ -324,25 +331,25 @@ PKCS7:
cert:
cert_info:
version: 2
- serialNumber: 4061203728062639434060493046878247211328523247
+ serialNumber: 0x2ECFB7E0D25F9A741FC3B19B56C4B74D25864788
signature:
algorithm: ecdsa-with-SHA384 (1.2.840.10045.4.3.3)
parameter:
- issuer: O=sigstore.dev, CN=sigstore
+ issuer: O=sigstore.dev, CN=sigstore-intermediate
validity:
- notBefore: May 2 20:51:47 2022 GMT
- notAfter: May 2 21:01:46 2022 GMT
+ notBefore: Jan 13 21:00:13 2023 GMT
+ notAfter: Jan 13 21:10:13 2023 GMT
subject:
key:
algor:
algorithm: id-ecPublicKey (1.2.840.10045.2.1)
parameter: OBJECT:prime256v1 (1.2.840.10045.3.1.7)
public_key: (0 unused bits)
- 0000 - 04 ec 60 4b 67 aa 28 d9-34 f3 83 9c 17 a5 ..`Kg.(.4.....
- 000e - c8 a5 87 5e de db c2 65-c8 8b e6 dc c4 6f ...^...e.....o
- 001c - 9c 87 60 05 42 18 f7 b7-0d 8f 06 f1 62 ce ..`.B.......b.
- 002a - 9a 59 9d 71 12 55 1b c3-d4 c7 90 a5 f6 0a .Y.q.U........
- 0038 - b4 1a b3 0e a7 3d 4e 12-38 .....=N.8
+ 0000 - 04 0d 3e f5 05 98 53 d2-68 21 9d e7 88 07 ..>...S.h!....
+ 000e - 0a d9 bc 8e 9f e3 00 e0-5d 28 b2 41 24 a7 ........](.A$.
+ 001c - a5 93 28 cc 45 d9 1e ee-a3 1c 8d 42 64 ab ..(.E......Bd.
+ 002a - 14 e6 ec 41 29 77 3a 0e-95 94 33 f7 40 62 ...A)w:...3.@b
+ 0038 - cd 25 fd 17 35 be 4d d4-f9 .%..5.M..
issuerUID:
subjectUID:
extensions:
@@ -356,23 +363,17 @@ PKCS7:
value:
0000 - 30 0a 06 08 2b 06 01 05-05 07 03 03 0...+.......
- object: X509v3 Basic Constraints (2.5.29.19)
- critical: TRUE
- value:
- 0000 - 30 0
- 0002 -
-
object: X509v3 Subject Key Identifier (2.5.29.14)
critical: BOOL ABSENT
value:
- 0000 - 04 14 a0 b1 ea 03 c5 08-4a 70 93 21 da ........Jp.!.
- 000d - a3 a0 0b 4c 55 49 d3 06-3d ...LUI..=
+ 0000 - 04 14 46 eb 25 b9 3b 3d-87 71 6a eb ba ..F.%.;=.qj..
+ 000d - e4 a4 4b b0 f1 17 4b 46-58 ..K...KFX
object: X509v3 Authority Key Identifier (2.5.29.35)
critical: BOOL ABSENT
value:
- 0000 - 30 16 80 14 58 c0 1e 5f-91 45 a5 66 a9 0...X.._.E.f.
- 000d - 7a cc 90 a1 93 22 d0 2a-c5 c5 fa z....".*...
+ 0000 - 30 16 80 14 df d3 e9 cf-56 24 11 96 f9 0.......V$...
+ 000d - a8 d8 e9 28 55 a2 c6 2e-18 64 3f ...(U....d?
object: X509v3 Subject Alternative Name (2.5.29.17)
critical: TRUE
@@ -383,27 +384,41 @@ PKCS7:
object: undefined (1.3.6.1.4.1.57264.1.1)
critical: BOOL ABSENT
value:
- 0000 - 68 74 74 70 73 3a 2f 2f-67 69 74 68 75 https://githu
- 000d - 62 2e 63 6f 6d 2f 6c 6f-67 69 6e 2f 6f b.com/login/o
- 001a - 61 75 74 68 auth
+ 0000 - 68 74 74 70 73 3a 2f 2f-61 63 63 6f 75 https://accou
+ 000d - 6e 74 73 2e 67 6f 6f 67-6c 65 2e 63 6f nts.google.co
+ 001a - 6d m
+
+ object: undefined (1.3.6.1.4.1.11129.2.4.2)
+ critical: BOOL ABSENT
+ value:
+ 0000 - 04 7b 00 79 00 77 00 dd-3d 30 6a c6 c7 .{.y.w..=0j..
+ 000d - 11 32 63 19 1e 1c 99 67-37 02 a2 4a 5e .2c....g7..J^
+ 001a - b8 de 3c ad ff 87 8a 72-80 2f 29 ee 8e ..<....r./)..
+ 0027 - 00 00 01 85 ac ee dc fa-00 00 04 03 00 .............
+ 0034 - 48 30 46 02 21 00 a1 e2-05 30 53 6f fb H0F.!....0So.
+ 0041 - 05 28 b6 bb 41 77 a9 7c-21 f4 a9 49 8b .(..Aw.|!..I.
+ 004e - f8 a6 1f 35 85 a7 40 b3-07 5c cb 04 02 ...5..@..\...
+ 005b - 21 00 f4 39 7b 17 5a 59-fa 10 1c f8 bf !..9{.ZY.....
+ 0068 - 46 cd bc de cc e8 39 7a-03 d4 1c 78 e5 F.....9z...x.
+ 0075 - b1 e7 7a ba 66 79 f2 c8- ..z.fy..
sig_alg:
algorithm: ecdsa-with-SHA384 (1.2.840.10045.4.3.3)
parameter:
signature: (0 unused bits)
- 0000 - 30 65 02 31 00 af be f5-bf e7 05 f5 15 e2 07 0e.1...........
- 000f - 85 48 00 ce 81 1e 3e ba-7b 21 d3 e4 a4 8a e6 .H....>.{!.....
- 001e - e5 38 9f 01 8a 16 6c 4c-d3 94 af 77 f0 7d 6e .8....lL...w.}n
- 002d - b1 9c f9 29 f9 2c b5 13-02 30 74 eb a5 5a 8a ...).,...0t..Z.
- 003c - 77 a0 95 7f 42 8e df 6a-ed 26 96 cc b4 30 29 w...B..j.&...0)
- 004b - b7 94 18 32 c6 8d a5 a4-06 88 e2 01 51 c4 61 ...2........Q.a
- 005a - 36 1a 1a 55 ec df a4 0a-84 b5 03 6e 12 6..U.......n.
+ 0000 - 30 65 02 30 5b 7c d7 ea-7c 5f 68 76 0b da 50 0e.0[|..|_hv..P
+ 000f - 14 cc bf 4c 65 07 70 68-52 33 9a 85 57 ce f5 ...Le.phR3..W..
+ 001e - ff 18 5b 8b 08 76 2a dd-7d 1a 19 7f b6 90 be ..[..v*.}......
+ 002d - ad 24 96 9a 2a 0a d6 02-31 00 ac 15 2b 1d 00 .$..*...1...+..
+ 003c - 6e 26 95 66 c9 6d cd 7e-e0 cd 12 0e 60 8b f9 n&.f.m.~....`..
+ 004b - 38 a9 0a dc 01 28 9a 39-e3 cd c9 eb a5 0c 08 8....(.9.......
+ 005a - 71 47 39 c8 dc 9d db c3-cf 8e f5 cd e9 qG9..........
crl:
signer_info:
version: 1
issuer_and_serial:
- issuer: O=sigstore.dev, CN=sigstore
- serial: 4061203728062639434060493046878247211328523247
+ issuer: O=sigstore.dev, CN=sigstore-intermediate
+ serial: 0x2ECFB7E0D25F9A741FC3B19B56C4B74D25864788
digest_alg:
algorithm: sha256 (2.16.840.1.101.3.4.2.1)
parameter:
@@ -414,190 +429,138 @@ PKCS7:
object: signingTime (1.2.840.113549.1.9.5)
value.set:
- UTCTIME:May 2 20:51:49 2022 GMT
+ UTCTIME:Jan 13 21:00:13 2023 GMT
object: messageDigest (1.2.840.113549.1.9.4)
value.set:
OCTET STRING:
- 0000 - 66 4e 98 f6 29 46 31 f6-ca 8f 21 44 06 fN..)F1...!D.
- 000d - 34 07 2a 8a b2 dd 64 29-4a e9 74 71 d0 4.*...d)J.tq.
- 001a - a1 84 ec d5 03 3f .....?
+ 0000 - 21 e9 ce 7a 69 ff 22 57-43 a2 fc c9 12 !..zi."WC....
+ 000d - 8a 67 c6 45 e7 31 88 4c-08 3f 26 9a 13 .g.E.1.L.?&..
+ 001a - ac 85 d6 6d f5 8e ...m..
digest_enc_alg:
algorithm: ecdsa-with-SHA256 (1.2.840.10045.4.3.2)
parameter:
enc_digest:
- 0000 - 30 45 02 20 58 02 c6 8c-30 51 df 4b 14 5e ff 0E. X...0Q.K.^.
- 000f - 54 a8 b3 44 0e 32 25 3a-2d 5b cf d9 e4 4e 4c T..D.2%:-[...NL
- 001e - 37 47 af 6e d4 17 02 21-00 81 d9 4c fc b7 e3 7G.n...!...L...
- 002d - 92 7e cd a7 c8 84 d6 ae-47 93 88 bd 17 c2 92 .~......G......
- 003c - a3 d4 a3 00 ec f6 c9 5b-8b 81 9a .......[...
+ 0000 - 30 46 02 21 00 cc 5a 1e-9a 27 70 ba 1f 70 7d 0F.!..Z..'p..p}
+ 000f - d6 f0 1c 56 f2 32 b3 d2-8f c4 63 dd 9c 82 cc ...V.2....c....
+ 001e - 69 30 2c cd 9e 90 f9 02-21 00 82 43 0a f7 79 i0,.....!..C..y
+ 002d - 64 41 14 6b 28 03 ac 38-2b a3 82 bd a8 a1 ea dA.k(..8+......
+ 003c - 52 db cf f2 5f d4 84 4f-85 b4 53 53 R..._..O..SS
unauth_attr:
-
-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-----
-```
+ 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
-### Verifying the Transparency Log
+ object: undefined (1.3.6.1.4.1.57264.3.1)
+ value.set:
+ INTEGER:1673643613
-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.
+ object: undefined (1.3.6.1.4.1.57264.3.3)
+ value.set:
+ INTEGER:11117788
-We can manually validate that the commit exists in the transparency log by
-running:
+ object: undefined (1.3.6.1.4.1.57264.3.2)
+ value.set:
+ PRINTABLESTRING:c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d
-```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="
- }
- }
- }
-}
+ object: undefined (1.3.6.1.4.1.57264.3.7)
+ value.set:
+ PRINTABLESTRING:373443ac6ee5e01d4bfa00666f79d5c7cee0380684ebe571fc98bdffea82f972
-$ 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-----
-```
+ 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
+6954358
+NzRDrG7l4B1L+gBmb3nVx87gOAaE6+Vx/Ji9/+qC+XI=
+Timestamp: 1673643613823629328
-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!
+\U2014 rekor.sigstore.dev wNI9ajBFAiB1IrUY3QV0nXQF0NFuo+1WtTRRYIKhaBI4rUj0Ry3WkwIhAI6D+kvZh+NhJ7Xi4HT0kPVB0nxGjR+cOHFOU1HJbUKF
-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.
+
+ object: undefined (1.3.6.1.4.1.57264.3.6)
+ value.set:
+ SEQUENCE:
+ 0:d=0 hl=4 l= 858 cons: SEQUENCE
+ 4:d=1 hl=2 l= 64 prim: PRINTABLESTRING :be961775858a32f96c8d12fb8db3c3101bb4d8296f37f53f74dc2cb51c22a9ad
+ 70:d=1 hl=2 l= 64 prim: PRINTABLESTRING :92bd4aedddebab9be5678442a28bcfbada3300e04c0726368796a6d8b32fd909
+ 136:d=1 hl=2 l= 64 prim: PRINTABLESTRING :6e5a335c4b2f89e25d5be75ed0a724b154e0f53367bd4888c625d96f4a1e6b79
+ 202:d=1 hl=2 l= 64 prim: PRINTABLESTRING :67bce8699de01f6fc9ac8865ee5b08ee3a6617b57328b59cc342c55a4067652b
+ 268:d=1 hl=2 l= 64 prim: PRINTABLESTRING :f06fad8a06e8b60133ec7847be1586d517728f2da95f6e81ec9d1e4b1bbfc9d1
+ 334:d=1 hl=2 l= 64 prim: PRINTABLESTRING :32f164dcc4d2ff3b095c4f2d2b4beb25223cffd028a53fae3cac98f70e4bbd83
+ 400:d=1 hl=2 l= 64 prim: PRINTABLESTRING :1c3b03f4eff02f6405ef856350ffd03650d5de5271a65f0cee51ffe4fc6a99af
+ 466:d=1 hl=2 l= 64 prim: PRINTABLESTRING :c73ab44c0792697f44a5e237a47fff42f9c4dbf869071ee08e95dec222917f09
+ 532:d=1 hl=2 l= 64 prim: PRINTABLESTRING :e1e7772b7c20874ea1b3bebb2fd4ec5b496bcf45c338495ddbe93ae1fbcabe2c
+ 598:d=1 hl=2 l= 64 prim: PRINTABLESTRING :5da6951fe16688f8a256fc9adf3ccda1806b811e2bc50caab99ee61ded6ef6a3
+ 664:d=1 hl=2 l= 64 prim: PRINTABLESTRING :e7d67f5102ddeda58eda651dcba76876d01955a4eca9fce4caaf9e0ba7521cdd
+ 730:d=1 hl=2 l= 64 prim: PRINTABLESTRING :616429db6c7d20c5b0eff1a6e512ea57a0734b94ae0bc7c914679463e01a7fba
+ 796:d=1 hl=2 l= 64 prim: PRINTABLESTRING :5a4ad1534b1e770f02bfde0de15008a6971cf1ffbfa963fc9c2a644973a8d2d1
+-----BEGIN PKCS7-----
+MIIJ3gYJKoZIhvcNAQcCoIIJzzCCCcsCAQExDTALBglghkgBZQMEAgEwCwYJKoZI
+hvcNAQcBoIICpTCCAqEwggInoAMCAQICFC7Pt+DSX5p0H8Oxm1bEt00lhkeIMAoG
+CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
+c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDExMzIxMDAxM1oXDTIzMDExMzIxMTAx
+M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABA0+9QWYU9JoIZ3niAcK2byO
+n+MA4F0oskEkp6WTKMxF2R7uoxyNQmSrFObsQSl3Og6VlDP3QGLNJf0XNb5N1Pmj
+ggFGMIIBQjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD
+VR0OBBYEFEbrJbk7PYdxauu65KRLsPEXS0ZYMB8GA1UdIwQYMBaAFN/T6c9WJBGW
++ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2
+MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiwYK
+KwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAv
+Ke6OAAABhazu3PoAAAQDAEgwRgIhAKHiBTBTb/sFKLa7QXepfCH0qUmL+KYfNYWn
+QLMHXMsEAiEA9Dl7F1pZ+hAc+L9GzbzezOg5egPUHHjlsed6umZ58sgwCgYIKoZI
+zj0EAwMDaAAwZQIwW3zX6nxfaHYL2lAUzL9MZQdwaFIzmoVXzvX/GFuLCHYq3X0a
+GX+2kL6tJJaaKgrWAjEArBUrHQBuJpVmyW3NfuDNEg5gi/k4qQrcASiaOePNyeul
+DAhxRznI3J3bw8+O9c3pMYIG/zCCBvsCAQEwTzA3MRUwEwYDVQQKEwxzaWdzdG9y
+ZS5kZXYxHjAcBgNVBAMTFXNpZ3N0b3JlLWludGVybWVkaWF0ZQIULs+34NJfmnQf
+w7GbVsS3TSWGR4gwCwYJYIZIAWUDBAIBoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3
+DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwMTEzMjEwMDEzWjAvBgkqhkiG9w0BCQQx
+IgQgIenOemn/IldDovzJEopnxkXnMYhMCD8mmhOshdZt9Y4wCgYIKoZIzj0EAwIE
+SDBGAiEAzFoemidwuh9wfdbwHFbyMrPSj8Rj3ZyCzGkwLM2ekPkCIQCCQwr3eWRB
+FGsoA6w4K6OCvaih6lLbz/Jf1IRPhbRTU6GCBdUwEwYKKwYBBAGDvzADCDEFAgNq
+HXYwEwYKKwYBBAGDvzADCTEFAgNqHXUwFAYKKwYBBAGDvzADATEGAgRjwcZdMBQG
+CisGAQQBg78wAwMxBgIEAKmk3DBQBgorBgEEAYO/MAMCMUITQGMwZDIzZDZhZDQw
+Njk3M2Y5NTU5ZjNiYTJkMWNhMDFmODQxNDdkOGZmYzViODQ0NWMyMjRmOThiOTU5
+MTgwMWQwUAYKKwYBBAGDvzADBzFCE0AzNzM0NDNhYzZlZTVlMDFkNGJmYTAwNjY2
+Zjc5ZDVjN2NlZTAzODA2ODRlYmU1NzFmYzk4YmRmZmVhODJmOTcyMFcGCisGAQQB
+g78wAwQxSQRHMEUCIADQiP+RGHUckEyq8zeURajKHqTeYBAKImkDyS3SDhqfAiEA
+r814hfJmXyLF06Jc/OLB/gzyJ6rw+v1zyl1YmJwA31wwggEMBgorBgEEAYO/MAMF
+MYH9DIH6cmVrb3Iuc2lnc3RvcmUuZGV2IC0gMjYwNTczNjY3MDk3Mjc5NDc0Ngo2
+OTU0MzU4Ck56UkRyRzdsNEIxTCtnQm1iM25WeDg3Z09BYUU2K1Z4L0ppOS8rcUMr
+WEk9ClRpbWVzdGFtcDogMTY3MzY0MzYxMzgyMzYyOTMyOAoK4oCUIHJla29yLnNp
+Z3N0b3JlLmRldiB3Tkk5YWpCRkFpQjFJclVZM1FWMG5YUUYwTkZ1bysxV3RUUlJZ
+SUtoYUJJNHJVajBSeTNXa3dJaEFJNkQra3ZaaCtOaEo3WGk0SFQwa1BWQjBueEdq
+UitjT0hGT1UxSEpiVUtGCjCCA24GCisGAQQBg78wAwYxggNeMIIDWhNAYmU5NjE3
+NzU4NThhMzJmOTZjOGQxMmZiOGRiM2MzMTAxYmI0ZDgyOTZmMzdmNTNmNzRkYzJj
+YjUxYzIyYTlhZBNAOTJiZDRhZWRkZGViYWI5YmU1Njc4NDQyYTI4YmNmYmFkYTMz
+MDBlMDRjMDcyNjM2ODc5NmE2ZDhiMzJmZDkwORNANmU1YTMzNWM0YjJmODllMjVk
+NWJlNzVlZDBhNzI0YjE1NGUwZjUzMzY3YmQ0ODg4YzYyNWQ5NmY0YTFlNmI3ORNA
+NjdiY2U4Njk5ZGUwMWY2ZmM5YWM4ODY1ZWU1YjA4ZWUzYTY2MTdiNTczMjhiNTlj
+YzM0MmM1NWE0MDY3NjUyYhNAZjA2ZmFkOGEwNmU4YjYwMTMzZWM3ODQ3YmUxNTg2
+ZDUxNzcyOGYyZGE5NWY2ZTgxZWM5ZDFlNGIxYmJmYzlkMRNAMzJmMTY0ZGNjNGQy
+ZmYzYjA5NWM0ZjJkMmI0YmViMjUyMjNjZmZkMDI4YTUzZmFlM2NhYzk4ZjcwZTRi
+YmQ4MxNAMWMzYjAzZjRlZmYwMmY2NDA1ZWY4NTYzNTBmZmQwMzY1MGQ1ZGU1Mjcx
+YTY1ZjBjZWU1MWZmZTRmYzZhOTlhZhNAYzczYWI0NGMwNzkyNjk3ZjQ0YTVlMjM3
+YTQ3ZmZmNDJmOWM0ZGJmODY5MDcxZWUwOGU5NWRlYzIyMjkxN2YwORNAZTFlNzc3
+MmI3YzIwODc0ZWExYjNiZWJiMmZkNGVjNWI0OTZiY2Y0NWMzMzg0OTVkZGJlOTNh
+ZTFmYmNhYmUyYxNANWRhNjk1MWZlMTY2ODhmOGEyNTZmYzlhZGYzY2NkYTE4MDZi
+ODExZTJiYzUwY2FhYjk5ZWU2MWRlZDZlZjZhMxNAZTdkNjdmNTEwMmRkZWRhNThl
+ZGE2NTFkY2JhNzY4NzZkMDE5NTVhNGVjYTlmY2U0Y2FhZjllMGJhNzUyMWNkZBNA
+NjE2NDI5ZGI2YzdkMjBjNWIwZWZmMWE2ZTUxMmVhNTdhMDczNGI5NGFlMGJjN2M5
+MTQ2Nzk0NjNlMDFhN2ZiYRNANWE0YWQxNTM0YjFlNzcwZjAyYmZkZTBkZTE1MDA4
+YTY5NzFjZjFmZmJmYTk2M2ZjOWMyYTY0NDk3M2E4ZDJkMQ==
+-----END PKCS7-----
+```
diff --git a/docs/verification.md b/docs/verification.md
new file mode 100644
index 00000000..99d70bf0
--- /dev/null
+++ b/docs/verification.md
@@ -0,0 +1,264 @@
+# Verification
+
+## Offline Verification
+
+### How we sign
+
+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.
+
+### 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)
+ value.set:
+ OCTET STRING:
+ 0000 - 08 af d5 d6 08 12 22 0a-20 c0 d2 3d 6a ......". ..=j
+ 000d - d4 06 97 3f 95 59 f3 ba-2d 1c a0 1f 84 ...?.Y..-....
+ 001a - 14 7d 8f fc 5b 84 45 c2-24 f9 8b 95 91 .}..[.E.$....
+ 0027 - 80 1d 1a 15 0a 0c 68 61-73 68 65 64 72 ......hashedr
+ 0034 - 65 6b 6f 72 64 12 05 30-2e 30 2e 31 20 ekord..0.0.1
+ 0041 - a1 fc f5 a1 06 2a 49 0a-47 30 45 02 21 .....*I.G0E.!
+ 004e - 00 fd ab 1a 0d 0b 39 fe-d5 0f f2 4d 87 ......9....M.
+ 005b - 40 06 bd 2d 84 e8 ca d8-a2 39 99 e5 d9 @..-.....9...
+ 0068 - 8a 3e b2 48 04 44 67 02-20 15 a5 02 7a .>.H.Dg. ...z
+ 0075 - 61 0b d1 58 46 81 b1 ff-53 e8 46 be b3 a..XF...S.F..
+ 0082 - 70 9b f1 55 07 0c e8 32-bb 61 4e aa ce p..U...2.aN..
+ 008f - 61 16 32 81 05 08 c8 c6-d8 06 12 20 3f a.2........ ?
+ 009c - 5f bc 03 da 94 4e 17 05-44 a8 c2 1b e9 _....N..D....
+ 00a9 - a7 6c 84 7d 39 66 4b 07-2f c2 7b 49 3d .l.}9fK./.{I=
+ 00b6 - 2b da 9a 84 30 18 c9 c6-d8 06 22 20 34 +...0....." 4
+ 00c3 - 8d 79 2a f5 5b 0d e8 8f-6e 6b 3f 39 8e .y*.[...nk?9.
+ 00d0 - 43 02 2a d3 b3 c3 6b d5-d1 c6 84 cd 7f C.*...k......
+ 00dd - 08 24 2f a6 6e 22 20 64-47 c9 39 2b 77 .$/.n" dG.9+w
+ 00ea - ba 3b b5 36 7f bd ea 8f-36 ef 32 33 14 .;.6....6.23.
+ 00f7 - 2a e2 ec 2d 57 51 a6 4b-8f 00 59 d2 5e *..-WQ.K..Y.^
+ 0104 - 22 20 c0 d8 57 e5 d0 82-b2 b8 cf 26 b0 " ..W......&.
+ 0111 - 58 e3 85 e5 71 ba 34 ab-5c 1b 49 5a 5e X...q.4.\.IZ^
+ 011e - c4 20 7b 7a 47 d6 02 0b-22 20 21 52 30 . {zG..." !R0
+ 012b - e1 48 37 62 5c 39 56 bc-78 a6 84 d5 c3 .H7b\9V.x....
+ 0138 - df 3d ea e4 75 80 07 a3-25 b9 c9 42 e6 .=..u...%..B.
+ 0145 - 34 8e 49 22 20 4a 88 54-e3 e8 ed dd f0 4.I" J.T.....
+ 0152 - 4b f4 e2 95 55 da a8 44-be 87 85 e6 d9 K...U..D.....
+ 015f - 57 52 8f 97 b3 3a d3 d7-96 32 f9 22 20 WR...:...2."
+ 016c - 35 b2 b6 5b 9f 02 a8 bc-7d d2 f8 64 30 5..[....}..d0
+ 0179 - d5 04 b1 c4 bb 2e 0c c8-bd 00 18 52 bb ...........R.
+ 0186 - 40 ad 84 6c 2d 68 22 20-4c 82 cf f1 63 @..l-h" L...c
+ 0193 - 90 df b5 b4 3a 8b 0f bf-04 43 3e 52 0e ....:....C>R.
+ 01a0 - ef f6 d0 0e d3 c0 01 31-b1 8f 1b 68 82 .......1...h.
+ 01ad - 74 22 20 ec 4c 65 15 56-3a 67 6a 41 1e t" .Le.V:gjA.
+ 01ba - 44 ad 06 b2 df 2d ff da-2c 03 77 87 ee D....-..,.w..
+ 01c7 - ba 00 c9 5b c3 b5 34 59-55 22 20 d6 30 ...[..4YU" .0
+ 01d4 - 92 c2 27 78 05 dc b4 cb-36 1b ea 6e 09 ..'x....6..n.
+ 01e1 - ac 7e d9 e9 e9 19 27 24-b8 f5 1e 57 e5 .~....'$...W.
+ 01ee - 4b df 35 31 22 20 9e 04-00 66 df e5 f0 K.51" ...f...
+ 01fb - 20 04 65 83 86 ac 66 cf-0b b6 ff e8 57 .e...f.....W
+ 0208 - ed 71 cb 33 7c 7f 55 45-ec f4 55 8b 2a .q.3|.UE..U.*
+ 0215 - fe 01 0a fb 01 72 65 6b-6f 72 2e 73 69 .....rekor.si
+ 0222 - 67 73 74 6f 72 65 2e 64-65 76 20 2d 20 gstore.dev -
+ 022f - 32 36 30 35 37 33 36 36-37 30 39 37 32 2605736670972
+ 023c - 37 39 34 37 34 36 0a 31-34 30 33 33 37 794746.140337
+ 0249 - 33 37 0a 50 31 2b 38 41-39 71 55 54 68 37.P1+8A9qUTh
+ 0256 - 63 46 52 4b 6a 43 47 2b-6d 6e 62 49 52 cFRKjCG+mnbIR
+ 0263 - 39 4f 57 5a 4c 42 79 2f-43 65 30 6b 39 9OWZLBy/Ce0k9
+ 0270 - 4b 39 71 61 68 44 41 3d-0a 54 69 6d 65 K9qahDA=.Time
+ 027d - 73 74 61 6d 70 3a 20 31-36 38 31 37 35 stamp: 168175
+ 028a - 31 35 38 35 32 37 34 35-37 36 37 30 31 1585274576701
+ 0297 - 0a 0a e2 80 94 20 72 65-6b 6f 72 2e 73 ..... rekor.s
+ 02a4 - 69 67 73 74 6f 72 65 2e-64 65 76 20 77 igstore.dev w
+ 02b1 - 4e 49 39 61 6a 42 45 41-69 42 31 56 4a NI9ajBEAiB1VJ
+ 02be - 48 46 6e 34 47 4e 63 32-65 38 65 42 78 HFn4GNc2e8eBx
+ 02cb - 48 6f 4b 41 6c 56 6f 77-44 77 4a 51 72 HoKAlVowDwJQr
+ 02d8 - 34 32 53 50 56 37 64 2f-6e 72 73 47 34 42SPV7d/nrsG4
+ 02e5 - 77 49 67 4c 49 73 36 77-2b 59 75 39 42 wIgLIs6w+Yu9B
+ 02f2 - 2f 35 2b 73 6b 6e 72 51-65 36 58 33 72 /5+sknrQe6X3r
+ 02ff - 68 6e 6b 41 65 6a 6d 76-55 6d 4d 5a 5a hnkAejmvUmMZZ
+ 030c - 69 51 75 4d 53 49 59 3d-0a iQuMSIY=.
+```
+
+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
+
+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:
+
+```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 includes 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 `), **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/go.mod b/go.mod
index d6b75d57..2ad18a17 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.6.0
github.com/sigstore/cosign/v2 v2.0.2
github.com/sigstore/fulcio v1.3.1
+ github.com/sigstore/protobuf-specs v0.1.0
github.com/sigstore/rekor v1.1.1
github.com/sigstore/sigstore v1.6.4
github.com/spf13/cobra v1.7.0
@@ -26,6 +27,7 @@ require (
golang.org/x/crypto v0.9.0
golang.org/x/oauth2 v0.8.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
+ google.golang.org/protobuf v1.30.0
)
require (
@@ -205,7 +207,6 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.54.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 54bcc11f..0c4a2196 100644
--- a/go.sum
+++ b/go.sum
@@ -676,6 +676,8 @@ github.com/sigstore/cosign/v2 v2.0.2 h1:Ttaj/OkJAy+ummhnHG2F+JSFeZQj8i0P6o8j2RY9
github.com/sigstore/cosign/v2 v2.0.2/go.mod h1:yJXtRmWrumyQA/XPjTTjOufnNckI87mmmVxv9rtEqgE=
github.com/sigstore/fulcio v1.3.1 h1:0ntW9VbQbt2JytoSs8BOGB84A65eeyvGSavWteYp29Y=
github.com/sigstore/fulcio v1.3.1/go.mod h1:/XfqazOec45ulJZpyL9sq+OsVQ8g2UOVoNVi7abFgqU=
+github.com/sigstore/protobuf-specs v0.1.0 h1:X0l/E2C2c79t/rI/lmSu8WAoKWsQtMqDzAMiDdEMGr8=
+github.com/sigstore/protobuf-specs v0.1.0/go.mod h1:5shUCxf82hGnjUEFVWiktcxwzdtn6EfeeJssxZ5Q5HE=
github.com/sigstore/rekor v1.1.1 h1:JCeSss+qUHnCATmwAZh4zT9k0Frdyq0BjmRwewSfEy4=
github.com/sigstore/rekor v1.1.1/go.mod h1:x/xK+HK08MiuJv+v4OxY/Oo3bhuz1DtJXNJrV7hrzvs=
github.com/sigstore/sigstore v1.6.4 h1:jH4AzR7qlEH/EWzm+opSpxCfuUcjHL+LJPuQE7h40WE=
diff --git a/internal/commands/root/sign.go b/internal/commands/root/sign.go
index 0eff4305..d1ff55a4 100644
--- a/internal/commands/root/sign.go
+++ b/internal/commands/root/sign.go
@@ -88,18 +88,23 @@ func commandSign(o *options, s *gsio.Streams, args ...string) error {
opts.UserName = o.Config.CommitterName
opts.UserEmail = o.Config.CommitterEmail
}
- sig, cert, tlog, err := git.Sign(ctx, rekor, userIdent, dataBuf.Bytes(), opts)
+
+ var fn git.SignFunc = git.LegacySHASign
+ if o.Config.RekorMode == "offline" {
+ fn = git.Sign
+ }
+ resp, err := fn(ctx, rekor, userIdent, dataBuf.Bytes(), opts)
if err != nil {
return fmt.Errorf("failed to sign message: %w", err)
}
- if tlog != nil && tlog.LogIndex != nil {
+ if tlog := resp.LogEntry; tlog != nil && tlog.LogIndex != nil {
fmt.Fprintf(s.TTYOut, "tlog entry created with index: %d\n", *tlog.LogIndex)
}
- gpgout.EmitSigCreated(cert, o.FlagDetachedSignature)
+ gpgout.EmitSigCreated(resp.Cert, o.FlagDetachedSignature)
- if _, err := s.Out.Write(sig); err != nil {
+ if _, err := s.Out.Write(resp.Signature); err != nil {
return errors.New("failed to write signature")
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 59d3c24c..ab11208b 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -24,6 +24,13 @@ import (
"strings"
)
+type RekorVerificationMode int
+
+const (
+ RekorVerificationOnline RekorVerificationMode = iota
+ RekorVerificationOffline
+)
+
var (
// execFn is a function to get the raw git config.
// Configurable to allow for overriding for testing.
@@ -39,6 +46,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
@@ -80,6 +93,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.
@@ -107,6 +122,7 @@ func Get() (*Config, error) {
}
out.LogPath = envOrValue("GITSIGN_LOG", out.LogPath)
+ out.RekorMode = envOrValue("GITSIGN_REKOR_MODE", out.RekorMode)
return out, nil
}
@@ -162,6 +178,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"):
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index b342d574..1598677d 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -85,6 +85,7 @@ func TestGet(t *testing.T) {
Issuer: "tacocat",
RedirectURL: "example.com",
ConnectorID: "bar",
+ RekorMode: "online",
}
execFn = func() (io.Reader, error) {
diff --git a/internal/fork/ietf-cms/signed_data.go b/internal/fork/ietf-cms/signed_data.go
index edfcf413..1d5af68b 100644
--- a/internal/fork/ietf-cms/signed_data.go
+++ b/internal/fork/ietf-cms/signed_data.go
@@ -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
+}
diff --git a/internal/git/git.go b/internal/git/git.go
index b9ad9bff..2aa9294d 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -18,20 +18,32 @@ package git
import (
"bytes"
"context"
- "crypto/x509"
"fmt"
"github.com/sigstore/gitsign/internal/fulcio"
"github.com/sigstore/gitsign/internal/signature"
"github.com/sigstore/gitsign/pkg/git"
"github.com/sigstore/gitsign/pkg/rekor"
- "github.com/sigstore/rekor/pkg/generated/models"
)
-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)
+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, nil, nil, fmt.Errorf("failed to sign message: %w", err)
+ return nil, fmt.Errorf("failed to sign message: %w", err)
}
// This uploads the commit SHA + sig(commit SHA) to the tlog using the same
@@ -40,23 +52,23 @@ func Sign(ctx context.Context, rekor rekor.Writer, ident *fulcio.Identity, data
// using the same key, this is probably okay? e.g. even if you could cause a SHA1 collision,
// you would still need the underlying commit to be valid and using the same key which seems hard.
- commit, err := git.ObjectHash(data, sig)
+ commit, err := git.ObjectHash(data, resp.Signature)
if err != nil {
- return nil, nil, nil, fmt.Errorf("error generating commit hash: %w", err)
+ return nil, fmt.Errorf("error generating commit hash: %w", err)
}
sv, err := ident.SignerVerifier()
if err != nil {
- return nil, nil, nil, fmt.Errorf("error getting signer: %w", err)
+ return nil, fmt.Errorf("error getting signer: %w", err)
}
commitSig, err := sv.SignMessage(bytes.NewBufferString(commit))
if err != nil {
- return nil, nil, nil, fmt.Errorf("error signing commit hash: %w", err)
+ return nil, fmt.Errorf("error signing commit hash: %w", err)
}
- tlog, err := rekor.Write(ctx, commit, commitSig, cert)
+ resp.LogEntry, err = rekor.Write(ctx, commit, commitSig, resp.Cert)
if err != nil {
- return nil, nil, nil, fmt.Errorf("error uploading tlog (commit): %w", err)
+ return nil, fmt.Errorf("error uploading tlog (commit): %w", err)
}
- return sig, cert, tlog, nil
+ return resp, nil
}
diff --git a/internal/gitsign/gitsign_test.go b/internal/gitsign/gitsign_test.go
index 6d1c02e2..91f9e66f 100644
--- a/internal/gitsign/gitsign_test.go
+++ b/internal/gitsign/gitsign_test.go
@@ -110,6 +110,7 @@ func generateCert(t *testing.T, tmpl *x509.Certificate) (*x509.Certificate, *ecd
func generateData(t *testing.T, cert *x509.Certificate, priv crypto.Signer) ([]byte, []byte) {
t.Helper()
+ ctx := context.Background()
// Generate commit data
commit := object.Commit{
@@ -132,7 +133,7 @@ func generateData(t *testing.T, cert *x509.Certificate, priv crypto.Signer) ([]b
cert: cert,
priv: priv,
}
- sig, _, err := signature.Sign(id, data, signature.SignOptions{
+ resp, err := signature.Sign(ctx, id, data, signature.SignOptions{
Detached: true,
Armor: true,
// Fake CA outputs self-signed certs, so we need to use -1 to make sure
@@ -144,7 +145,7 @@ func generateData(t *testing.T, cert *x509.Certificate, priv crypto.Signer) ([]b
t.Fatalf("Sign() = %v", err)
}
- return data, sig
+ return data, resp.Signature
}
type fakeRekor struct{}
@@ -153,6 +154,10 @@ func (fakeRekor) Verify(ctx context.Context, commitSHA string, cert *x509.Certif
return nil, nil
}
+func (fakeRekor) VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error) {
+ return nil, nil
+}
+
type identity struct {
signature.Identity
cert *x509.Certificate
diff --git a/internal/rekor/oid/oid.go b/internal/rekor/oid/oid.go
new file mode 100644
index 00000000..c7ef28db
--- /dev/null
+++ b/internal/rekor/oid/oid.go
@@ -0,0 +1,123 @@
+// Copyright 2023 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oid
+
+import (
+ "context"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/asn1"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+
+ "github.com/github/smimesign/ietf-cms/protocol"
+ "github.com/go-openapi/strfmt"
+ "github.com/go-openapi/swag"
+ rekorpb "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1"
+ "github.com/sigstore/rekor/pkg/generated/models"
+ "github.com/sigstore/rekor/pkg/types"
+ "github.com/sigstore/rekor/pkg/types/hashedrekord"
+ hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
+ "github.com/sigstore/sigstore/pkg/cryptoutils"
+ "google.golang.org/protobuf/proto"
+)
+
+var (
+ // OIDRekorTransparencyLogEntry is the OID for a serialized Rekor TransparencyLogEntry proto.
+ // See https://github.com/sigstore/rekor/pull/1390
+ OIDRekorTransparencyLogEntry = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 3, 1}
+)
+
+// ToLogEntry reconstructs a Rekor HashedRekord from Git commit signature PKCS7 components.
+func ToLogEntry(ctx context.Context, message []byte, sig []byte, cert *x509.Certificate, attrs protocol.Attributes) (*models.LogEntryAnon, error) {
+ var b []byte
+ if err := unmarshalAttribute(attrs, OIDRekorTransparencyLogEntry, &b); err != nil {
+ return nil, fmt.Errorf("error unmarshalling attribute: %w", err)
+ }
+ pb := new(rekorpb.TransparencyLogEntry)
+ if err := proto.Unmarshal(b, pb); err != nil {
+ return nil, fmt.Errorf("error unmarshalling TransparencyLogEntry attribute: %w", err)
+ }
+ out := logEntryAnonFromProto(pb)
+
+ // Recompute HashedRekord body.
+ hash := sha256.Sum256(message)
+ certPEM, err := cryptoutils.MarshalCertificateToPEM(cert)
+ if err != nil {
+ return nil, fmt.Errorf("error marshalling cert: %w", err)
+ }
+ re := &hashedrekord_v001.V001Entry{
+ HashedRekordObj: models.HashedrekordV001Schema{
+ Data: &models.HashedrekordV001SchemaData{
+ Hash: &models.HashedrekordV001SchemaDataHash{
+ Algorithm: swag.String("sha256"),
+ Value: swag.String(hex.EncodeToString(hash[:])),
+ },
+ },
+ Signature: &models.HashedrekordV001SchemaSignature{
+ Content: strfmt.Base64(sig),
+ PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
+ Content: strfmt.Base64(certPEM),
+ },
+ },
+ },
+ }
+ body, err := types.CanonicalizeEntry(ctx, re)
+ if err != nil {
+ return nil, fmt.Errorf("error canonicalizing entry: %w", err)
+ }
+ out.Body = base64.StdEncoding.EncodeToString(body)
+
+ return out, nil
+}
+
+func unmarshalAttribute(attrs protocol.Attributes, oid asn1.ObjectIdentifier, target any) error {
+ rv, err := attrs.GetOnlyAttributeValueBytes(oid)
+ if err != nil {
+ return fmt.Errorf("get oid: %w", err)
+ }
+
+ if _, err := asn1.Unmarshal(rv.FullBytes, target); err != nil {
+ return fmt.Errorf("asn1.unmarshal(%v): %w", oid, err)
+ }
+ return nil
+}
+
+// ToAttributes takes a Rekor log entry and extracts fields into Attributes suitable to be included in the signature's
+// unauthenticated attributes.
+func ToAttributes(tlog *models.LogEntryAnon) (protocol.Attributes, error) {
+ pb, err := logEntryAnonToProto(tlog, &rekorpb.KindVersion{
+ Kind: hashedrekord.KIND,
+ Version: hashedrekord_v001.APIVERSION,
+ })
+ if err != nil {
+ return nil, err
+ }
+ // Clear out body - we store this data elsewhere so including is in the serialized log entry is redundant.
+ pb.CanonicalizedBody = nil
+ out, err := proto.Marshal(pb)
+ if err != nil {
+ return nil, err
+ }
+
+ attrs := protocol.Attributes{}
+ attr, err := protocol.NewAttribute(OIDRekorTransparencyLogEntry, out)
+ if err != nil {
+ return nil, err
+ }
+ attrs = append(attrs, attr)
+ return attrs, nil
+}
diff --git a/internal/rekor/oid/oid_test.go b/internal/rekor/oid/oid_test.go
new file mode 100644
index 00000000..5ee76883
--- /dev/null
+++ b/internal/rekor/oid/oid_test.go
@@ -0,0 +1,113 @@
+// Copyright 2023 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oid
+
+import (
+ "context"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "os"
+ "testing"
+
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/storage/memory"
+ "github.com/google/go-cmp/cmp"
+ cms "github.com/sigstore/gitsign/internal/fork/ietf-cms"
+ "github.com/sigstore/rekor/pkg/generated/models"
+)
+
+func TestOID(t *testing.T) {
+ tlog := new(models.LogEntryAnon)
+ if err := json.Unmarshal(readfile(t, "testdata/tlog.json"), tlog); err != nil {
+ t.Fatal(err)
+ }
+
+ attr, err := ToAttributes(tlog)
+ if err != nil {
+ t.Fatalf("ToAttributes: %v", err)
+ }
+
+ commit := parseCommit(t, "testdata/commit.txt")
+ message, sig, cert := parseSignature(t, commit)
+
+ ctx := context.Background()
+ got, err := ToLogEntry(ctx, message, sig, cert, attr)
+ if err != nil {
+ t.Fatalf("ToLogEntry: %v", err)
+ }
+
+ if diff := cmp.Diff(tlog, got); diff != "" {
+ t.Errorf(diff)
+ }
+}
+
+func readfile(t *testing.T, path string) []byte {
+ t.Helper()
+ b, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return b
+}
+
+func parseCommit(t *testing.T, path string) *object.Commit {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("error reading input: %v", err)
+ }
+
+ storage := memory.NewStorage()
+ obj := storage.NewEncodedObject()
+ obj.SetType(plumbing.CommitObject)
+ w, err := obj.Writer()
+ if err != nil {
+ t.Fatalf("error getting git object writer: %v", err)
+ }
+ if _, err := w.Write(raw); err != nil {
+ t.Fatalf("error writing git commit: %v", err)
+ }
+
+ c, err := object.DecodeCommit(storage, obj)
+ if err != nil {
+ t.Fatalf("error decoding commit: %v", err)
+ }
+ return c
+}
+
+// Returns: body, sig, cert
+func parseSignature(t *testing.T, c *object.Commit) ([]byte, []byte, *x509.Certificate) {
+ // Parse signature
+ blk, _ := pem.Decode([]byte(c.PGPSignature))
+ sd, err := cms.ParseSignedData(blk.Bytes)
+ if err != nil {
+ t.Fatalf("failed to parse signature: %v", err)
+ }
+ si := sd.Raw().SignerInfos[0]
+
+ body, err := si.SignedAttrs.MarshaledForVerification()
+ if err != nil {
+ t.Fatalf("error marshalling commit body for verification: %v", err)
+ }
+
+ certs, err := sd.GetCertificates()
+ if err != nil {
+ t.Fatalf("error getting signature certs: %v", err)
+ }
+ cert := certs[0]
+
+ return body, si.Signature, cert
+}
diff --git a/internal/rekor/oid/pbcompat.go b/internal/rekor/oid/pbcompat.go
new file mode 100644
index 00000000..b95c0e6b
--- /dev/null
+++ b/internal/rekor/oid/pbcompat.go
@@ -0,0 +1,105 @@
+// Copyright 2023 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oid
+
+import (
+ "encoding/hex"
+ "fmt"
+
+ "github.com/go-openapi/swag"
+ v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
+ rekorpb "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1"
+ "github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// This file contains helper functions from going to/from Rekor types <-> protobuf-specs.
+// This may be pulled out into a more general library in the future.
+
+func logEntryAnonToProto(le *models.LogEntryAnon, kind *rekorpb.KindVersion) (*rekorpb.TransparencyLogEntry, error) {
+ if le == nil {
+ return nil, nil
+ }
+
+ logID, err := hex.DecodeString(*le.LogID)
+ if err != nil {
+ return nil, fmt.Errorf("error decoding LogID: %w", err)
+ }
+
+ hashes := make([][]byte, 0, len(le.Verification.InclusionProof.Hashes))
+ for i, h := range le.Verification.InclusionProof.Hashes {
+ b, err := hex.DecodeString(h)
+ if err != nil {
+ return nil, fmt.Errorf("error decoding Verification.InclusionProof.Hashes[%d]: %w", i, err)
+ }
+ hashes = append(hashes, b)
+ }
+
+ rootHash, err := hex.DecodeString(*le.Verification.InclusionProof.RootHash)
+ if err != nil {
+ return nil, fmt.Errorf("error decoding Verification.InclusionProof.RootHash: %w", err)
+ }
+
+ out := &rekorpb.TransparencyLogEntry{
+ LogIndex: *le.LogIndex,
+ LogId: &v1.LogId{
+ KeyId: logID,
+ },
+ IntegratedTime: *le.IntegratedTime,
+ InclusionPromise: &rekorpb.InclusionPromise{
+ SignedEntryTimestamp: le.Verification.SignedEntryTimestamp,
+ },
+ InclusionProof: &rekorpb.InclusionProof{
+ LogIndex: *le.Verification.InclusionProof.LogIndex,
+ RootHash: rootHash,
+ TreeSize: *le.Verification.InclusionProof.TreeSize,
+ Hashes: hashes,
+ Checkpoint: &rekorpb.Checkpoint{
+ Envelope: *le.Verification.InclusionProof.Checkpoint,
+ },
+ },
+ KindVersion: kind,
+ }
+
+ switch b := le.Body.(type) {
+ case string:
+ out.CanonicalizedBody = []byte(b)
+ default:
+ return nil, fmt.Errorf("unknown body type %T", le.Body)
+ }
+ return out, nil
+}
+
+func logEntryAnonFromProto(in *rekorpb.TransparencyLogEntry) *models.LogEntryAnon {
+ out := &models.LogEntryAnon{
+ LogID: swag.String(hex.EncodeToString(in.GetLogId().GetKeyId())),
+ LogIndex: swag.Int64(in.GetLogIndex()),
+ IntegratedTime: swag.Int64(in.GetIntegratedTime()),
+ Verification: &models.LogEntryAnonVerification{
+ SignedEntryTimestamp: in.GetInclusionPromise().GetSignedEntryTimestamp(),
+ InclusionProof: &models.InclusionProof{
+ LogIndex: swag.Int64(in.GetInclusionProof().GetLogIndex()),
+ Checkpoint: swag.String(in.GetInclusionProof().GetCheckpoint().GetEnvelope()),
+ TreeSize: swag.Int64(in.GetInclusionProof().GetTreeSize()),
+ RootHash: swag.String(hex.EncodeToString(in.GetInclusionProof().GetRootHash())),
+ Hashes: make([]string, 0, len(in.GetInclusionProof().GetHashes())),
+ },
+ },
+ Body: string(in.GetCanonicalizedBody()),
+ }
+ for _, h := range in.GetInclusionProof().GetHashes() {
+ out.Verification.InclusionProof.Hashes = append(out.Verification.InclusionProof.Hashes, hex.EncodeToString(h))
+ }
+ return out
+}
diff --git a/internal/rekor/oid/pbcompat_test.go b/internal/rekor/oid/pbcompat_test.go
new file mode 100644
index 00000000..0a46bc29
--- /dev/null
+++ b/internal/rekor/oid/pbcompat_test.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Sigstore Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package oid
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// Simple test to make sure we can go to/from rekor types to proto types.
+func TestConvert(t *testing.T) {
+ in := new(models.LogEntryAnon)
+ json.Unmarshal(readfile(t, "testdata/tlog.json"), in)
+
+ // Kind is useful debug information, but isn't really used by us since we assume input/output types.
+ pb, err := logEntryAnonToProto(in, nil)
+ if err != nil {
+ t.Fatalf("logEntryAnonToProto(): %v", err)
+ }
+
+ out := logEntryAnonFromProto(pb)
+
+ if diff := cmp.Diff(in, out); diff != "" {
+ t.Error(diff)
+ }
+}
diff --git a/internal/rekor/oid/testdata/commit.txt b/internal/rekor/oid/testdata/commit.txt
new file mode 100644
index 00000000..9b29d99e
--- /dev/null
+++ b/internal/rekor/oid/testdata/commit.txt
@@ -0,0 +1,51 @@
+tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+parent 8f71f8da0720689322457d69be6cbcf6701ce978
+author Billy Lynch 1681496842 -0400
+committer Billy Lynch 1681496842 -0400
+gpgsig -----BEGIN SIGNED MESSAGE-----
+ MIIH5wYJKoZIhvcNAQcCoIIH2DCCB9QCAQExDTALBglghkgBZQMEAgEwCwYJKoZI
+ hvcNAQcBoIIC0DCCAswwggJToAMCAQICFD/6FqofcAbhhJj/WM1S79/2A7tWMAoG
+ CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
+ c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDE4MjcyM1oXDTIzMDQxNDE4Mzcy
+ M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFZiKeh8JJtHGmy2ibIAPeKK
+ zHqz0qYC88YJl0+h33H8qPCJ1Fn8VMRahJPigTirbWVMucGIUTXcfumQIhlqnHej
+ ggFyMIIBbjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD
+ VR0OBBYEFC3DWs+Yowo88LrHSaIGZ9oLnPFDMB8GA1UdIwQYMBaAFN/T6c9WJBGW
+ +ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2
+ MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgor
+ BgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYKKwYB
+ BAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6O
+ AAABh4EFpsEAAAQDAEcwRQIgeLZJCeysHbCMGk9NH0uG3hj1YrmWx9cPFVZwjF1/
+ 7EQCIQC6RvPxocQJFlImT4iP0GJaeFVV1L75Y/QJPsHFYULiXjAKBggqhkjOPQQD
+ AwNnADBkAjB4ERsW18xtsT0cu7A3N4atNo4Ol5301gZYcfRmxXXEYU5INrhBGtBw
+ bhizmji7rtACMDoq1brbKYBvaVZp1U5X2o0wsYPXYe9PwvkYQwat2c+j43wWn4ye
+ V4Ot/OUigERBBDGCBN0wggTZAgEBME8wNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2
+ MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFD/6FqofcAbhhJj/WM1S
+ 79/2A7tWMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw
+ HAYJKoZIhvcNAQkFMQ8XDTIzMDQxNDE4MjcyNFowLwYJKoZIhvcNAQkEMSIEIMoV
+ PQYpb6Zf9fHMVI7fYfkEnA+3gLZ92vyf1p+05Jn4MAoGCCqGSM49BAMCBEcwRQIg
+ Rn+DgE+ulf2+N37i+4CrTQv3ve+VjwkZ+6TTD/AQmjQCIQDp2QYzJl1FaWXpkbmR
+ 0UE/J+NhRXn6kkyp7nUGs1Y/gqGCA7QwggOwBgorBgEEAYO/MAMBMYIDoASCA5wI
+ jIzJCBIiCiDA0j1q1AaXP5VZ87otHKAfhBR9j/xbhEXCJPmLlZGAHRoVCgxoYXNo
+ ZWRyZWtvcmQSBTAuMC4xIIy25qEGKkgKRjBEAiAsfOdZz8kcJtekpPZWadC36IOv
+ 3h9FJMMsIBe+7m7aowIgfioerM8yC8GgBa7uOHuFvXT04aMUx1gFCk7TLdAHMM8y
+ iQYIpf3KBhIgXqLVvFcpaU5EMF/fPebBSGsN2Vo9pWMKt0URqlhoFpkYpv3KBiIg
+ pthkFudFq2tR6i4TQTdEpjH44kmjaMGefgMCx2CX/MYiIFOjPPtkdT17sgh4eHnB
+ egpyo1IlSIQBoBWNP6kUHtmsIiBOsaQm4W2Pm319XZLqTpyTb2FIH67VnEdl14Oo
+ mcgaBSIge76TDfaxRCpFCNikrSj5Mk43TwEKxd+Sga2kHpgPLHsiIPjmzhbgn6DL
+ EeL3zjoqCudUmpTe6CglvUXshb90Ve62IiBDOYUeZ4icPH2zQeAQfbXWsTYnDqg+
+ WDnVzANzWfkNAyIgHV2F/1Ak+sOuRXQI6sgyi0C8fdS6LqSI5TwFlGkl5c0iIMIG
+ El+osngj/HSKFFsOJxXqhb1OXQUiQZoom3HRwcz/IiCX6AMB5FLqAGg6rd9qlckN
+ Q9yyyTzYJ46BPYM9uIv1UCIgfToSm87uhT0NgbdUMtkyLPJhweIRzEzcJb1HmQGM
+ spUiIPX72A2FM9pdqkHSckAElySSZMLi/4sBg4KRBP9QapDxIiDsTGUVVjpnakEe
+ RK0Gst8t/9osA3eH7roAyVvDtTRZVSIg1jCSwid4Bdy0yzYb6m4JrH7Z6ekZJyS4
+ 9R5X5UvfNTEiIJ4EAGbf5fAgBGWDhqxmzwu2/+hX7XHLM3x/VUXs9FWLKv4BCvsB
+ cmVrb3Iuc2lnc3RvcmUuZGV2IC0gMjYwNTczNjY3MDk3Mjc5NDc0NgoxMzgxMTM2
+ NgpYcUxWdkZjcGFVNUVNRi9mUGViQlNHc04yVm85cFdNS3QwVVJxbGhvRnBrPQpU
+ aW1lc3RhbXA6IDE2ODE0OTY4NDQ0ODY0MzI0MTYKCuKAlCByZWtvci5zaWdzdG9y
+ ZS5kZXYgd05JOWFqQkZBaUVBempKTGE0Wk1NOXNmaDlIVks0ZGowMDBzRG5pU0Z2
+ S3hxYmdtR3VUdThwMENJRSswMi90Wlh5Nnd4Vm5pWkhsbExRcEx1RFhLUnF1YjQ5
+ bW03VDVNQVlpTgo=
+ -----END SIGNED MESSAGE-----
+
+asdf
diff --git a/internal/rekor/oid/testdata/tlog.json b/internal/rekor/oid/testdata/tlog.json
new file mode 100644
index 00000000..690bc419
--- /dev/null
+++ b/internal/rekor/oid/testdata/tlog.json
@@ -0,0 +1,39 @@
+{
+ "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmZTI4YTkwOWY4OTVkY2JmNDU3OTVjMjhjNzJiYmZhNDg2MGM5YTdlMGFjOTJjNTBjZDFjN2M1MWEyNjNjM2I1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRVovZzRCUHJwWDl2amQrNHZ1QXEwMEw5NzN2bFk4SkdmdWswdy93RUpvMEFpRUE2ZGtHTXlaZFJXbGw2Wkc1a2RGQlB5ZmpZVVY1K3BKTXFlNTFCck5XUDRJPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZSRU5EUVd4UFowRjNTVUpCWjBsVlVDOXZWM0ZvT1hkQ2RVZEZiVkE1V1hwV1RIWXpMMWxFZFRGWmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVFUlRCTlZHZDVUbnBKZWxkb1kwNU5hazEzVGtSRk1FMVVaM3BPZWtsNlYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZXYlVsd05raDNhMjB3WTJGaVRHRktjMmRCT1RSdmNrMWxjbEJUY0dkTWVuaG5iVmdLVkRaSVptTm1lVzg0U1c1VlYyWjRWWGhHY1VWckswdENUMHQwZEZwVmVUVjNXV2hTVG1SNEt6WmFRV2xIVjNGalpEWlBRMEZZU1hkblowWjFUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZNWTA1aENubzFhV3BEYW5wM2RYTmtTbTluV200eVozVmpPRlZOZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsbldVUldVakJTUVZGSUwwSkNaM2RHYjBWVldXMXNjMkpJYkVGWk1taG9ZVmMxYm1SWFJubGFRelZyV2xoWmQwdFJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1FWRlJZbUZJVWpCalNFMDJUSGs1YUZreVRuWmtWelV3WTNrMWJtSXlPVzVpUjFWMVdUSTVkRTFEYzBkRGFYTkhRVkZSUW1jM09IZEJVV2RGQ2toUmQySmhTRkl3WTBoTk5reDVPV2haTWs1MlpGYzFNR041Tlc1aU1qbHVZa2RWZFZreU9YUk5TVWRMUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJZDBVS1pXZENORUZJV1VFelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIwaG5VVmR0ZDFGQlFRcENRVTFCVW5wQ1JrRnBRalIwYTJ0S04wdDNaSE5KZDJGVU1EQm1VelJpWlVkUVZtbDFXbUpJTVhjNFZsWnVRMDFZV0M5elVrRkphRUZNY0VjNEwwZG9DbmhCYTFkVmFWcFFhVWt2VVZsc2NEUldWbGhWZG5ac2FqbEJheXQzWTFab1VYVktaVTFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbU5CVFVkUlEwMUlaMUlLUjNoaVdIcEhNbmhRVW5rM2MwUmpNMmh4TURKcVp6WllibVpVVjBKc2FIZzVSMkpHWkdOU2FGUnJaekoxUlVWaE1FaENkVWRNVDJGUFRIVjFNRUZKZHdwUGFYSldkWFJ6Y0dkSE9YQldiVzVXVkd4bVlXcFVRM2huT1dSb056QXZReXRTYUVSQ2NUTmFlalpRYW1aQ1lXWnFTalZZWnpZek9EVlRTMEZTUlVWRkNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19",
+ "integratedTime": 1681496844,
+ "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
+ "logIndex": 17974796,
+ "verification": {
+ "inclusionProof": {
+ "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n14023763\nObGwMlDNPGiBMjt9BOFqk2H07lHLWQppUQuRE7wAg9o=\nTimestamp: 1681741564475733616\n\n— rekor.sigstore.dev wNI9ajBGAiEAll1SlTQdBQLIo0PHPTvfkQOQ+P5qzZuMaqslBqbdD4gCIQCj59tzyX12Z3iDVy8PRRZDJ1AF8gnp6bt6g0awPMwZbw==\n",
+ "hashes": [
+ "a6d86416e745ab6b51ea2e13413744a631f8e249a368c19e7e0302c76097fcc6",
+ "6c1caf296a95ba1a443187abb3b2673b774a0aab647b7f63f2b3bb2508b4eadd",
+ "53a33cfb64753d7bb208787879c17a0a72a35225488401a0158d3fa9141ed9ac",
+ "564af2b2877e2a41950c04108471c2bd0267544efa0cae23b4a6882c722732f8",
+ "0fa696e667612372a23495fc520a15bd5f7e0ff23829a56dd6ca0c77eebf8ac4",
+ "4eb1a426e16d8f9b7d7d5d92ea4e9c936f61481faed59c4765d783a899c81a05",
+ "ad3a1e4cc6112361b7eeb4cdd466357e6074920b96d9d28f0d95d312b8431d8f",
+ "7bbe930df6b1442a4508d8a4ad28f9324e374f010ac5df9281ada41e980f2c7b",
+ "ac4cfd341e5e6cd320c163f186303f236585b4317dd0a4a7789a0d6cc1360686",
+ "f8e6ce16e09fa0cb11e2f7ce3a2a0ae7549a94dee82825bd45ec85bf7455eeb6",
+ "4339851e67889c3c7db341e0107db5d6b136270ea83e5839d5cc037359f90d03",
+ "1d5d85ff5024fac3ae457408eac8328b40bc7dd4ba2ea488e53c05946925e5cd",
+ "c206125fa8b27823fc748a145b0e2715ea85bd4e5d0522419a289b71d1c1ccff",
+ "97e80301e452ea00683aaddf6a95c90d43dcb2c93cd8278e813d833db88bf550",
+ "646807ef641abbad2c6dac2de56d1119625a05ed80ae8532028061e345fd7e4e",
+ "7d3a129bceee853d0d81b75432d9322cf261c1e211cc4cdc25bd4799018cb295",
+ "1bc65fbc780ba3bbb0a4d55c3690ac2dc65bc4a59ee0dd58ed4c497da53c9dab",
+ "f5fbd80d8533da5daa41d272400497249264c2e2ff8b0183829104ff506a90f1",
+ "d1d0bf047266e39d6f450c219384a1d327a72344e43b07105d9f73363100ed28",
+ "ec4c6515563a676a411e44ad06b2df2dffda2c037787eeba00c95bc3b5345955",
+ "d63092c2277805dcb4cb361bea6e09ac7ed9e9e9192724b8f51e57e54bdf3531",
+ "9e040066dfe5f02004658386ac66cf0bb6ffe857ed71cb337c7f5545ecf4558b"
+ ],
+ "logIndex": 13811365,
+ "rootHash": "39b1b03250cd3c6881323b7d04e16a9361f4ee51cb590a69510b9113bc0083da",
+ "treeSize": 14023763
+ },
+ "signedEntryTimestamp": "MEQCIB1DYO4lIhrzVqP2lahPhW+IKiaMmrw2MwoEmsME2by5AiBTFR5YZvLNpMxKulwXST8QN9qjBqD52wwTn/fgW9OR0g=="
+ }
+}
diff --git a/internal/signature/sign.go b/internal/signature/sign.go
index ae9fe7b9..eb6033a5 100644
--- a/internal/signature/sign.go
+++ b/internal/signature/sign.go
@@ -17,13 +17,17 @@ package signature
import (
"bytes"
+ "context"
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
- cms "github.com/github/smimesign/ietf-cms"
+ cms "github.com/sigstore/gitsign/internal/fork/ietf-cms"
+ rekoroid "github.com/sigstore/gitsign/internal/rekor/oid"
+ "github.com/sigstore/gitsign/pkg/rekor"
+ "github.com/sigstore/rekor/pkg/generated/models"
)
type SignOptions struct {
@@ -49,6 +53,10 @@ type SignOptions struct {
// UserEmail specifies the email to match against. If present, signing
// will fail if the Fulcio identity SAN email does not match the git committer email.
UserEmail string
+
+ // Rekor client - if specified, Rekor details are embedded directly in the
+ // signature output.
+ Rekor rekor.Writer
}
// Identity is a copy of smimesign.Identity to allow for compatibility without
@@ -67,12 +75,21 @@ type Identity interface {
Close()
}
+// SignResponse is the response from Sign containing the signature and other related metadata.
+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 (e.g. SignOpts.Rekor was passed in)
+ LogEntry *models.LogEntryAnon
+}
+
// Sign signs a given payload for the given identity.
// The resulting signature and cert used is returned.
-func Sign(ident Identity, body []byte, opts SignOptions) ([]byte, *x509.Certificate, error) {
+func Sign(ctx context.Context, ident Identity, body []byte, opts SignOptions) (*SignResponse, error) {
cert, err := ident.Certificate()
if err != nil {
- return nil, nil, fmt.Errorf("failed to get identity certificate: %w", err)
+ return nil, fmt.Errorf("failed to get identity certificate: %w", err)
}
// If specified, check if retrieved identity matches the expected identity.
@@ -85,22 +102,22 @@ func Sign(ident Identity, body []byte, opts SignOptions) ([]byte, *x509.Certific
if len(cert.URIs) > 0 {
san = append(san, fmt.Sprintf("uri: %v", cert.URIs))
}
- return nil, nil, fmt.Errorf("gitsign.matchCommitter: certificate identity does not match config - want %s <%s>, got %s", opts.UserName, opts.UserEmail, strings.Join(san, ","))
+ return nil, fmt.Errorf("gitsign.matchCommitter: certificate identity does not match config - want %s <%s>, got %s", opts.UserName, opts.UserEmail, strings.Join(san, ","))
}
}
signer, err := ident.Signer()
if err != nil {
- return nil, 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)
if err != nil {
- return nil, nil, fmt.Errorf("failed to create signed data: %w", err)
+ return nil, fmt.Errorf("failed to create signed data: %w", err)
}
if err := sd.Sign([]*x509.Certificate{cert}, signer); err != nil {
- return nil, nil, fmt.Errorf("failed to sign message: %w", err)
+ return nil, fmt.Errorf("failed to sign message: %w", err)
}
if opts.Detached {
sd.Detached()
@@ -108,37 +125,53 @@ func Sign(ident Identity, body []byte, opts SignOptions) ([]byte, *x509.Certific
if len(opts.TimestampAuthority) > 0 {
if err = sd.AddTimestamps(opts.TimestampAuthority); err != nil {
- return nil, nil, fmt.Errorf("failed to add timestamp: %w", err)
+ return nil, fmt.Errorf("failed to add timestamp: %w", err)
}
}
chain, err := ident.CertificateChain()
if err != nil {
- return nil, nil, fmt.Errorf("failed to get identity certificate chain: %w", err)
+ return nil, fmt.Errorf("failed to get identity certificate chain: %w", err)
}
// TODO: look into adding back support for opts.IncludeCerts here.
// This was removed due to unstable ordering in the cert chain when
// intermediates were included.
if chain, err = certsForSignature(chain, 1); err != nil {
- return nil, nil, fmt.Errorf("failed to extract certificates from chain: %w", err)
+ return nil, fmt.Errorf("failed to extract certificates from chain: %w", err)
}
if err := sd.SetCertificates(chain); err != nil {
- return nil, nil, fmt.Errorf("failed to set certificates: %w", err)
+ return nil, fmt.Errorf("failed to set certificates: %w", err)
+ }
+
+ var lea *models.LogEntryAnon
+ if opts.Rekor != nil {
+ var err error
+ lea, err = attachRekorLogEntry(ctx, sd, cert, opts.Rekor)
+ if err != nil {
+ return nil, err
+ }
}
der, err := sd.ToDER()
if err != nil {
- return nil, nil, fmt.Errorf("failed to serialize signature: %w", err)
+ return nil, fmt.Errorf("failed to serialize signature: %w", err)
}
if opts.Armor {
- return pem.EncodeToMemory(&pem.Block{
- Type: "SIGNED MESSAGE",
- Bytes: der,
- }), cert, nil
+ return &SignResponse{
+ Signature: pem.EncodeToMemory(&pem.Block{
+ Type: "SIGNED MESSAGE",
+ Bytes: der,
+ }),
+ Cert: cert,
+ LogEntry: lea,
+ }, nil
}
-
- return der, cert, nil
+ return &SignResponse{
+ Signature: der,
+ Cert: cert,
+ LogEntry: lea,
+ }, nil
}
// certsForSignature determines which certificates to include in the signature
@@ -209,3 +242,34 @@ func matchSAN(cert *x509.Certificate, name, email string) bool {
return false
}
+
+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.
+ if len(raw.SignerInfos) < 1 {
+ return nil, fmt.Errorf("no SignerInfo found")
+ }
+ si := raw.SignerInfos[0]
+ message, err := si.SignedAttrs.MarshaledForVerification()
+ if err != nil {
+ return nil, err
+ }
+
+ // Store HashedRekord of the commit content that was signed.
+ lea, err := rekor.WriteMessage(ctx, message, si.Signature, cert)
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert LogEntry into attributes.
+ attrs, err := rekoroid.ToAttributes(lea)
+ if err != nil {
+ return nil, err
+ }
+ si.UnsignedAttrs = append(si.UnsignedAttrs, attrs...)
+ // SignerInfo isn't a pointer so we need to reassign it in the SignerInfo list.
+ raw.SignerInfos[0] = si
+
+ return lea, nil
+}
diff --git a/pkg/git/signature_test.go b/pkg/git/signature_test.go
index bee39362..0dd37a11 100644
--- a/pkg/git/signature_test.go
+++ b/pkg/git/signature_test.go
@@ -70,7 +70,7 @@ func TestSignVerify(t *testing.T) {
for _, detached := range []bool{true, false} {
t.Run(fmt.Sprintf("detached(%t)", detached), func(t *testing.T) {
- sig, _, err := signature.Sign(id, data, signature.SignOptions{
+ resp, err := signature.Sign(ctx, id, data, signature.SignOptions{
Detached: detached,
Armor: true,
// Fake CA outputs self-signed certs, so we need to use -1 to make sure
@@ -84,7 +84,7 @@ func TestSignVerify(t *testing.T) {
// Deprecated, included for completeness
t.Run("VerifySignature", func(t *testing.T) {
- if _, err := VerifySignature(data, sig, detached, roots, ca.ChainPool()); err != nil {
+ if _, err := VerifySignature(data, resp.Signature, detached, roots, ca.ChainPool()); err != nil {
t.Fatalf("Verify() = %v", err)
}
})
@@ -94,7 +94,7 @@ func TestSignVerify(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if _, err := cv.Verify(ctx, data, sig, detached); err != nil {
+ if _, err := cv.Verify(ctx, data, resp.Signature, detached); err != nil {
t.Fatalf("Verify() = %v", err)
}
})
diff --git a/pkg/git/verify.go b/pkg/git/verify.go
index 1e7d81aa..c9d011d0 100644
--- a/pkg/git/verify.go
+++ b/pkg/git/verify.go
@@ -74,11 +74,19 @@ func Verify(ctx context.Context, git Verifier, rekor rekor.Verifier, data, sig [
}
claims = append(claims, NewClaim(ClaimValidatedSignature, true))
+ if tlog, err := rekor.VerifyOffline(ctx, sig); err == nil {
+ return &VerificationSummary{
+ Cert: cert,
+ LogEntry: tlog,
+ Claims: claims,
+ }, nil
+ }
+
+ // Legacy commit based lookup.
commit, err := ObjectHash(data, sig)
if err != nil {
return nil, err
}
-
tlog, err := rekor.Verify(ctx, commit, cert)
if err != nil {
return nil, fmt.Errorf("failed to validate rekor entry: %w", err)
diff --git a/pkg/rekor/rekor.go b/pkg/rekor/rekor.go
index b8c0d59b..6fdb8b2f 100644
--- a/pkg/rekor/rekor.go
+++ b/pkg/rekor/rekor.go
@@ -22,6 +22,7 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
+ "encoding/pem"
"errors"
"fmt"
"strings"
@@ -31,6 +32,8 @@ import (
"github.com/go-openapi/swag"
"github.com/sigstore/cosign/v2/pkg/cosign"
+ cms "github.com/sigstore/gitsign/internal/fork/ietf-cms"
+ rekoroid "github.com/sigstore/gitsign/internal/rekor/oid"
rekor "github.com/sigstore/rekor/pkg/client"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/client/index"
@@ -43,14 +46,16 @@ import (
"github.com/sigstore/sigstore/pkg/tuf"
)
-// Verifier represents a mechanism to get and verify Rekor entries for the given Git commit.
+// Verifier represents a mechanism to get and verify Rekor entries for the given Git data.
type Verifier interface {
Verify(ctx context.Context, commitSHA string, cert *x509.Certificate) (*models.LogEntryAnon, error)
+ VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error)
}
// 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.
@@ -74,16 +79,21 @@ func New(url string, opts ...rekor.Option) (*Client, error) {
}, nil
}
+// Deprecated: Use [WriteMessage] instead.
func (c *Client) Write(ctx context.Context, commitSHA string, sig []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) {
+ return c.WriteMessage(ctx, []byte(commitSHA), sig, cert)
+}
+
+func (c *Client) WriteMessage(ctx context.Context, message, signature []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) {
pem, err := cryptoutils.MarshalCertificateToPEM(cert)
if err != nil {
return nil, err
}
checkSum := sha256.New()
- if _, err := checkSum.Write([]byte(commitSHA)); err != nil {
+ if _, err := checkSum.Write(message); err != nil {
return nil, err
}
- return cosign.TLogUpload(ctx, c.Rekor, sig, checkSum, pem)
+ return cosign.TLogUpload(ctx, c.Rekor, signature, checkSum, pem)
}
func (c *Client) get(ctx context.Context, data []byte, cert *x509.Certificate) (*models.LogEntryAnon, error) {
@@ -163,6 +173,15 @@ func rekorPubsFromClient(rekorClient *client.Rekor) (*cosign.TrustedTransparency
return &publicKeys, nil
}
+// Verify verifies a commit using online verification.
+//
+// This is done by:
+// 1. Searching Rekor for an entry matching the commit SHA + cert.
+// 2. Use the same cert to verify the commit content.
+//
+// Note: While not truly deprecated, Client.VerifyOffline is generally preferred.
+// This function relies on non-GA behavior of Rekor, and remains for backwards
+// compatibility with older signatures.
func (c *Client) Verify(ctx context.Context, commitSHA string, cert *x509.Certificate) (*models.LogEntryAnon, error) {
e, err := c.get(ctx, []byte(commitSHA), cert)
if err != nil {
@@ -225,3 +244,52 @@ func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
func (c *Client) PublicKeys() *cosign.TrustedTransparencyLogPubKeys {
return c.publicKeys
}
+
+// VerifyOffline verifies a signature using offline verification.
+// Unlike Client.Verify, only the commit content is considered during verification.
+func (c *Client) VerifyOffline(ctx context.Context, sig []byte) (*models.LogEntryAnon, error) {
+ // Try decoding as PEM
+ var der []byte
+ if blk, _ := pem.Decode(sig); blk != nil {
+ der = blk.Bytes
+ } else {
+ der = sig
+ }
+ // Parse signature
+ sd, err := cms.ParseSignedData(der)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse signature: %w", err)
+ }
+
+ // Generate verification options.
+ certs, err := sd.GetCertificates()
+ if err != nil {
+ return nil, fmt.Errorf("error getting signature certs: %w", err)
+ }
+ if len(certs) == 0 {
+ return nil, fmt.Errorf("no certificates found in signature")
+ }
+ cert := certs[0]
+
+ // Recompute HashedRekord body by rehashing the authenticated attributes.
+ r := sd.Raw()
+ if len(r.SignerInfos) == 0 {
+ return nil, fmt.Errorf("no signers found in signature")
+ }
+ si := r.SignerInfos[0]
+ message, err := si.SignedAttrs.MarshaledForVerification()
+ if err != nil {
+ return nil, err
+ }
+
+ tlog, err := rekoroid.ToLogEntry(ctx, message, si.Signature, cert, si.UnsignedAttrs)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := cosign.VerifyTLogEntryOffline(ctx, tlog, c.PublicKeys()); err != nil {
+ return nil, err
+ }
+
+ return tlog, nil
+}