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 +}