One of the main values of sigstore is to sign/validate/verify artifacts, like thirdparty software dependencies, to avoid supply chain attacks. It has not been very easy to sign code/modules/containers etc before which has led to people simply not doing it. It is the software and also the maintaince of keys which adds makes this difficult.
sigstore is a project under the CNFC and it goal is to provide a
software-signing equivalent to Let's Encrypt
. It is not just one tool but a
collection of tools namely; fulico
, rekor
, and cosign
.
With sigstore we don't have to manage keys, and it makes it simpler to handle revocation.
$ go install github.com/sigstore/cosign/cmd/cosign@latest
Install crane which is needed for some commands:
go install github.com/google/go-containerregistry/cmd/crane@latest
The following section discuss and show output from running the following cosign command:
COSIGN_EXPERIMENTAL=1 \
../cosign sign-blob \
--output-certificate firmware.crt \
--output-signature firmware.sig \
firmware.bin
Is a root CA for code signing certs, and issues code-signing certificates. Importantly it is an automatic certificate authority.
Before we can send a certificate request to Fulcio we need to first accuire an OpenID Connect token. This token will be a JSON Web Token (JWT) and is a base64 encoded string with three parts:
base64(header).base64(payload).signature
For example, a decoded JWT might look like this: Header:
{
"alg": "RS256",
"kid": "9a3e6e2cf1821e020288a8c380a6856b16aae69f"
}
Payload:
{
"iss": "https://oauth2.sigstore.dev/auth",
"sub": "CgY0MzIzNTESJmh0dHBzOiUyRiUyRmdpdGh1Yi5jb20lMkZsb2dpbiUyRm9hdXRo",
"aud": "sigstore",
"exp": 1668669015,
"iat": 1668668955,
"nonce": "2HfGuvSQXxt99jj0vA3Q4i4z3PR",
"at_hash": "EFHCQ52e1Wy43Z9Buzow_A",
"c_hash": "y5o-9uXlkDrnKgX28-DLIw",
"email": "[email protected]",
"email_verified": true,
"federated_claims": {
"connector_id": "https://github.com/login/oauth",
"user_id": "432351"
}
}
Signature:
rTMenbpytO2EqK9LQmHaFltwO4RLaWOrYHuS7MOEzIEv-4dmbxEr2oJTdgpLTnEDFqoLEp3TLEfyr9OO8CYYIVUJcUydhIILWWOC1zKlA56gwU-17uzvlImj6kucN9ezHfScaKl-Mkv4zmxXtrLBxjIKKC6T63H0pyUzj0e_uLS-W3yzxOQbqs-LLOKaRPJGeftDNvHI7B2GfWlpVLwABkc3qNH7Sh1lmfTrGmZ3QyGQ6Vi-apmqzcuBTkCKTeqqg4fYi_Vtw7GkGXYeN0PIEW3oYJ3WOObzvSyRSbzxLsziyQDq9HRzxZloBJk0EPBSoC3qvj6-qYh7ZLf2W5-0Og
Now if we look at the header we see that we have an algorithm which in this case
is RSA 256, and a key id. This is the public key id that can be used to verify
the signature of the header and payload. This signature was generated by the
identity providers its private key. So how do we find the public key?
There is a public endpoint exposed by most identity providers that can be used
get information about the identity provider:
$ curl -s https://oauth2.sigstore.dev/auth/.well-known/openid-configuration | jq '.jwks_uri'
"https://oauth2.sigstore.dev/auth/keys"
And we can use that url to find out information about the keys:
$ curl -s https://oauth2.sigstore.dev/auth/keys | jq
{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "9a3e6e2cf1821e020288a8c380a6856b16aae69f",
"alg": "RS256",
"n": "vuS5WUPi3m2fbfLFWdYqOUifg0-x0IW1vbtCklG4mUielqmEMgHOQmc47ZRi39LWw2fZUJmfp-q3R0wEJruDv1ZIKHQugKZpT2xqN936jV2Jtrz_WcBHHGZM9XfaOVbh_WnukjaLPtHkvX_bu3dl-dWYNF0IGA2OmmTOp4heQeqFujBaFWb9ZKbzo9pOIFwKDwKjWMgnLVDMHmEV2UmB6pad7zoMQEPMxUDHr2bSNDL9QicewBJcrVMKd-QDwZ8p4T3dNfnA9Lztrx6A-Pb7VH5x2AAtAcWNcMKXWnHd5QoGkCzyBdY5LBvqDUhBmY2ek7rH4F54L7VJx6tvZNiRxQ",
"e": "AQAB"
},
{
"use": "sig",
"kty": "RSA",
"kid": "1456029e0714f659284b99cc6b27bdd61a455d7f",
"alg": "RS256",
"n": "9fZODOmWJINdAetTxzZkLnk3Vr9_jb6--NX284FQtxIxb-tbHR8oEW94Wl-moe76pGfk0SFJac2uq6_C-805kkaqgIQqgES_CCD0-8BXikdY1-5Q-dj6yQU98GUhrRDxx-WsdGnyrbsSrUre32_WQkaEQEP2IcEo63b3Y4F4WZ8arBFua_rYz0uwR3oX7HuOTIhGi5R6oy_FSsx2NYxlqnJxSWc7vv9GiQ3WabRtAN2OiETSIM-SNpRCjf7WngfBq2gcSvPJ8mp-epARPQUe0FCevZT_dq4YU29okOn9mRwo_s7eiy-9JIuTWbTYmUyobERXFaEMWSzpyzC_f6ts_w",
"e": "AQAB"
}
]
AQAB}
So we have n
and e
which together make out the public key in RSA.
$ curl -s https://oauth2.sigstore.dev/auth/keys | jq -c '.keys[] | select (.kid | contains("1456029e0714f659284b99cc6b27bdd61a455d7f"))' | jq -r '.n'
$ curl -s https://oauth2.sigstore.dev/auth/keys | jq -c '.keys[] | select (.kid | contains("1456029e0714f659284b99cc6b27bdd61a455d7f"))' | jq -r '.e' > pub-exponent
We can decode this into:
$ openssl rsa -pubin -in pubkey.pem -text -noout
RSA Public-Key: (2048 bit)
Modulus:
00:f5:f6:4e:0c:e9:96:24:83:5d:01:eb:53:c7:36:
64:2e:79:37:56:bf:7f:8d:be:be:f8:d5:f6:f3:81:
50:b7:12:31:6f:eb:5b:1d:1f:28:11:6f:78:5a:5f:
a6:a1:ee:fa:a4:67:e4:d1:21:49:69:cd:ae:ab:af:
c2:fb:cd:39:92:46:aa:80:84:2a:80:44:bf:08:20:
f4:fb:c0:57:8a:47:58:d7:ee:50:f9:d8:fa:c9:05:
3d:f0:65:21:ad:10:f1:c7:e5:ac:74:69:f2:ad:bb:
12:ad:4a:de:df:6f:d6:42:46:84:40:43:f6:21:c1:
28:eb:76:f7:63:81:78:59:9f:1a:ac:11:6e:6b:fa:
d8:cf:4b:b0:47:7a:17:ec:7b:8e:4c:88:46:8b:94:
7a:a3:2f:c5:4a:cc:76:35:8c:65:aa:72:71:49:67:
3b:be:ff:46:89:0d:d6:69:b4:6d:00:dd:8e:88:44:
d2:20:cf:92:36:94:42:8d:fe:d6:9e:07:c1:ab:68:
1c:4a:f3:c9:f2:6a:7e:7a:90:11:3d:05:1e:d0:50:
9e:bd:94:ff:76:ae:18:53:6f:68:90:e9:fd:99:1c:
28:fe:ce:de:8b:2f:bd:24:8b:93:59:b4:d8:99:4c:
a8:6c:44:57:15:a1:0c:59:2c:e9:cb:30:bf:7f:ab:
6c:ff
Exponent: 65537 (0x10001)
So the verifying party can indeed get the public key by using the kid
and
the public endpoint to get the modulus and the public exponent.
TODO: check in scripts that shows these steps.
For more information about jws see jwt.md.
The JWT, which is base64 encoded, is set in the http header of the certificate request that is then sent to Fulcio
{
"publicKey": {
"content": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6LMMj8/QTpGz5gC4TGaFi3jLPh7qwXEewB1Hl2TX2FH/N5ZmoYU0ZlhHm7tGQOY03UwBpIKlGh9yobp2/NAJzQ==",
"algorithm": "ecdsa"
},
"signedEmailAddress": "MEUCIQCaryidkKEVkSNnI+t4uoWolyxpfUWVukp/CWunzFW2HgIgfzJC2+JP0sjfX8nNaTL1tmpPYnco11hrJnllLex31Ro=",
"certificateSigningRequest": null
}
Now, the client will generate a keypair for this communication and send the public key as shown above, the private key of this pair will have been used to sign the email address.
The certificates that Fulicio provides are not like the ones we use for TLS but they are intended for code signing.
base64 -d firmwarecrt | openssl x509 --noout --text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
23:6f:80:29:d7:da:ad:93:d0:17:67:c3:49:df:fa:ce:59:ed:06:33
Signature Algorithm: ecdsa-with-SHA384
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
Not Before: Nov 17 07:09:15 2022 GMT
Not After : Nov 17 07:19:15 2022 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:a7:cf:ac:cc:a1:65:3b:c0:e7:09:ac:2d:12:68:
c1:57:a3:48:9f:4e:fd:3a:d8:37:cd:9c:03:95:f3:
38:b5:b6:1d:27:60:98:2b:d8:33:ee:76:88:91:c8:
08:42:d6:ae:58:1a:d9:cc:59:97:5f:7d:8f:0e:e6:
02:bd:29:86:f6
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Key Identifier:
01:1D:36:CB:AC:36:6E:9D:71:C1:A1:6F:88:6F:CB:5C:96:5B:10:31
X509v3 Authority Key Identifier:
keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
X509v3 Subject Alternative Name: critical
email:[email protected]
1.3.6.1.4.1.57264.1.1:
https://github.com/login/oauth
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
Timestamp : Nov 17 07:09:15.629 2022 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:45:02:21:00:89:F4:6A:A2:B9:2F:D2:38:12:F6:0A:
16:EA:FA:18:B7:07:D1:B4:0D:33:F6:BE:67:F7:FC:4A:
9D:58:71:46:39:02:20:6A:C9:B9:A1:DE:EA:CE:D2:AB:
6C:83:6E:AF:30:92:66:A7:07:3B:3C:8A:9C:59:FF:68:
60:07:8A:B8:D6:A5:BD
Signature Algorithm: ecdsa-with-SHA384
30:65:02:30:27:0a:2f:e8:ed:19:9e:80:f5:f6:5c:86:33:d5:
9f:3a:f1:07:83:46:95:83:c1:ed:3a:fb:30:33:66:a0:87:7d:
90:0c:97:73:87:1e:aa:66:6f:ce:f0:1f:60:7e:c0:41:02:31:
00:f3:73:f4:fc:e0:4f:5d:cf:c2:f7:39:ca:f2:ed:cb:af:55:
09:02:8e:23:36:cc:91:22:c6:1e:3e:78:05:55:6d:a5:96:93:
24:9e:eb:f3:7b:09:c9:ff:f4:53:21:a3:82
For the meaning of critical see certificates.md.
The Subject
field is also different. This is typically a domain name in a TLS
certificate but in a code-signing certificate these are email addresses (people)
or SPIFFE SVID (workloads).
Fulcio has to validate the user/system that is requesting a certificate to be created for it. OpenID Connect (OIDC) can be used so it will handle the email address, Fulcio signs X.509 certificates valid for 10 minutes.
Every certificate issued will be appended to a public Certificate Transparency (CT) log. This log can be inspected by anyone and the certificates can be used to verify signatures. The CT log is specified in RFC-6962. This allows anyone to audit/check issued certificates. This is not to be confused with Rekor which is the transparency log which stores signed artifacts and attestations.
So Fulcio will first create a special x.509 extension called a poision extension in the certifiate before it is added to the CT log. This kind of cert is called a pre-certifcate and is not useful by clients at this stage.
The response from the CT is a Signed Certificate Timestamp (SCT) which is a promise of inclusion in the CT log. Now, this SCT is embedded into the certificate and it is signed again to include this information in the signature. Then the certificate is returned to the client.
The cosign sign-blob
command will also uplaod a rekord to Rekor.
Is the transparency log which is immutable, append only log which can be used by other parties to check what was signed and by whom.
It can hold signed metadata generated by software projects. So a project can sign any metadata and have this metadata recorded in Rekor. This allows consumers of the artifacts that the metadata is about to verify and either trust or not trust the artifacts.
Note that Rekor does not store the actual data we are signing, which is a good thing as that could mean that it would require a lot of storage, and also that data might be private.
So in what format does Rekor store the data it handles?
Rekor allows for pluggable-types, or schemas, for what is stored in the log.
The currently supported schemas can be found in openapi.yaml.
The example we have been using will upload a json object that looks like this:
{
"apiVersion": "0.0.1",
"spec": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95"
}
},
"signature": {
"content": "MEYCIQCd0q6y+7zYTiGHd4Ej6IHDJ5FTiXQTJiiYcpgHYwSclAIhALHGzEMYUlTqPOETIaCoYZIAFDHHlt16wE15FtUzAidS",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxRENDQWk2Z0F3SUJBZ0lVSTIrQUtkZmFyWlBRRjJmRFNkLzZ6bG50QmpNd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJeE1URTNNRGN3T1RFMVdoY05Nakl4TVRFM01EY3hPVEUxV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVwOCtzektGbE84RG5DYXd0RW1qQlY2TkluMDc5T3RnM3pad0QKbGZNNHRiWWRKMkNZSzlnejduYUlrY2dJUXRhdVdCclp6Rm1YWDMyUER1WUN2U21HOXFPQ0FVMHdnZ0ZKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVBUjAyCnk2dzJicDF4d2FGdmlHL0xYSlpiRURFd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHCkFRUUIxbmtDQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnUKamdBQUFZU0VhNXZ0QUFBRUF3QkhNRVVDSVFDSjlHcWl1Uy9TT0JMMkNoYnEraGkzQjlHMERUUDJ2bWYzL0VxZApXSEZHT1FJZ2FzbTVvZDdxenRLcmJJTnVyekNTWnFjSE96eUtuRm4vYUdBSGlyaldwYjB3Q2dZSUtvWkl6ajBFCkF3TURhQUF3WlFJd0p3b3Y2TzBabm9EMTlseUdNOVdmT3ZFSGcwYVZnOEh0T3Zzd00yYWdoMzJRREpkemh4NnEKWm0vTzhCOWdmc0JCQWpFQTgzUDAvT0JQWGMvQzl6bks4dTNMcjFVSkFvNGpOc3lSSXNZZVBuZ0ZWVzJsbHBNawpudXZ6ZXduSi8vUlRJYU9DCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
}
}
},
"kind": "hashedrekord"
Now if we take a look at the `spec.hash.value' we can see that it is the same value as the data we signed (firmware.bin):
$ sha256sum firmware.bin
311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95 firmware.bin
And we can see that 'signature.content' matches the signature that was returned from Fulcio:
$ cat firmware.sig
MEYCIQCd0q6y+7zYTiGHd4Ej6IHDJ5FTiXQTJiiYcpgHYwSclAIhALHGzEMYUlTqPOETIaCoYZIAFDHHlt16wE15FtUzAidS
And we can see the signature.publicKey.content
matches the certificate that
we got from Fulcio:
$ cat firmware.crt
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxRENDQWk2Z0F3SUJBZ0lVSTIrQUtkZmFyWlBRRjJmRFNkLzZ6bG50QmpNd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJeE1URTNNRGN3T1RFMVdoY05Nakl4TVRFM01EY3hPVEUxV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVwOCtzektGbE84RG5DYXd0RW1qQlY2TkluMDc5T3RnM3pad0QKbGZNNHRiWWRKMkNZSzlnejduYUlrY2dJUXRhdVdCclp6Rm1YWDMyUER1WUN2U21HOXFPQ0FVMHdnZ0ZKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVBUjAyCnk2dzJicDF4d2FGdmlHL0xYSlpiRURFd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHCkFRUUIxbmtDQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnUKamdBQUFZU0VhNXZ0QUFBRUF3QkhNRVVDSVFDSjlHcWl1Uy9TT0JMMkNoYnEraGkzQjlHMERUUDJ2bWYzL0VxZApXSEZHT1FJZ2FzbTVvZDdxenRLcmJJTnVyekNTWnFjSE96eUtuRm4vYUdBSGlyaldwYjB3Q2dZSUtvWkl6ajBFCkF3TURhQUF3WlFJd0p3b3Y2TzBabm9EMTlseUdNOVdmT3ZFSGcwYVZnOEh0T3Zzd00yYWdoMzJRREpkemh4NnEKWm0vTzhCOWdmc0JCQWpFQTgzUDAvT0JQWGMvQzl6bks4dTNMcjFVSkFvNGpOc3lSSXNZZVBuZ0ZWVzJsbHBNawpudXZ6ZXduSi8vUlRJYU9DCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K$
So, from our command that is what is sent to Rekor, a hash over the data and not the data itself. The signature over that hash, and the certificate that can be used to verify the signature. At least that is my understanding of how things work.
The response from Rekor will be a log entry:
cat logentry.json | jq
{
"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzMTEzNzU5MDhiMmExMDY4OGZiODg0MWI2MWQ4YjFkYWE5YTNlOTA0Zjg0ZDJlZDg4ZDBhMzVjYjRmMGUxYTk1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNkMHE2eSs3ellUaUdIZDRFajZJSERKNUZUaVhRVEppaVljcGdIWXdTY2xBSWhBTEhHekVNWVVsVHFQT0VUSWFDb1laSUFGREhIbHQxNndFMTVGdFV6QWlkUyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnhSRU5EUVdrMlowRjNTVUpCWjBsVlNUSXJRVXRrWm1GeVdsQlJSakptUkZOa0x6WjZiRzUwUW1wTmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFVUlROTlJHTjNUMVJGTVZkb1kwNU5ha2w0VFZSRk0wMUVZM2hQVkVVeFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ3T0N0emVrdEdiRTg0Ukc1RFlYZDBSVzFxUWxZMlRrbHVNRGM1VDNSbk0zcGFkMFFLYkdaTk5IUmlXV1JLTWtOWlN6bG5lamR1WVVsclkyZEpVWFJoZFZkQ2NscDZSbTFZV0RNeVVFUjFXVU4yVTIxSE9YRlBRMEZWTUhkblowWktUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCVWpBeUNuazJkekppY0RGNGQyRkdkbWxITDB4WVNscGlSVVJGZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwM1dVUldVakJTUVZGSUwwSkNNSGRITkVWYVdrZEdkV0ZYVm5OTWJVcHNaRzFXZFdGWVZucFJSMlIwV1Zkc2MweHRUblppVkVGelFtZHZjZ3BDWjBWRlFWbFBMMDFCUlVKQ1FqVnZaRWhTZDJONmIzWk1NbVJ3WkVkb01WbHBOV3BpTWpCMllrYzVibUZYTkhaaU1rWXhaRWRuZDJkWmIwZERhWE5IQ2tGUlVVSXhibXREUWtGSlJXWkJValpCU0dkQlpHZEVaRkJVUW5GNGMyTlNUVzFOV2tob2VWcGFlbU5EYjJ0d1pYVk9ORGh5Wml0SWFXNUxRVXg1Ym5VS2FtZEJRVUZaVTBWaE5YWjBRVUZCUlVGM1FraE5SVlZEU1ZGRFNqbEhjV2wxVXk5VFQwSk1Na05vWW5FcmFHa3pRamxITUVSVVVESjJiV1l6TDBWeFpBcFhTRVpIVDFGSloyRnpiVFZ2WkRkeGVuUkxjbUpKVG5WeWVrTlRXbkZqU0U5NmVVdHVSbTR2WVVkQlNHbHlhbGR3WWpCM1EyZFpTVXR2V2tsNmFqQkZDa0YzVFVSaFFVRjNXbEZKZDBwM2IzWTJUekJhYm05RU1UbHNlVWROT1ZkbVQzWkZTR2N3WVZabk9FaDBUM1p6ZDAweVlXZG9NekpSUkVwa2VtaDRObkVLV20wdlR6aENPV2RtYzBKQ1FXcEZRVGd6VURBdlQwSlFXR012UXpsNmJrczRkVE5NY2pGVlNrRnZOR3BPYzNsU1NYTlpaVkJ1WjBaV1Z6SnNiSEJOYXdwdWRYWjZaWGR1U2k4dlVsUkpZVTlEQ2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn19fX0=",
"integratedTime": 1668668956,
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
"logIndex": 7257243,
"verification": {
"inclusionProof": {
"checkpoint": "rekor.sigstore.dev - 2605736670972794746\n3093813\noxeC1ADPhXe1kAfHfarju6sBF59FXLEQ+9a/6tG+Nw0=\nTimestamp: 1668668956499493067\n\n— rekor.sigstore.dev wNI9ajBEAiBXRrpFSv5wvAs8Y7HhhIt4fohFciubpp6wX9UdnjF3vAIgfdRpAK6+/DfHe0jb6PkyG0hfylWkCCVfhMfGKntB61k=\n",
"hashes": [
"65a0aba5d75dcd8e3fd0b551dd0d4ffd6972c0a60360a35398e5cf25fda3b002",
"06d367abc373cda7531ac9ca302afce88cc24ff57b1731e13c8251b7f0b0282f",
"798ea9dfe348671b9a0570cbf5314df3cfdc2ca50d0f5cae5b94c065e35cd719",
"bf4f21e35771b64629af8fab20212c89f14a1bd21aa40c25adf35f58a9914729",
"9428810f5b8380d30e8e4ffb5e46a561de3aa979dfee33ed3297edf253f29862",
"b4f1e7db3e4f4ad295073af3085ec987c8a6997be537a2d027d7ca58fb295f7a",
"1184c2a7a2094b4b8ec5eb58d5a0fce22dae2a68eca3230d92c113c60dc812ac",
"fc77f10859b9c663ac33a28d4f72203326a7dbd0fa45acd8655fa536b290232d",
"be4c4c0f9190fd5be4b2498ef7cc28a90d9818ce00ee8b3389121b4ba3eff4ba",
"9a991dc567e6dd4fa21d6235df691252ca81eab5e1301c849c8aa2151363e6a7",
"732af72ebcc0c8a16dbdba657c7b755d2097691d08882f308dbc5b133372c170",
"2747468d0ed5e5b1138bba7b7968367a9842437d9004b3166391f115cb867d1e"
],
"logIndex": 3093812,
"rootHash": "a31782d400cf8577b59007c77daae3bbab01179f455cb110fbd6bfead1be370d",
"treeSize": 3093813
},
"signedEntryTimestamp": "MEQCIB2iQ2cM80vbtSW+aG+TyUl3GrEs3K5r8ZyJgEo5mHlXAiBGuL5HTLoO1aplgcWDpAmpO+aMvrL7g8L/i50vP3flbg=="
}
}
Now, the body is base64 encoded which we can inspect using:
$ jq --raw-output '.body' logentry.json | base64 --decode | jq --sort-keys
{
"apiVersion": "0.0.1",
"kind": "hashedrekord",
"spec": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95"
}
},
"signature": {
"content": "MEYCIQCd0q6y+7zYTiGHd4Ej6IHDJ5FTiXQTJiiYcpgHYwSclAIhALHGzEMYUlTqPOETIaCoYZIAFDHHlt16wE15FtUzAidS",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxRENDQWk2Z0F3SUJBZ0lVSTIrQUtkZmFyWlBRRjJmRFNkLzZ6bG50QmpNd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJeE1URTNNRGN3T1RFMVdoY05Nakl4TVRFM01EY3hPVEUxV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVwOCtzektGbE84RG5DYXd0RW1qQlY2TkluMDc5T3RnM3pad0QKbGZNNHRiWWRKMkNZSzlnejduYUlrY2dJUXRhdVdCclp6Rm1YWDMyUER1WUN2U21HOXFPQ0FVMHdnZ0ZKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVBUjAyCnk2dzJicDF4d2FGdmlHL0xYSlpiRURFd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHCkFRUUIxbmtDQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnUKamdBQUFZU0VhNXZ0QUFBRUF3QkhNRVVDSVFDSjlHcWl1Uy9TT0JMMkNoYnEraGkzQjlHMERUUDJ2bWYzL0VxZApXSEZHT1FJZ2FzbTVvZDdxenRLcmJJTnVyekNTWnFjSE96eUtuRm4vYUdBSGlyaldwYjB3Q2dZSUtvWkl6ajBFCkF3TURhQUF3WlFJd0p3b3Y2TzBabm9EMTlseUdNOVdmT3ZFSGcwYVZnOEh0T3Zzd00yYWdoMzJRREpkemh4NnEKWm0vTzhCOWdmc0JCQWpFQTgzUDAvT0JQWGMvQzl6bks4dTNMcjFVSkFvNGpOc3lSSXNZZVBuZ0ZWVzJsbHBNawpudXZ6ZXduSi8vUlRJYU9DCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
}
}
}
}
Notice that this matches the content we uploaded.
And the signedEntryTimestamp
is evidence that it was added to the log:
$ cat logentry.json | jq '.verification.signedEntryTimestamp'
"MEQCIB2iQ2cM80vbtSW+aG+TyUl3GrEs3K5r8ZyJgEo5mHlXAiBGuL5HTLoO1aplgcWDpAmpO+aMvrL7g8L/i50vP3flbg=="
Rekor has a restful API server, https://rekor.sigstore.dev. Example of retrieving a log entry can be done using:
$ curl -s https://rekor.sigstore.dev/api/v1/log/entries?logIndex=3321511 | jq
Installing rekor-cli
:
$ go install -v github.com/sigstore/rekor/cmd/rekor-cli@latest
Is a signing tool which I think is named after container signing but it can be used to sign anything blob of data.
Signing steps:
- A keypair for code signing is generated.
- The user authenticates to an OpenID Connect Provider (OIDC) to verify the ownership of their email address.
- Upon successful authentication a code-signing cert is received.
- The code-signing cert is published to Rekor, the transparency log.
- User signs an artifact using the certificatate and the private key from the keypair.
- The signature from the signed artifact is published to Rekor.
- The keypair used are deleted
- The signed artifact can be published.
The examples in this section can be found in the container-image directory.
So first we start by creating a container image:
$ make container-image
podman build -t ttl.sh/danbev-simple-container:2h .
STEP 1/2: FROM scratch
STEP 2/2: CMD ["echo "something"]
--> Using cache ad48f6de0a4343b0cd92991120e973782862e70040e47ba60612d38d5ed920d4
COMMIT ttl.sh/danbev-simple-container:2h
--> ad48f6de0a4
Successfully tagged ttl.sh/danbev-simple-container:2h
Successfully tagged localhost/simple-container:latest
ad48f6de0a4343b0cd92991120e973782862e70040e47ba60612d38d5ed920d4
We can list it to see the repository, tag, and image id:
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
ttl.sh/danbev-simple-container 2h ad48f6de0a43 4 minutes ago 1.12 kB
$ cd sigstore/container-image
$ podman build -t simple-container .
Then we can push this container image to a registry:
$ make push
podman push "ttl.sh/danbev-simple-container:2h"
Getting image source signatures
Copying config ad48f6de0a done
Writing manifest to image destination
Storing signatures
So now we have an image that is in a container registry. We can now sign it using cosign. First we create a key pair to use when signing:
$ make keys
I've left the password empty.
Now, we can sign the image using:
$ make sign
cosign sign -key cosign.key "ttl.sh/danbev-simple-container:2h"
Pushing signature to: ttl.sh/danbev-simple-container
We can use crane to get the digest of the image:
$ make digest
crane digest "ttl.sh/danbev-simple-container:2h"
sha256:96d13e1500053d6f21aee389b74c5826b3192cda9dd226a6026cef0474a351da
So that is the hash of the image, which uses sha256. This is a hash of the manifest of the image:
$ make manifest
crane manifest "ttl.sh/danbev-simple-container:2h" | jq
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:ad48f6de0a4343b0cd92991120e973782862e70040e47ba60612d38d5ed920d4",
"size": 433
},
"layers": [],
"annotations": {
"org.opencontainers.image.base.digest": "",
"org.opencontainers.image.base.name": ""
}
}
And we can verify this using by passing the manifest file to sha256sum
:
$ make digest-manifest
crane manifest "ttl.sh/danbev-simple-container:2h" | sha256sum
96d13e1500053d6f21aee389b74c5826b3192cda9dd226a6026cef0474a351da -
This produces the same output as crane digest.
Now, sigstore will use this hash to push an image using that hash as part of its name. The name of this image can be retrieved using:
$ make triangulate
cosign triangulate "ttl.sh/danbev-simple-container:2h"
ttl.sh/danbev-simple-container:sha256-96d13e1500053d6f21aee389b74c5826b3192cda9dd226a6026cef0474a351da.sig
Notice that the tag here is the same as the output of the digest command above.
We can use crane manifest
to see the manifest of this signature image:
$ crane manifest ttl.sh/danbev-simple-container:sha256-96d13e1500053d6f21aee389b74c5826b3192cda9dd226a6026cef0474a351da.sig | jq
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 248,
"digest": "sha256:fca11d85342bd4bde3708cd2712dec318322852e5d1e220729356e0c6478a5bd"
},
"layers": [
{
"mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
"size": 246,
"digest": "sha256:754122687a83f6ed95dfd06e354238ec7c3805d5910f77fee0469d624d0abe81",
"annotations": {
"dev.cosignproject.cosign/signature": "MEUCIDF7Q/9GP7PxzcWL0C5V0ocu4LHRhBBAWYKVitwMfhyBAiEA2yKFfyva7aSuq5zuAvoDOrsF0PNjtZzwoJVm4Wn2Usg="
}
}
]
}
Cosign uses SimpleSigning so it will take some of the information above and create a json document that looks something like this:
{
"critical": {
"identity": {
"docker-reference": ""
},
"image": {
"Docker-manifest-digest": "sha256-96d13e1500053d6f21aee389b74c5826b3192cda9dd226a6026cef0474a351da"
},
"type": "cosign container signature"
},
"optional": {
}
}
I think that this will be canonicalized and then signed, and then base64 encoded
and added to the annotations object with the key
dev.cosignproject.cosign/signature
.
To verify an image, the image itself needs to be fetched, and also the signature
image (the one with the .sig suffic). The value of the Docker-manifest-digest
needs to match the signature of image that we fetched.
Even though cosign "has container in its name" it can be used to store other
types of files. We can store any blob we like using cosign upload blob
:
First create a file that we want to upload:
$ echo "bajja" > artifact
Next, we can hash this file using:
$ shasum -a 258 artifact
311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95 artifact
Create a unique name for the file to be uploaded:
$ uuidgen | head -c 8
c408de42
We can take that and c408de42 and use it in a name to make our artifact name
unique, for example danbev/c408de42
.
Now, upload the blob using cosign to ttl.sh:
$ cosign upload blob -f artifact ttl.sh/danbev/c408de42:2h
Uploading file from [artifact] to [ttl.sh/danbev/c408de42:2h] with media type [text/plain]
File [artifact] is available directly at
[ttl.sh/v2/danbev/c408de42/blobs/sha256:311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95]
Uploaded image to:
ttl.sh/danbev/c408de42@sha256:7502069007aa8fb8319c139f29fbdd511f288607ce1dd183bda06fcca783536
So the artifact, in this case just a file is available using the url displayed above:
$ curl -L ttl.sh/v2/test-blob-upload-c408de42/blobs/sha256:311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95 > fetched-artifact
Notice that the url contains the sha256 digest:
sha256:311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95
$ shasum -a 256 fetched-artifact
311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95 fetched-artifact
We can now sign the blob using:
$ cosign sign --key cosign.key ttl.sh/danbev/c408de42:2h
Enter password for private key:
WARNING: Image reference ttl.sh/danbev/c408de42:2h uses a tag, not a digest, to identify the image to sign.
This can lead you to sign a different image than the intended one. Please use a
digest (example.com/ubuntu@sha256:abc123...) rather than tag
(example.com/ubuntu:latest) for the input to cosign. The ability to refer to
images by tag will be removed in a future release.
Pushing signature to: ttl.sh/danbev/c408de42
And to verify the blob:
$ cosign verify --key cosign.pub ttl.sh/danbev/c408de42:2h | jq
Verification for ttl.sh/danbev/c408de42:2h --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[
{
"critical": {
"identity": {
"docker-reference": "ttl.sh/danbev/c408de42"
},
"image": {
"docker-manifest-digest": "sha256:7502069007aa8fb8319c139f29fbdd511f288607ce1dd183bda06fcca783536f"
},
"type": "cosign container image signature"
},
"optional": null
}
]
We can save the signature using cosign save
:
$ cosign save --dir out/ ttl.sh/danbev/c408de42:2h
And we can inspect it:
$ ls out/
blobs index.json oci-layout
$ cat out/index.json
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 366,
"digest": "sha256:7502069007aa8fb8319c139f29fbdd511f288607ce1dd183bda06fcca783536f",
"annotations": {
"kind": "dev.cosignproject.cosign/image"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 558,
"digest": "sha256:3858bff64c98a08de76a0b9dcf90e66517c46b939c84680ed48d8090baccea52",
"annotations": {
"kind": "dev.cosignproject.cosign/sigs"
}
}
]
}
The image containing the signature can be found using cosign triangulate
:
$ cosign triangulate ttl.sh/danbev/c408de42:2h
ttl.sh/danbev/c408de42:sha256-7502069007aa8fb8319c139f29fbdd511f288607ce1dd183bda06fcca783536f.sig
Use -d
to get debugging output.
We can also use skopeo
to inspect the image that contains the signature:
$ skopeo inspect --raw docker://$(cosign triangulate ttl.sh/danbev/c408de42:2h) | jq
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 248,
"digest": "sha256:287baa3058e11295538c4a97d8b0b9565955ea3ad721625c8412fb74d86d972e"
},
"layers": [
{
"mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
"size": 238,
"digest": "sha256:640e155f4ff1294c658cedf2acf4e49154ac976efa8fb298646dbddd47a6893f",
"annotations": {
"dev.cosignproject.cosign/signature": "MEUCICgRI1p03iFNnN5YwnA8I7yXdvsTK1ol5x8fc+D2xYcBAiEAya2rjeCbyR5VRJybGRz/fn92GdKGJIDtoAiyAfhqRYQ="
}
}
]
}
By default cosign will communicate with the remote registry to get the resources required for the verification process resources. As shown above we can get the resources and provided them instead, so there does not need to be any interaction with the remote registry.
First, we get the signature using:
$ skopeo inspect --raw docker://$(cosign triangulate ttl.sh/danbev/c408de42:2h) | jq -r '.layers[0].annotations["dev.cosignproject.cosign/signature"]' > danbev-blob.sig
Now, we can use this signature with cosign verify
:
$ cosign verify --signature=danbev-blob.sig --key cosign.pub ttl.sh/danbev/c408de42:2h | jq
We can instruct cosign to not upload the signature, and that it be stored in a file instead, and also that the certificate be stored in a file.
$ cosign sign --upload=false --output-signature=danbev-blob.sig --output-certificate=danbev-blob.crt --key cosign.key ttl.sh/danbev/c408de42:2h
And we can use those files to verify:
$ cosign verify --signature=danbev-blob.sig --key danbev-blob.crt ttl.sh/danbev/c408de42:2h | jq
These are when shortlived keys are used.
$ COSIGN_EXPERIMENTAL=1 cosign sign $IMAGE_DIGEST
To verify the identity of a system we need to ask that system to present us with credentials. And we then need to verify those credentials. To do this we need to have something at the beginning of this flow that we trust can verify these credentials. In WebPKI there are Root Certificate Authorities (CA) which are built into browsers and OS's. A browser will check that a website has the identity it claims by checking that it can be chained back to one of these Root CA's. In sigstores case the root trust allows users and systems to automatically retreive digital certificates that prove who they are, and they use these certs to sign artifacts that they produce and distribute. The users of these artifacts can verify the signatures and certificates against the trust root.
This is used for authentication.
First we have to install https://github.com/sigstore/sigstore-js and npm link it:
$ git clone https://github.com/sigstore/sigstore-js
$ npm i && npm r build
$ npm link
Next we have to generate the artifact to be signed:
$ cd sigstore/npm-example
$ npm link sigstore
$ npm pack
This will generate a file named something like npm-example-1.0.0.tgz
.
We can now sign this artifact using sigstore
:
$ which sigstore
~/.nvm/versions/node/v18.4.0/bin/sigstore
$ sigstore sign ./npm-example-1.0.0.tgz > signature
Your browser will now be opened to: https://oauth2.sigstore.dev/auth/auth?response_type=code&client_id=sigstore&client_secret=&scope=openid+email&redirect_uri=http%3A%2F%2Flocalhost%3A45341&code_challenge=uL8NRr4KXZqeF3R5oi6YxBB17frl7WwjE6fengMrLuA&code_challenge_method=S256&state=lhufn6GM15TjGVmaxlIc2w&nonce=DOCLvVWN2UHTm6izNFaoLA
Created entry at index 4874058, available at
https://rekor.sigstore.dev/api/v1/log/entries?logIndex=4874058
The above command will open a browser and ask for the Sigstore OAuth page. I used GitHub as the identity provider to be used.
We can now inspect the generated signature
file that was the output of the
above command:
$ cat signature | jq
{
"attestationType": "attestation/blob",
"attestation": {
"payloadHash": "4f2fb5c9d049ebad8d3d25f55ffdf49b290f7f84f56b89fe3d6663b48b856ad1",
"payloadHashAlgorithm": "sha256",
"signature": "MEYCIQDPnJgOdxFnx4Sb6XZRBnnThGfTGGFBgA6ZnQ/YE15GygIhAI9mlq+gAvSfSv/ilA0l90ogR2HSxntOQrd8FIsKMrS5"
},
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwekNDQWk2Z0F3SUJBZ0lVYlltS0wranVScVRjV2Y5bjRCdnErMUZnM3kwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09ESTVNRGt3TURNeFdoY05Nakl3T0RJNU1Ea3hNRE14V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVNWWtwT0NibFRIUURxT2tqeDNwRFo1QnRIZkZPRlFCeittcnkKeGpseDRyb2xpTU0vVGd6ZlVoUkhrSE9BUTJDL2NHZzRwSGRVeWRib0lnMGZzK2RSMTZPQ0FVMHdnZ0ZKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVU5bXlHCmZOWjErRzNJcysvcnJYaDVMOENabk5zd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHCkFRUUIxbmtDQkFJRWZBUjZBSGdBZGdBSVlKTHdLRkwvYUVYUjBXc25oSnhGWnhpc0ZqM0RPTkp0NXJ3aUJqWnYKY2dBQUFZTG8xTHNmQUFBRUF3QkhNRVVDSVFENktGd1dOcE0yNS8ySDFCVExsZnhXU3owVDBFS0g5cGZFRVFkTQpQdEQ2OVFJZ1hLZnR0OXhQNjdBaUJKOHhlY3kwOFpHZUlwdzJubkl1M083K1B0WDZObUV3Q2dZSUtvWkl6ajBFCkF3TURad0F3WkFJd0VFM3NWVFhXNFBWeWZtMGx6dlZnVG1pclNMSzJzZmRtdzdrTmhKVVY5YjNTTVJ3N250UDgKVElJNDZSWHBIVHJWQWpCbTZCVGg2MTNHZVBzeXdHNUtDMkwyVjBRZS8wK2p0MkJuQjQ4M3JwTW5XcnVoaVNxYwpBVnR1UU95SlBWSko0anc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNHakNDQWFHZ0F3SUJBZ0lVQUxuVmlWZm5VMGJySmFzbVJrSHJuL1VuZmFRd0NnWUlLb1pJemowRUF3TXcKS2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweQpNakEwTVRNeU1EQTJNVFZhRncwek1URXdNRFV4TXpVMk5UaGFNRGN4RlRBVEJnTlZCQW9UREhOcFozTjBiM0psCkxtUmxkakVlTUJ3R0ExVUVBeE1WYzJsbmMzUnZjbVV0YVc1MFpYSnRaV1JwWVhSbE1IWXdFQVlIS29aSXpqMEMKQVFZRks0RUVBQ0lEWWdBRThSVlMveXNIK05PdnVEWnlQSVp0aWxnVUY5TmxhcllwQWQ5SFAxdkJCSDFVNUNWNwo3TFNTN3MwWmlING5FN0h2N3B0UzZMdnZSL1NUazc5OExWZ016TGxKNEhlSWZGM3RIU2FleExjWXBTQVNyMWtTCjBOL1JnQkp6LzlqV0NpWG5vM3N3ZVRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RXdZRFZSMGxCQXd3Q2dZSUt3WUIKQlFVSEF3TXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFkQmdOVkhRNEVGZ1FVMzlQcHoxWWtFWmI1cU5qcApLRldpeGk0WVpEOHdId1lEVlIwakJCZ3dGb0FVV01BZVg1RkZwV2FwZXN5UW9aTWkwQ3JGeGZvd0NnWUlLb1pJCnpqMEVBd01EWndBd1pBSXdQQ3NRSzREWWlaWURQSWFEaTVIRktuZnhYeDZBU1NWbUVSZnN5bllCaVgyWDZTSlIKblpVODQvOURaZG5GdnZ4bUFqQk90NlFwQmxjNEovMER4dmtUQ3FwY2x2emlMNkJDQ1BuamRsSUIzUHUzQnhzUApteWdVWTdJaTJ6YmRDZGxpaW93PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCOXpDQ0FYeWdBd0lCQWdJVUFMWk5BUEZkeEhQd2plRGxvRHd5WUNoQU8vNHdDZ1lJS29aSXpqMEVBd013CktqRVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVJFd0R3WURWUVFERXdoemFXZHpkRzl5WlRBZUZ3MHkKTVRFd01EY3hNelUyTlRsYUZ3MHpNVEV3TURVeE16VTJOVGhhTUNveEZUQVRCZ05WQkFvVERITnBaM04wYjNKbApMbVJsZGpFUk1BOEdBMVVFQXhNSWMybG5jM1J2Y21Vd2RqQVFCZ2NxaGtqT1BRSUJCZ1VyZ1FRQUlnTmlBQVQ3ClhlRlQ0cmIzUFFHd1M0SWFqdExrMy9PbG5wZ2FuZ2FCY2xZcHNZQnI1aSs0eW5CMDdjZWIzTFAwT0lPWmR4ZXgKWDY5YzVpVnV5SlJRK0h6MDV5aStVRjN1QldBbEhwaVM1c2gwK0gyR0hFN1NYcmsxRUM1bTFUcjE5TDlnZzkyagpZekJoTUE0R0ExVWREd0VCL3dRRUF3SUJCakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlJZCndCNWZrVVdsWnFsNnpKQ2hreUxRS3NYRitqQWZCZ05WSFNNRUdEQVdnQlJZd0I1ZmtVV2xacWw2ekpDaGt5TFEKS3NYRitqQUtCZ2dxaGtqT1BRUURBd05wQURCbUFqRUFqMW5IZVhacCsxM05XQk5hK0VEc0RQOEcxV1dnMXRDTQpXUC9XSFBxcGFWbzBqaHN3ZU5GWmdTczBlRTd3WUk0cUFqRUEyV0I5b3Q5OHNJa29GM3ZaWWRkMy9WdFdCNWI5ClROTWVhN0l4L3N0SjVUZmNMTGVBQkxFNEJOSk9zUTR2bkJISgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t",
"signedEntryTimestamp": "MEUCIQCUmBbbcNVQZheORGqGbdz4PBajob1+j0p8nFbie5mUYAIgaL2tFFnMyGoFecxAnmbW1ZTFRGe1VjuUH4dh/c7fdQE=",
"integratedTime": 1661763632,
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
"logIndex": 3305621
}
We can extract and inspect the certificate file using openssl
:
$ cat signature | jq --raw-output '.certificate' | base64 -d | openssl x509 -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6d:89:8a:2f:e8:ee:46:a4:dc:59:ff:67:e0:1b:ea:fb:51:60:df:2d
Signature Algorithm: ecdsa-with-SHA384
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
Not Before: Aug 29 09:00:31 2022 GMT
Not After : Aug 29 09:10:31 2022 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:31:89:29:38:26:e5:4c:74:03:a8:e9:23:c7:7a:
43:67:90:6d:1d:f1:4e:15:00:73:fa:6a:f2:c6:39:
71:e2:ba:25:88:c3:3f:4e:0c:df:52:14:47:90:73:
80:43:60:bf:70:68:38:a4:77:54:c9:d6:e8:22:0d:
1f:b3:e7:51:d7
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Key Identifier:
F6:6C:86:7C:D6:75:F8:6D:C8:B3:EF:EB:AD:78:79:2F:C0:99:9C:DB
X509v3 Authority Key Identifier:
keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
X509v3 Subject Alternative Name: critical
email:[email protected]
1.3.6.1.4.1.57264.1.1:
https://github.com/login/oauth
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : 08:60:92:F0:28:52:FF:68:45:D1:D1:6B:27:84:9C:45:
67:18:AC:16:3D:C3:38:D2:6D:E6:BC:22:06:36:6F:72
Timestamp : Aug 29 09:00:31.903 2022 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:45:02:21:00:FA:28:5C:16:36:93:36:E7:FD:87:D4:
14:CB:95:FC:56:4B:3D:13:D0:42:87:F6:97:C4:11:07:
4C:3E:D0:FA:F5:02:20:5C:A7:ED:B7:DC:4F:EB:B0:22:
04:9F:31:79:CC:B4:F1:91:9E:22:9C:36:9E:72:2E:DC:
EE:FE:3E:D5:FA:36:61
Signature Algorithm: ecdsa-with-SHA384
30:64:02:30:10:4d:ec:55:35:d6:e0:f5:72:7e:6d:25:ce:f5:
60:4e:68:ab:48:b2:b6:b1:f7:66:c3:b9:0d:84:95:15:f5:bd:
d2:31:1c:3b:9e:d3:fc:4c:82:38:e9:15:e9:1d:3a:d5:02:30:
66:e8:14:e1:eb:5d:c6:78:fb:32:c0:6e:4a:0b:62:f6:57:44:
1e:ff:4f:a3:b7:60:67:07:8f:37:ae:93:27:5a:bb:a1:89:2a:
9c:01:5b:6e:40:ec:89:3d:52:49:e2:3c
-----BEGIN CERTIFICATE-----
MIICpzCCAi6gAwIBAgIUbYmKL+juRqTcWf9n4Bvq+1Fg3y0wCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIwODI5MDkwMDMxWhcNMjIwODI5MDkxMDMxWjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEMYkpOCblTHQDqOkjx3pDZ5BtHfFOFQBz+mry
xjlx4roliMM/TgzfUhRHkHOAQ2C/cGg4pHdUydboIg0fs+dR16OCAU0wggFJMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU9myG
fNZ1+G3Is+/rrXh5L8CZnNswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJwYDVR0RAQH/BB0wG4EZZGFuaWVsLmJldmVuaXVzQGdtYWlsLmNvbTAsBgor
BgEEAYO/MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYoGCisG
AQQB1nkCBAIEfAR6AHgAdgAIYJLwKFL/aEXR0WsnhJxFZxisFj3DONJt5rwiBjZv
cgAAAYLo1LsfAAAEAwBHMEUCIQD6KFwWNpM25/2H1BTLlfxWSz0T0EKH9pfEEQdM
PtD69QIgXKftt9xP67AiBJ8xecy08ZGeIpw2nnIu3O7+PtX6NmEwCgYIKoZIzj0E
AwMDZwAwZAIwEE3sVTXW4PVyfm0lzvVgTmirSLK2sfdmw7kNhJUV9b3SMRw7ntP8
TII46RXpHTrVAjBm6BTh613GePsywG5KC2L2V0Qe/0+jt2BnB483rpMnWruhiSqc
AVtuQOyJPVJJ4jw=
-----END CERTIFICATE-----
Verify the artifact:
$ sigstore verify npm-example-1.0.0.tgz signature signingcert.pem
Verified OK
Now, in the sigstore-js README.md there is a reference to the Rekor url that looks like it was "supposed" to be in the sign commands output. I was not able to find this and opened sigstore/sigstore-js#68 to address this.
But if we have the url we can get entry inforation from Rekor:
$ curl --silent https://rekor.sigstore.dev/api/v1/log/entries?logIndex=4874058 | jq --raw-output '.[].body' | base64 --decode | jq
work in progress
Currently hawkbit is storing binary firmware along with metadata. We should be able sign the binary using sigstore and storing the signature and certificate in the json metadata. This is then later downloaded/sent to the device during a firmware update. Questions:
-
Can we verify the signatur of these downloaded firmware blobs without having to reach/call out to Rekor?
-
Can we store the sigstore root CA to verify the signature without making and external call.
-
A device would configure the DFU that is should perform verification. It is the firmware application that is responsible for configuring DFU I think and we should be able to hook this in there somehow.
$ curl -q https://fulcio.sigstore.dev/api/v1/rootCert | openssl x509 -text
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1531 100 1531 0 0 3147 0 --:--:-- --:--:-- --:--:-- 3143
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
b9:d5:89:57:e7:53:46:eb:25:ab:26:46:41:eb:9f:f5:27:7d:a4
Signature Algorithm: ecdsa-with-SHA384
Issuer: O = sigstore.dev, CN = sigstore
Validity
Not Before: Apr 13 20:06:15 2022 GMT
Not After : Oct 5 13:56:58 2031 GMT
Subject: O = sigstore.dev, CN = sigstore-intermediate
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (384 bit)
pub:
04:f1:15:52:ff:2b:07:f8:d3:af:b8:36:72:3c:86:
6d:8a:58:14:17:d3:65:6a:b6:29:01:df:47:3f:5b:
c1:04:7d:54:e4:25:7b:ec:b4:92:ee:cd:19:88:7e:
27:13:b1:ef:ee:9b:52:e8:bb:ef:47:f4:93:93:bf:
7c:2d:58:0c:cc:b9:49:e0:77:88:7c:5d:ed:1d:26:
9e:c4:b7:18:a5:20:12:af:59:12:d0:df:d1:80:12:
73:ff:d8:d6:0a:25:e7
ASN1 OID: secp384r1
NIST CURVE: P-384
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Extended Key Usage:
Code Signing
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0
X509v3 Subject Key Identifier:
DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
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
Signature Algorithm: ecdsa-with-SHA384
30:64:02:30:3c:2b:10:2b:80:d8:89:96:03:3c:86:83:8b:91:
c5:2a:77:f1:5f:1e:80:49:25:66:11:17:ec:ca:76:01:89:7d:
97:e9:22:51:9d:95:3c:e3:ff:43:65:d9:c5:be:fc:66:02:30:
4e:b7:a4:29:06:57:38:27:fd:03:c6:f9:13:0a:aa:5c:96:fc:
e2:2f:a0:42:08:f9:e3:76:52:01:dc:fb:b7:07:1b:0f:9b:28:
14:63:b2:22:db:36:dd:09:d9:62:8a:8c
-----BEGIN CERTIFICATE-----
MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7
7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS
0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB
BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp
KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI
zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR
nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP
mygUY7Ii2zbdCdliiow=
-----END CERTIFICATE-----
From https://www.chainguard.dev/unchained/busting-5-sigstore-myths:
Another common use case is that organizations need to run systems in air-gapped
environments with no outside network access. That means it’s not possible to
look up a signature in the transparency log, Rekor, right? Wrong! We use what’s
called “stapled inclusion proofs” by default, meaning you can verify an object
is present in the transparency log without needing to contact the transparency
log! The signer is responsible for gathering this evidence from the log and
presenting it alongside the artifact and signature. We store this in an OCI
image automatically, but you can treat it like a normal file and copy it around
for verification as well.
So I think this means that we should be able to add these "stapled inclusion proofs" to some type of storage which could be an layer/attachment/reference in OCI repository.
So lets first try signing a binary blob. First we generate a keypair:
$ echo something > artifact.txt
$ COSIGN_EXPERIMENTAL=1 cosign sign-blob --bundle=artifact.bundle artifact.txt
And we can verify this using:
$ cosign verify-blob --bundle=artifact.bundle artifact.txt
tlog entry verified offline
Verified OK
The following section will go into more details about the Bundle itself.
Sigstore/Cosign can create a bundle
, which contains all the information
required for stapled inclusion proofs, and this can be saved somewhere.
For example:
$ COSIGN_EXPERIMENTAL=1 cosign sign-blob --bundle=artifact.bundle artifact.txt
The file artifact.bundle
is file in json format that looks like this:
$ cat artifact.bundle | jq
{
"base64Signature": "MEUCIEwujBWM+kBZkNPlVZ4tclosmQUNCSNrhBrGOnf8lZv+AiEAv/VRaBGk1tN6jMvl7M9XbxwyDi86tD+Nc+tvrI4GaOU=",
"cert": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFL1lKVGs0M2RGOUJZWUlKV1BKWDlSYytCSGhQNgpHRVJaNFRqa2tCOWwvdnBIVTZSRTJnU1QxcnpBcEUyN3pCWEVXTWVyZzRGNHdsTXA4WjNxbXdsdDlnPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==",
"rekorBundle": {
"SignedEntryTimestamp": "MEQCIAPZVWW0hqsRsy/oymge/6FSJz5ghL++h7kx3Hx0ERysAiB4ydjcdx888b2M9g2IkoEIY+37l8eUSVTUCYNp5uJoEQ==",
"Payload": {
"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0YmM0NTNiNTNjYjNkOTE0YjQ1ZjRiMjUwMjk0MjM2YWRiYTJjMGUwOWZmNmYwMzc5Mzk0OWU3ZTM5ZmQ0Y2MxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRXd1akJXTStrQlprTlBsVlo0dGNsb3NtUVVOQ1NOcmhCckdPbmY4bFp2K0FpRUF2L1ZSYUJHazF0TjZqTXZsN005WGJ4d3lEaTg2dEQrTmMrdHZySTRHYU9VPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGTDFsS1ZHczBNMlJHT1VKWldVbEtWMUJLV0RsU1l5dENTR2hRTmdwSFJWSmFORlJxYTJ0Q09Xd3ZkbkJJVlRaU1JUSm5VMVF4Y25wQmNFVXlOM3BDV0VWWFRXVnlaelJHTkhkc1RYQTRXak54Ylhkc2REbG5QVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=",
"integratedTime": 1671439882,
"logIndex": 9394536,
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
}
}
}
base64Signature
is the same as the signature in the rekor.Payload.body. This
is something that I missed initially that the bundle64Sigature field and the
signature in the payload content are the same:
$ cat artifact.bundle | jq '.base64Signature'
"MEUCIEwujBWM+kBZkNPlVZ4tclosmQUNCSNrhBrGOnf8lZv+AiEAv/VRaBGk1tN6jMvl7M9XbxwyDi86tD+Nc+tvrI4GaOU="
$ cat artifact.bundle | jq -r '.rekorBundle.Payload.body' | base64 -d - | jq '.spec.signature.content'
"MEUCIEwujBWM+kBZkNPlVZ4tclosmQUNCSNrhBrGOnf8lZv+AiEAv/VRaBGk1tN6jMvl7M9XbxwyDi86tD+Nc+tvrI4GaOU="
SignedEntryTimestamp
is a signature of the logIndex
, body
, and
the integratedTime
time fields created by Rekor.
We can check that the signature in fact has been stored in Rekor using the following command:
$ curl --silent https://rekor.sigstore.dev/api/v1/log/entries?logIndex=4874058 | jq '.[].verification.signedEntryTimestamp'
"MEQCIAD7UUGDjQPvdOP28REv7Lq/ZGQn3j5u4HVdz6IMDBEHAiAlpXP5BD0Hx5CRkcqcfbRJRIjdpschUGf0XcOC6xuuyw=="
The Payload of a Bundle
contains a base64 encoded string:
$ cat artifact.bundle | jq -r '.rekorBundle.Payload.body' | base64 -d - | jq
Which will produce:
{
"apiVersion": "0.0.1",
"kind": "hashedrekord",
"spec": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "4bc453b53cb3d914b45f4b250294236adba2c0e09ff6f03793949e7e39fd4cc1"
}
},
"signature": {
"content": "MEQCIGp1XZP5zaImosrBhDPCdXn3f8xI9FHGLsGVx6UeRPCgAiAt5GrsdQhOKnZcA3EWecvgJSHzCIjWifFBQkD7Hdsymg==",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxRENDQWkrZ0F3SUJBZ0lVVFBXVGZPLzFOUmFTRmRlY2FBUS9wQkRHSnA4d0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJeE1USTFNRGN6TnpFeVdoY05Nakl4TVRJMU1EYzBOekV5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVKUVE0Vy81WFA5bTRZYldSQlF0SEdXd245dVVoYWUzOFVwY0oKcEVNM0RPczR6VzRNSXJNZlc0V1FEMGZ3cDhQVVVSRFh2UTM5NHBvcWdHRW1Ta3J1THFPQ0FVNHdnZ0ZLTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVvM0tuCmpKUVowWGZpZ2JENWIwT1ZOTjB4cVNvd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZc0dDaXNHCkFRUUIxbmtDQkFJRWZRUjdBSGtBZHdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnUKamdBQUFZU3R1Qkh5QUFBRUF3QklNRVlDSVFETTVZU1EvR0w2S0k1UjlPZGNuL3BTaytxVkQ2YnNMODMrRXA5UgoyaFdUYXdJaEFLMWppMWxaNTZEc2Z1TGZYN2JCQzluYlIzRWx4YWxCaHYxelFYTVU3dGx3TUFvR0NDcUdTTTQ5CkJBTURBMmNBTUdRQ01CSzh0c2dIZWd1aCtZaGVsM1BpakhRbHlKMVE1SzY0cDB4cURkbzdXNGZ4Zm9BUzl4clAKczJQS1FjZG9EOWJYd2dJd1g2ekxqeWJaa05IUDV4dEJwN3ZLMkZZZVp0ME9XTFJsVWxsY1VETDNULzdKUWZ3YwpHU3E2dlZCTndKMDB3OUhSCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
}
}
}
}
And we can check that this matches the record in Rekor using this command:
$ curl --silent https://rekor.sigstore.dev/api/v1/log/entries?logIndex=4874058 | jq -r '."24296fb24b8ad77addfc1725ba17e24520eb3c7899bb4467ffd22396be7aae0366bd1f1574384262".body' | base64 -d - | jq
{
"apiVersion": "0.0.1",
"kind": "hashedrekord",
"spec": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "4f2fb5c9d049ebad8d3d25f55ffdf49b290f7f84f56b89fe3d6663b48b856ad1"
}
},
"signature": {
"content": "MEUCIQC81kdqAOdcEEuGA8CIk9ToTJhX8P42N9t59t2zgLfQTQIgfhILgdc5bFmBxayTkabRhOyeUY1fkr0x4GqBTVlixx8=",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxVENDQWk2Z0F3SUJBZ0lVQXkvM1haU0xsWmwrUExXZ05DU0VZaXV2Nyswd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJeE1ERXhNRGN5TURBd1doY05Nakl4TURFeE1EY3pNREF3V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVzaEVyNW9LL1hTSkoyaFg4am1ZVE9mYW1oQjM4aDRISEx0R2QKQkE1L3Z1ZUxsVENWTUpnRm1FMXZ3MUJudWljS3R4RjNGaWVjT1JhS3M3TUlmT1RDQ3FPQ0FVMHdnZ0ZKTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVvWjFGClM4Sk1mS3d4M2k4bE9mWW1Uc05GVkJvd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHCkFRUUIxbmtDQkFJRWZBUjZBSGdBZGdBSVlKTHdLRkwvYUVYUjBXc25oSnhGWnhpc0ZqM0RPTkp0NXJ3aUJqWnYKY2dBQUFZUEY2aWdSQUFBRUF3QkhNRVVDSVFDK1FCR2swdkRiMjhZcVgzSElXWTBlSElPYW56eFI3Zk9GQmorYQpKQnFxSUFJZ2ZkV21MZGhqYWJuYnZmWmpmcTBlQy95N29NYmxkRzdYZEdSamlwVGd2YTB3Q2dZSUtvWkl6ajBFCkF3TURhUUF3WmdJeEFPb3lXY1J6cU9KZVZJWmx2Q096M1BTb3ZFVnAzZXZ0UU1iZWxpMFFRc1FxNWt2OUFJZUYKYkxOTmdmazk2U1BwRXdJeEFLcTVuQmFOZmpEQnJIOXpQUTdzK1JadkJoMUR1d1RtV1NNdmZZNkYyZnhZallZOApIdFc4L2c3bFhHUTBKdElLUGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDR2pDQ0FhR2dBd0lCQWdJVUFMblZpVmZuVTBickphc21Sa0hybi9VbmZhUXdDZ1lJS29aSXpqMEVBd013CktqRVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVJFd0R3WURWUVFERXdoemFXZHpkRzl5WlRBZUZ3MHkKTWpBME1UTXlNREEyTVRWYUZ3MHpNVEV3TURVeE16VTJOVGhhTURjeEZUQVRCZ05WQkFvVERITnBaM04wYjNKbApMbVJsZGpFZU1Cd0dBMVVFQXhNVmMybG5jM1J2Y21VdGFXNTBaWEp0WldScFlYUmxNSFl3RUFZSEtvWkl6ajBDCkFRWUZLNEVFQUNJRFlnQUU4UlZTL3lzSCtOT3Z1RFp5UEladGlsZ1VGOU5sYXJZcEFkOUhQMXZCQkgxVTVDVjcKN0xTUzdzMFppSDRuRTdIdjdwdFM2THZ2Ui9TVGs3OThMVmdNekxsSjRIZUlmRjN0SFNhZXhMY1lwU0FTcjFrUwowTi9SZ0JKei85aldDaVhubzNzd2VUQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd013RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVTM5UHB6MVlrRVpiNXFOanAKS0ZXaXhpNFlaRDh3SHdZRFZSMGpCQmd3Rm9BVVdNQWVYNUZGcFdhcGVzeVFvWk1pMENyRnhmb3dDZ1lJS29aSQp6ajBFQXdNRFp3QXdaQUl3UENzUUs0RFlpWllEUElhRGk1SEZLbmZ4WHg2QVNTVm1FUmZzeW5ZQmlYMlg2U0pSCm5aVTg0LzlEWmRuRnZ2eG1BakJPdDZRcEJsYzRKLzBEeHZrVENxcGNsdnppTDZCQ0NQbmpkbElCM1B1M0J4c1AKbXlnVVk3SWkyemJkQ2RsaWlvdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQjl6Q0NBWHlnQXdJQkFnSVVBTFpOQVBGZHhIUHdqZURsb0R3eVlDaEFPLzR3Q2dZSUtvWkl6ajBFQXdNdwpLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQWVGdzB5Ck1URXdNRGN4TXpVMk5UbGFGdzB6TVRFd01EVXhNelUyTlRoYU1Db3hGVEFUQmdOVkJBb1RESE5wWjNOMGIzSmwKTG1SbGRqRVJNQThHQTFVRUF4TUljMmxuYzNSdmNtVXdkakFRQmdjcWhrak9QUUlCQmdVcmdRUUFJZ05pQUFUNwpYZUZUNHJiM1BRR3dTNElhanRMazMvT2xucGdhbmdhQmNsWXBzWUJyNWkrNHluQjA3Y2ViM0xQME9JT1pkeGV4Clg2OWM1aVZ1eUpSUStIejA1eWkrVUYzdUJXQWxIcGlTNXNoMCtIMkdIRTdTWHJrMUVDNW0xVHIxOUw5Z2c5MmoKWXpCaE1BNEdBMVVkRHdFQi93UUVBd0lCQmpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSWQp3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYrakFmQmdOVkhTTUVHREFXZ0JSWXdCNWZrVVdsWnFsNnpKQ2hreUxRCktzWEYrakFLQmdncWhrak9QUVFEQXdOcEFEQm1BakVBajFuSGVYWnArMTNOV0JOYStFRHNEUDhHMVdXZzF0Q00KV1AvV0hQcXBhVm8wamhzd2VORlpnU3MwZUU3d1lJNHFBakVBMldCOW90OThzSWtvRjN2WllkZDMvVnRXQjViOQpUTk1lYTdJeC9zdEo1VGZjTExlQUJMRTRCTkpPc1E0dm5CSEoKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
}
}
}
}
Notice the signature
elemnent above, which contains a content. Could this
be the signature of the content?
Lets take a look at the spec.signature.publicKey.content
to see what it is:
$ cat artifact.bundle | jq -r '.rekorBundle.Payload.body' | base64 -d - | jq -r '.spec.signature.publicKey.content' | base64 -d -
-----BEGIN CERTIFICATE-----
MIICqDCCAi+gAwIBAgIUTPWTfO/1NRaSFdecaAQ/pBDGJp8wCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIxMTI1MDczNzEyWhcNMjIxMTI1MDc0NzEyWjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEJQQ4W/5XP9m4YbWRBQtHGWwn9uUhae38UpcJ
pEM3DOs4zW4MIrMfW4WQD0fwp8PUURDXvQ394poqgGEmSkruLqOCAU4wggFKMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUo3Kn
jJQZ0XfigbD5b0OVNN0xqSowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJwYDVR0RAQH/BB0wG4EZZGFuaWVsLmJldmVuaXVzQGdtYWlsLmNvbTAsBgor
BgEEAYO/MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYsGCisG
AQQB1nkCBAIEfQR7AHkAdwDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynu
jgAAAYStuBHyAAAEAwBIMEYCIQDM5YSQ/GL6KI5R9Odcn/pSk+qVD6bsL83+Ep9R
2hWTawIhAK1ji1lZ56DsfuLfX7bBC9nbR3ElxalBhv1zQXMU7tlwMAoGCCqGSM49
BAMDA2cAMGQCMBK8tsgHeguh+Yhel3PijHQlyJ1Q5K64p0xqDdo7W4fxfoAS9xrP
s2PKQcdoD9bXwgIwX6zLjybZkNHP5xtBp7vK2FYeZt0OWLRlUllcUDL3T/7JQfwc
GSq6vVBNwJ00w9HR
-----END CERTIFICATE-----
So that contains a certificate, but which certificate?
$ cat artifact.bundle | jq -r '.rekorBundle.Payload.body' | base64 -d - | jq -r '.spec.signature.publicKey.content' | base64 -d - | openssl x509 -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
4c:f5:93:7c:ef:f5:35:16:92:15:d7:9c:68:04:3f:a4:10:c6:26:9f
Signature Algorithm: ecdsa-with-SHA384
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
Not Before: Nov 25 07:37:12 2022 GMT
Not After : Nov 25 07:47:12 2022 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:25:04:38:5b:fe:57:3f:d9:b8:61:b5:91:05:0b:
47:19:6c:27:f6:e5:21:69:ed:fc:52:97:09:a4:43:
37:0c:eb:38:cd:6e:0c:22:b3:1f:5b:85:90:0f:47:
f0:a7:c3:d4:51:10:d7:bd:0d:fd:e2:9a:2a:80:61:
26:4a:4a:ee:2e
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Key Identifier:
A3:72:A7:8C:94:19:D1:77:E2:81:B0:F9:6F:43:95:34:DD:31:A9:2A
X509v3 Authority Key Identifier:
keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
X509v3 Subject Alternative Name: critical
email:[email protected]
1.3.6.1.4.1.57264.1.1:
https://github.com/login/oauth
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
Timestamp : Nov 25 07:37:12.434 2022 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:CC:E5:84:90:FC:62:FA:28:8E:51:F4:
E7:5C:9F:FA:52:93:EA:95:0F:A6:EC:2F:CD:FE:12:9F:
51:DA:15:93:6B:02:21:00:AD:63:8B:59:59:E7:A0:EC:
7E:E2:DF:5F:B6:C1:0B:D9:DB:47:71:25:C5:A9:41:86:
FD:73:41:73:14:EE:D9:70
Signature Algorithm: ecdsa-with-SHA384
30:64:02:30:12:bc:b6:c8:07:7a:0b:a1:f9:88:5e:97:73:e2:
8c:74:25:c8:9d:50:e4:ae:b8:a7:4c:6a:0d:da:3b:5b:87:f1:
7e:80:12:f7:1a:cf:b3:63:ca:41:c7:68:0f:d6:d7:c2:02:30:
5f:ac:cb:8f:26:d9:90:d1:cf:e7:1b:41:a7:bb:ca:d8:56:1e:
66:dd:0e:58:b4:65:52:59:5c:50:32:f7:4f:fe:c9:41:fc:1c:
19:2a:ba:bd:50:4d:c0:9d:34:c3:d1:d1
-----BEGIN CERTIFICATE-----
MIICqDCCAi+gAwIBAgIUTPWTfO/1NRaSFdecaAQ/pBDGJp8wCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjIxMTI1MDczNzEyWhcNMjIxMTI1MDc0NzEyWjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEJQQ4W/5XP9m4YbWRBQtHGWwn9uUhae38UpcJ
pEM3DOs4zW4MIrMfW4WQD0fwp8PUURDXvQ394poqgGEmSkruLqOCAU4wggFKMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUo3Kn
jJQZ0XfigbD5b0OVNN0xqSowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJwYDVR0RAQH/BB0wG4EZZGFuaWVsLmJldmVuaXVzQGdtYWlsLmNvbTAsBgor
BgEEAYO/MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYsGCisG
AQQB1nkCBAIEfQR7AHkAdwDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynu
jgAAAYStuBHyAAAEAwBIMEYCIQDM5YSQ/GL6KI5R9Odcn/pSk+qVD6bsL83+Ep9R
2hWTawIhAK1ji1lZ56DsfuLfX7bBC9nbR3ElxalBhv1zQXMU7tlwMAoGCCqGSM49
BAMDA2cAMGQCMBK8tsgHeguh+Yhel3PijHQlyJ1Q5K64p0xqDdo7W4fxfoAS9xrP
s2PKQcdoD9bXwgIwX6zLjybZkNHP5xtBp7vK2FYeZt0OWLRlUllcUDL3T/7JQfwc
GSq6vVBNwJ00w9HR
-----END CERTIFICATE-----
So if I got this right we have a signature that was created using the private key (pseudo code):
let sig = private_key.sign(hash(logIndex + body + integratedTime));
And this signature can be verified using the public key which is in the
certificate (which binds it to my email address in this case).
This certificate need to be checked and for this it is possible to supply a
root certificate to be use in the verification process. This is something that
I missed initially as it does not have to be specified as a command line
option, (--certificate-chain
), and by default will use Fulcio's root
certificates.
We can get the root certificates using:
$ curl -q https://fulcio.sigstore.dev/api/v1/rootCert > downloaded-root.crt
And then specify the --certificate-chain
option when verifying:
$ cosign verify-blob --bundle=artifact.bundle --certificate-chain=downloaded-root.crt artifact.txt
tlog entry verified offline
Verified OK
Let say we sign a blob using cosign:
$ COSIGN_EXPERIMENTAL=1 cosign sign-blob --output-signature=signature \
--output-certificate=artifact.cert artifact.bin
Using payload from: artifact.bin
Generating ephemeral keys...
Retrieving signed certificate...
Note that there may be personally identifiable information associated with this signed artifact.
This may include the email address associated with the account with which you authenticate.
This information will be used for signing this artifact and will be stored in public transparency logs and cannot be removed later.
By typing 'y', you attest that you grant (or have permission to grant) and agree to have this information stored permanently in transparency logs.
Are you sure you want to continue? (y/[N]): y
Your browser will now be opened to:
https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=i2FR4ebTXeg_0gQYrrxCqlqDaWL-0VS0in1LSIQLW4s&code_challenge_method=S256&nonce=2KZn4GWbrY0HsRKlPDXd0zEZUt6&redirect_uri=http%3A%2F%2Flocalhost%3A39771%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=2KZn4IE8XZ2Ew3z8AeRmSaxoFt2
Successfully verified SCT...
using ephemeral certificate:
-----BEGIN CERTIFICATE-----
MIICqTCCAi+gAwIBAgIUQN7wCBUg1AJUyEHM3jqTTCWDjWwwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjMwMTIwMDQ1OTQ0WhcNMjMwMTIwMDUwOTQ0WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAElDC4ZRkQAyzUtBuuVwBa2FayCiOo/LNu293e
DQxiebyabsnaOZdShgs9junBABaojXTYj8aUUWoxgE4CvvQWraOCAU4wggFKMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUIlZI
/wY144jmEryO4UdkmrdimOgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJwYDVR0RAQH/BB0wG4EZZGFuaWVsLmJldmVuaXVzQGdtYWlsLmNvbTAsBgor
BgEEAYO/MAEBBB5odHRwczovL2dpdGh1Yi5jb20vbG9naW4vb2F1dGgwgYsGCisG
AQQB1nkCBAIEfQR7AHkAdwDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynu
jgAAAYXNjAb/AAAEAwBIMEYCIQDwjl5Vo0puV5in7H1rcE1wK24V3fqtbU/yBScf
RdqQXwIhAKjtlZK0SIVFYEddp/YdV69RXheKG3vNZHdFwKIFN4krMAoGCCqGSM49
BAMDA2gAMGUCMAkbyDOt4y5f91TBMzZxKMlLCgpEQ6Nub9Yfj8k8MTv55Pav1/yN
hz+MXiy3yFazuwIxAIMLF4yfXd0p344Y3tdPDcbdnshxxiOABZcWQYAoNl9Tz0gK
7hvkWL3Oy1a3LXromw==
-----END CERTIFICATE-----
tlog entry created with index: 11560124
Signature wrote in the file signature
Certificate wrote in the file artifact.cert
Now, if we take a look at the certificate that was created we see the following:
$ base64 -d artifact.cert | openssl x509 --noout --text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
40:de:f0:08:15:20:d4:02:54:c8:41:cc:de:3a:93:4c:25:83:8d:6c
Signature Algorithm: ecdsa-with-SHA384
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
Not Before: Jan 20 04:59:44 2023 GMT
Not After : Jan 20 05:09:44 2023 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:94:30:b8:65:19:10:03:2c:d4:b4:1b:ae:57:00:
5a:d8:56:b2:0a:23:a8:fc:b3:6e:db:dd:de:0d:0c:
62:79:bc:9a:6e:c9:da:39:97:52:86:0b:3d:8e:e9:
c1:00:16:a8:8d:74:d8:8f:c6:94:51:6a:31:80:4e:
02:be:f4:16:ad
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Key Identifier:
22:56:48:FF:06:35:E3:88:E6:12:BC:8E:E1:47:64:9A:B7:62:98:E8
X509v3 Authority Key Identifier:
keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
X509v3 Subject Alternative Name: critical
email:[email protected]
1.3.6.1.4.1.57264.1.1:
https://github.com/login/oauth
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
Timestamp : Jan 20 04:59:44.255 2023 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:F0:8E:5E:55:A3:4A:6E:57:98:A7:EC:
7D:6B:70:4D:70:2B:6E:15:DD:FA:AD:6D:4F:F2:05:27:
1F:45:DA:90:5F:02:21:00:A8:ED:95:92:B4:48:85:45:
60:47:5D:A7:F6:1D:57:AF:51:5E:17:8A:1B:7B:CD:64:
77:45:C0:A2:05:37:89:2B
Signature Algorithm: ecdsa-with-SHA384
30:65:02:30:09:1b:c8:33:ad:e3:2e:5f:f7:54:c1:33:36:71:
28:c9:4b:0a:0a:44:43:a3:6e:6f:d6:1f:8f:c9:3c:31:3b:f9:
e4:f6:af:d7:fc:8d:87:3f:8c:5e:2c:b7:c8:56:b3:bb:02:31:
00:83:0b:17:8c:9f:5d:dd:29:df:8e:18:de:d7:4f:0d:c6:dd:
9e:c8:71:c6:23:80:05:97:16:41:80:28:36:5f:53:cf:48:0a:
ee:1b:e4:58:bd:ce:cb:56:b7:2d:7a:e8:9b
Notice the Validity
of this certificate issued by Fulico (CA) which is only
10 mins:
Validity
Not Before: Jan 20 04:59:44 2023 GMT
Not After : Jan 20 05:09:44 2023 GMT
But we are still able to verify artifacts after this period which is done with the help of the log entry in Rekor, which we will show in the following command. First we print the current date to show that this is being executed after the certificate validity period:
$ date
Fri Jan 20 08:24:00 AM CET 20231
The we run cosign verify-blob
:
$ COSIGN_EXPERIMENTAL=1 cosign verify-blob --cert artifact.cert --signature signature artifact.bin
tlog entry verified with uuid: b61919f59757b7717b50230f4340e67cf4c8bcd1c9e39204dd860ff4341d883d index: 11560124
Verified OK
And like we mentioned it is still possible to verify after the certificate validity period has passed.
We can inspect check the integratedTime
in the Rekor log entry using:
$ curl -s https://rekor.sigstore.dev/api/v1/log/entries?logIndex=11560124 | jq -r '.[].integratedTime'
1674190784
And the parse that timestamp into a date using:
$ date -ud @1674190784
Fri Jan 20 04:59:44 AM UTC 2023
The integratedTime
field is one of the fields that is included when Rekor
creates the signedEntryTimestamp
signature.
During validation, cosign will check that this timestamp was during the period of certificate validity.
This works because Sigstore separates the lifetime of the artifact from the lifetime of the certificate. This is where Rekor comes into the picture.
We can look at the Rekor log entry using curl:
$ curl -s https://rekor.sigstore.dev/api/v1/log/entries?logIndex=11560124 | jq -r '.[]'
{
"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzMTEzNzU5MDhiMmExMDY4OGZiODg0MWI2MWQ4YjFkYWE5YTNlOTA0Zjg0ZDJlZDg4ZDBhMzVjYjRmMGUxYTk1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRmxaZGU4MzFCb21oQ2RzTFBZUW85dCtwZWFaNExPSEdUK0luaFdoVnZqVEFpRUExMmpkMEdkcEJwNE9LWEEraU1ucGg0T2JJYkxCczMvZ3E1enhTNU54WE9FPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnhWRU5EUVdrclowRjNTVUpCWjBsVlVVNDNkME5DVldjeFFVcFZlVVZJVFROcWNWUlVRMWRFYWxkM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDFVU1hkTlJGRXhUMVJSTUZkb1kwNU5hazEzVFZSSmQwMUVWWGRQVkZFd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZzUkVNMFdsSnJVVUY1ZWxWMFFuVjFWbmRDWVRKR1lYbERhVTl2TDB4T2RUSTVNMlVLUkZGNGFXVmllV0ZpYzI1aFQxcGtVMmhuY3pscWRXNUNRVUpoYjJwWVZGbHFPR0ZWVlZkdmVHZEZORU4yZGxGWGNtRlBRMEZWTkhkblowWkxUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZKYkZwSkNpOTNXVEUwTkdwdFJYSjVUelJWWkd0dGNtUnBiVTluZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwM1dVUldVakJTUVZGSUwwSkNNSGRITkVWYVdrZEdkV0ZYVm5OTWJVcHNaRzFXZFdGWVZucFJSMlIwV1Zkc2MweHRUblppVkVGelFtZHZjZ3BDWjBWRlFWbFBMMDFCUlVKQ1FqVnZaRWhTZDJONmIzWk1NbVJ3WkVkb01WbHBOV3BpTWpCMllrYzVibUZYTkhaaU1rWXhaRWRuZDJkWmMwZERhWE5IQ2tGUlVVSXhibXREUWtGSlJXWlJVamRCU0d0QlpIZEVaRkJVUW5GNGMyTlNUVzFOV2tob2VWcGFlbU5EYjJ0d1pYVk9ORGh5Wml0SWFXNUxRVXg1Ym5VS2FtZEJRVUZaV0U1cVFXSXZRVUZCUlVGM1FrbE5SVmxEU1ZGRWQycHNOVlp2TUhCMVZqVnBiamRJTVhKalJURjNTekkwVmpObWNYUmlWUzk1UWxOalpncFNaSEZSV0hkSmFFRkxhblJzV2tzd1UwbFdSbGxGWkdSd0wxbGtWalk1VWxob1pVdEhNM1pPV2toa1JuZExTVVpPTkd0eVRVRnZSME5EY1VkVFRUUTVDa0pCVFVSQk1tZEJUVWRWUTAxQmEySjVSRTkwTkhrMVpqa3hWRUpOZWxwNFMwMXNURU5uY0VWUk5rNTFZamxaWm1vNGF6aE5WSFkxTlZCaGRqRXZlVTRLYUhvclRWaHBlVE41Um1GNmRYZEplRUZKVFV4R05IbG1XR1F3Y0RNME5Ga3pkR1JRUkdOaVpHNXphSGg0YVU5QlFscGpWMUZaUVc5T2JEbFVlakJuU3dvM2FIWnJWMHd6VDNreFlUTk1XSEp2YlhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ==",
"integratedTime": 1674190784,
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
"logIndex": 11560124,
"verification": {
"inclusionProof": {
"checkpoint": "rekor.sigstore.dev - 2605736670972794746\n7398089\nd1DQUHLBKbT7Sl4m1f5RZ7homaI52tiF60buxAyhJCM=\nTimestamp: 1674192874293544902\n\n— rekor.sigstore.dev wNI9ajBFAiAeYBe4Xt4xyhtFxBozJXSnYebAM9xGzC0RpFsqnjSA7QIhAPstqAuhDibJrr3svyiYaULONn0ZSjV5TcyEA2gxQx79\n",
"hashes": [
"a45805f461d9ae59a337a228f84e68b5e72415f8f7058434da97078ccc3bc0dd",
"4794f0f809cef2fb1b472af55c80ef9104b4df919a3e082ea9efa95e8e1e5bda",
"c2da2e7f2ffcdddc349e41d3ae62747f126d44b7b4579810b508869e67407cc9",
"1398f13b70e90842a7a9e9cc866377972cec0f6ff70f7f6db9ba4abb2c1a7c5e",
"a3803a6dc31a2cc8bbb291f5e96bffcf4260b15523872bc2256bf12bbf0a5fa7",
"d93ce7ae9c328464e133c8db5f345c30e84d274e39b412b20e347a12dc5e218d",
"317763148e311a0b9721080d97e482427bb363fbe1875e5af38ef1d560122153",
"e69c00b93c48e79d81dd482b0537a18c234b14866a3209812be140b42ed4d760",
"025762f9da46287f9c0cf4610f36c0c078afba1b1d0d03004b6e91fcae635dc1",
"1ace4460a740618f909188b96d886b449424ff2b1b36229809504fb77043e498",
"eab77380e22f6d8396bf06b59987a3ace90b37698930f8fc59cedd3cbe202e1e",
"b7873706fe7c9af04715c7e4f0f4fe32654c9cef53c256a5e5d0c62f68d9880a",
"5218ea43f5bb78336c0fe0bd461d3d9dd3a8fad205aeaf4598a46ea3bf1093ed",
"ab94c094711e570e0bfe172882d8f7d8f318f3a28e8f238bc513737e61d7a9c2",
"866c91027797653add7fe591cc361e089976d0f8c0e4f1b56827055c4e714228",
"da55a1381116c4092bc6abc560de2da344a1f243f4dbe58382f99b06505c9bf6",
"4b6df664d9552bc24d48a4c7d5659a8270065e1fedbc39103b010ab235a87850",
"616429db6c7d20c5b0eff1a6e512ea57a0734b94ae0bc7c914679463e01a7fba",
"5a4ad1534b1e770f02bfde0de15008a6971cf1ffbfa963fc9c2a644973a8d2d1"
],
"logIndex": 7396693,
"rootHash": "7750d05072c129b4fb4a5e26d5fe5167b86899a239dad885eb46eec40ca12423",
"treeSize": 7398089
},
"signedEntryTimestamp": "MEYCIQCeg0q60gzijtU3V+FdpemQyX6kozLs9r5fq1pMs91MGgIhAJHRSQowwcrWQ/Myj5QqhjH/uHH6Sn+yWQatMsREGki2"
}
}
The signedEntryTimestamp
is a signature created by Rekor and includes the
body
field, the logIndex
and the integratedTime
. The integratedTime is
the time that this record was integrated into the Rekor log.
We can inspect the body and see the contents using:
$ curl -s https://rekor.sigstore.dev/api/v1/log/entries?logIndex=11560124 | jq -r '.[].body' | base64 -d | jq
{
"apiVersion": "0.0.1",
"kind": "hashedrekord",
"spec": {
"data": {
"hash": {
"algorithm": "sha256",
"value": "311375908b2a10688fb8841b61d8b1daa9a3e904f84d2ed88d0a35cb4f0e1a95"
}
},
"signature": {
"content": "MEUCIFlZde831BomhCdsLPYQo9t+peaZ4LOHGT+InhWhVvjTAiEA12jd0GdpBp4OKXA+iMnph4ObIbLBs3/gq5zxS5NxXOE=",
"publicKey": {
"content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNxVENDQWkrZ0F3SUJBZ0lVUU43d0NCVWcxQUpVeUVITTNqcVRUQ1dEald3d0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd01USXdNRFExT1RRMFdoY05Nak13TVRJd01EVXdPVFEwV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVsREM0WlJrUUF5elV0QnV1VndCYTJGYXlDaU9vL0xOdTI5M2UKRFF4aWVieWFic25hT1pkU2hnczlqdW5CQUJhb2pYVFlqOGFVVVdveGdFNEN2dlFXcmFPQ0FVNHdnZ0ZLTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVJbFpJCi93WTE0NGptRXJ5TzRVZGttcmRpbU9nd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0p3WURWUjBSQVFIL0JCMHdHNEVaWkdGdWFXVnNMbUpsZG1WdWFYVnpRR2R0WVdsc0xtTnZiVEFzQmdvcgpCZ0VFQVlPL01BRUJCQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZc0dDaXNHCkFRUUIxbmtDQkFJRWZRUjdBSGtBZHdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnUKamdBQUFZWE5qQWIvQUFBRUF3QklNRVlDSVFEd2psNVZvMHB1VjVpbjdIMXJjRTF3SzI0VjNmcXRiVS95QlNjZgpSZHFRWHdJaEFLanRsWkswU0lWRllFZGRwL1lkVjY5UlhoZUtHM3ZOWkhkRndLSUZONGtyTUFvR0NDcUdTTTQ5CkJBTURBMmdBTUdVQ01Ba2J5RE90NHk1ZjkxVEJNelp4S01sTENncEVRNk51YjlZZmo4azhNVHY1NVBhdjEveU4KaHorTVhpeTN5RmF6dXdJeEFJTUxGNHlmWGQwcDM0NFkzdGRQRGNiZG5zaHh4aU9BQlpjV1FZQW9ObDlUejBnSwo3aHZrV0wzT3kxYTNMWHJvbXc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
}
}
}
}
The Rekor Log Index for this example can be found here.
And the example files created above can be found in sigstore/elf.
Currently, the code that expects a JWT token will use the TokenProvider::Static enum variant:
let id_token: CoreIdToken = CoreIdToken::from_str(&token).unwrap();
TokenProvider::Static((id_token, "keygen".to_string()))
Notice the hardcoded keygen
. This was something, if I recall correctly, that
I just used the name of the program that I was working on at the time. This was
very much trying this out and I forgot about it.
I'm trying to figure out what the correct value of this field should be. In sigstore-rs the code that uses this string looks like this:
let (token, challenge) = self.token_provider.get_token().await?;
let signer = signing_scheme.create_signer()?;
let signature = signer.sign(challenge.as_bytes())?;
let signature = BASE64_STD_ENGINE.encode(signature);
let key_pair = signer.to_sigstore_keypair()?;
let public_key = key_pair.public_key_to_der()?;
let public_key = BASE64_STD_ENGINE.encode(public_key);
let csr = Csr {
public_key: Some(PublicKey(public_key, signing_scheme)),
signed_email_address: Some(signature),
};
let csr: Body = csr.try_into()?;
let client = reqwest::Client::new();
let response = client
.post(self.root_url.join(SIGNING_CERT_PATH)?)
.header(CONTENT_TYPE_HEADER_NAME, "application/json")
.bearer_auth(token.to_string())
.body(csr)
.send()
.await
.map_err(|_| SigstoreError::SigstoreFulcioCertificatesNotProvidedError)?;
So what should be specified for the challenge
value?
To answer this we need to find out which issuers are available for Fulico, which
can be done using the following command:
$ curl -Ls https://fulcio.sigstore.dev/api/v2/configuration | jq
{
"issuers": [
{
"issuerUrl": "https://accounts.google.com",
"audience": "sigstore",
"challengeClaim": "email",
"spiffeTrustDomain": ""
},
{
"issuerUrl": "https://allow.pub",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": "allow.pub"
},
{
"issuerUrl": "https://oauth2.sigstore.dev/auth",
"audience": "sigstore",
"challengeClaim": "email",
"spiffeTrustDomain": ""
},
{
"issuerUrl": "https://token.actions.githubusercontent.com",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
},
{
"wildcardIssuerUrl": "https://*.oic.prod-aks.azure.com/*",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
},
{
"wildcardIssuerUrl": "https://container.googleapis.com/v1/projects/*/locations/*/clusters/*",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
},
{
"wildcardIssuerUrl": "https://oidc.eks.*.amazonaws.com/id/*",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
},
{
"wildcardIssuerUrl": "https://oidc.prod-aks.azure.com/*",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
}
]
}
In our case the issuer is https://token.actions.githubusercontent.com
:
$ curl -Ls https://fulcio.sigstore.dev/api/v2/configuration | jq '.[][] | select(.issuerUrl=="https://token.actions.githubusercontent.com")'
{
"issuerUrl": "https://token.actions.githubusercontent.com",
"audience": "sigstore",
"challengeClaim": "sub",
"spiffeTrustDomain": ""
}
So we can see that we need to set our aud
field to sigstore
. And notice that
the challengeClaim
is specified as sub
, to we should be using the subject
of the id_token for the challenge value.
Also notice the audience
which needs to be specified when we request the
action, which is added as a request query parameter &audience=sigstore
.
We can set the audience in our workflow when we make the request to get an OICD id token from github's authorization server:
- name: Generate OIDC Token
id: token
run: |
echo oidc_token=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \
-H "User-Agent: actions/oidc-client" \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
| jq '.value' | tr '"' ' ') >> $GITHUB_OUTPUT
Now, we also need to extract the value of sub
from the OIDC id_token returned
from the request above. To do this in a safe manner, we will need to have access
to the JSON Web Key Set (JWKS). We can acccess these keys using the following
command which gets github's OIDC server configuration:
$ curl -Ls https://token.actions.githubusercontent.com/auth/.well-known/openid-configuration | jq
{
"issuer": "https://token.actions.githubusercontent.com/auth",
"jwks_uri": "https://token.actions.githubusercontent.com/.well-known/jwks",
"subject_types_supported": [
"public",
"pairwise"
],
"response_types_supported": [
"id_token"
],
"claims_supported": [
"sub",
"aud",
"exp",
"iat",
"iss",
"jti",
"nbf",
"ref",
"repository",
"repository_id",
"repository_owner",
"repository_owner_id",
"run_id",
"run_number",
"run_attempt",
"actor",
"actor_id",
"workflow",
"workflow_ref",
"workflow_sha",
"head_ref",
"base_ref",
"event_name",
"ref_type",
"environment",
"environment_node_id",
"job_workflow_ref",
"job_workflow_sha",
"repository_visibility"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}
And we can get/inspect the keys using jwks_uri
:
$ curl -Ls https://token.actions.githubusercontent.com/.well-known/jwks | jq
With those changes in place we should be able to get the CI workflow working and see the certificates printed out and not the error message.
One thing I also had to do was to compile/build this project before running
the workflow.sh script. The reason for this is that the script uses cargo and
will compile the project. This takes some time and I believe enough time for
the OIDC id_token to expire. The strange thing is that there was no error
message from the server, well it does return a 401
in this case but that is
not checked for, instead the if we print out the cert field it will be:
{"code":16,"message":"There was an error processing the credentials for this request","details":[]}
I think this could be improved and the 401
handled. Also it would be nice to
see if the error message from the server could be improved in this situation.
The following will print the html page with seletion of providers to choose from.
$ curl -s `curl -s https://oauth2.sigstore.dev/auth/.well-known/openid-configuration | jq -r '.authorization_endpoint'` | grep https | sed 's/\%252F/\//g' | sed 's/<a href="\/auth\/auth\///' | sed 's/\" target=\"_self">$//g'
https://github.com/login/oauth
https://accounts.google.com
https://login.microsoftonline.com
This section will show an example of attaching an attestation to a container image in a an OCI registry.
$ cd sigstore/container-image
$ make container-image
$ make push
$ make keys
$ make sign
$ make attest
Using payload from: some-predicate
tlog entry created with index: 17176049
After those commands have been run we can use rekor-cli
to inspect the
entry:
$ rekor-cli get --log-index 17176049 --format json | jq
{
"Attestation": "{\"_type\":\"https://in-toto.io/Statement/v0.1\",\"predicateType\":\"https://cosign.sigstore.dev/attestation/v1\",\"subject\":[{\"name\":\"ttl.sh/danbev-simple-container\",\"digest\":{\"sha256\":\"e5ca0e505f1cced20b16711015e9145a98b9400b9b2db28ec274b3866de52aae\"}}],\"predicate\":{\"Data\":\"bajja\\n\",\"Timestamp\":\"2023-04-05T13:29:58Z\"}}",
"AttestationType": "",
"Body": {
"IntotoObj": {
"content": {
"hash": {
"algorithm": "sha256",
"value": "de21be2442339a3fa55c9ef773f199db5f67a09a7defef5382a9af3b72e346ef"
},
"payloadHash": {
"algorithm": "sha256",
"value": "3359b1d531e9eac3de95051a0caefb781d9511741134236b4ebf6aadd844d735"
}
},
"publicKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFaDZ0TTNhQUR4T2FLaXlmejJEc2RROEo3ZU1iWApCek05dFI0ckhHWjFtZjZTc2o5MS9GRWhiMDArRi8vbHBQLzBwRmltMHo4cjNZREx5SGRwS1R2bzV3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
}
},
"LogIndex": 17176049,
"IntegratedTime": 1680701401,
"UUID": "24296fb24b8ad77aa0457d4da202c26691f077fdc6775fa4e2184931ce3078fa8b21f6f4f439b2d8",
"LogID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
}
The use case here is that we want to verify an attestation, and the example here will be the sigstore-js npm package (but this will hopefully be the same for all npm packages that use npm publish provenance).
We can start by accessing the attestation url:
$ curl -s https://registry.npmjs.org/sigstore/1.3.0 | jq -r '.dist.attestations.url'
https://registry.npmjs.org/-/npm/v1/attestations/[email protected]
With that url we can find the DSSE envelope using:
$ curl -s https://registry.npmjs.org/-/npm/v1/attestations/[email protected] | jq '.attestations[0].bundle.dsseEnvelope'
{
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInN1YmplY3QiOlt7Im5hbWUiOiJwa2c6bnBtL3NpZ3N0b3JlQDEuMy4wIiwiZGlnZXN0Ijp7InNoYTUxMiI6Ijc2MTc2ZmZhMzM4MDhiNTQ2MDJjN2MzNWRlNWM2ZTlhNGRlYjk2MDY2ZGJhNjUzM2Y1MGFjMjM0ZjRmMWY0YzZiMzUyNzUxNWRjMTdjMDZmYmUyODYwMDMwZjQxMGVlZTY5ZWEyMDA3OWJkM2EyYzZmM2RjZjNiMzI5YjEwNzUxIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MC4yIiwicHJlZGljYXRlIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vY2xpL2doYS92MiIsImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vYWN0aW9ucy9ydW5uZXIifSwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qc0ByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsic2hhMSI6ImRhZThiZDhlYjQzM2E0MTQ3YjQ2NTVjMDBmZTczZTBmMjJiYzBmYjEifSwiZW50cnlQb2ludCI6Ii5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sIn0sInBhcmFtZXRlcnMiOnt9LCJlbnZpcm9ubWVudCI6eyJHSVRIVUJfRVZFTlRfTkFNRSI6InB1c2giLCJHSVRIVUJfUkVGIjoicmVmcy9oZWFkcy9tYWluIiwiR0lUSFVCX1JFUE9TSVRPUlkiOiJzaWdzdG9yZS9zaWdzdG9yZS1qcyIsIkdJVEhVQl9SRVBPU0lUT1JZX0lEIjoiNDk1NTc0NTU1IiwiR0lUSFVCX1JFUE9TSVRPUllfT1dORVJfSUQiOiI3MTA5NjM1MyIsIkdJVEhVQl9SVU5fQVRURU1QVCI6IjEiLCJHSVRIVUJfUlVOX0lEIjoiNDczNTM4NDI2NSIsIkdJVEhVQl9TSEEiOiJkYWU4YmQ4ZWI0MzNhNDE0N2I0NjU1YzAwZmU3M2UwZjIyYmMwZmIxIiwiR0lUSFVCX1dPUktGTE9XX1JFRiI6InNpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbiIsIkdJVEhVQl9XT1JLRkxPV19TSEEiOiJkYWU4YmQ4ZWI0MzNhNDE0N2I0NjU1YzAwZmU3M2UwZjIyYmMwZmIxIn19LCJtZXRhZGF0YSI6eyJidWlsZEludm9jYXRpb25JZCI6IjQ3MzUzODQyNjUtMSIsImNvbXBsZXRlbmVzcyI6eyJwYXJhbWV0ZXJzIjpmYWxzZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qc0ByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsic2hhMSI6ImRhZThiZDhlYjQzM2E0MTQ3YjQ2NTVjMDBmZTczZTBmMjJiYzBmYjEifX1dfX0=",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"sig": "MEQCIAYR4pbfGEzpbBjJc9m8/VeE7qudH9f9MqgtnyiOUxMVAiBSvgyuJpGNN1FpXQB7JbEv0JgqMwgVSuAI2XbDWQAmfA==",
"keyid": ""
}
]
}
With the envelope we can then use it in a seedwing policy and use
intoto::verify-envelope
to verify it.
$ cd sigstore/attestations
We will be using an existing attestation in Rekor for this example.
Lets start by downloading the attestation and saving it to a file:
$ rekor-cli get --uuid 24296fb24b8ad77a2074cd21a9282ea75d7dea9b7f4ed9e1a5b5c4bfbe163ee67878cc411cb25cba --format json| jq -r '.Attestation' | jq > attestation
Next we can get the public_key:
$ rekor-cli get --uuid 24296fb24b8ad77a2074cd21a9282ea75d7dea9b7f4ed9e1a5b5c4bfbe163ee67878cc411cb25cba --format json| jq -r '.Body.IntotoObj.content.envelope.signatures[0].publicKey' | base64 -d | openssl x509 -pubkey -noout > public_key
And then the signature:
$ rekor-cli get --uuid 24296fb24b8ad77a2074cd21a9282ea75d7dea9b7f4ed9e1a5b5c4bfbe163ee67878cc411cb25cba --format json| jq -r '.Body.IntotoObj.content.envelope.signatures[0].sig' > signature
So the attestation
contains a subject which looks like this:
$ cat attestation | jq '.subject[0]'
{
"name": "pkg:npm/[email protected]",
"digest": {
"sha512": "76176ffa33808b54602c7c35de5c6e9a4deb96066dba6533f50ac234f4f1f4c6b3527515dc17c06fbe2860030f410eee69ea20079bd3a2c6f3dcf3b329b10751"
}
}
The digest is a sha512 hash of the .tgz of the npm package:
$ wget `curl -s https://registry.npmjs.org/sigstore/1.3.0 | jq '.dist.tarball' | tr -d '"'`
We can confirm this by calculating the sha512 hash ourselves using:
$ sha512sum sigstore-1.3.0.tgz
76176ffa33808b54602c7c35de5c6e9a4deb96066dba6533f50ac234f4f1f4c6b3527515dc17c06fbe2860030f410eee69ea20079bd3a2c6f3dcf3b329b10751 sigstore-1.3.0.tgz