はじめに
手動でKeyless Signingしてみようという誰にも望まれていないシリーズの4本目です。
- curlでKeyless Signingする (1) - OpenID Connect編
- curlでKeyless Signingする (2) - Fulcio編
- curlでKeyless Signingする (3) - Signed Certificate Timestamp編
- curlでKeyless Signingする (4) - Rekor編(本記事)
- curlでKeyless Signingする (5) - Verify編
- curlでKeyless Signingする (6) - Trillian編
前回までの記事で手元で鍵ペアを作ってFulcionから証明書を発行するところまでを行いました。今回は実際にソフトウェアに署名を行い、その署名と証明書をRekorにアップロードするところまでをやってみます。ここまで終わればKeyless Signingは完了です。
Rekorは Software Supply Chain Transparency Log
という説明から分かるように、ソフトウェアサプライチェーンのためのTransparency Logサーバです。
前回の記事でCertificate Transparency (CT)について説明した際にCTログサーバが登場しましたが、Rekorはそれと近い役割を持っています。ただしRekorは証明書だけではなく署名なども保存しますし、attestationと呼ばれる署名付きメタデータも保存できます。そのため保存されるログはCertificate Transparency LogではなくTransparency Log (tlog)と呼ばれています。
Cosignで試す
まず普通にCosign CLIを使ってRekorにエントリを追加してみます。
$ echo hello > foo.txt $ COSIGN_EXPERIMENTAL=1 cosign sign-blob foo.txt -d -y Using payload from: foo.txt 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. Your browser will now be opened to: https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=H13ag3T3yPmFwkMTSseirGjQ5tfKS2qikJ1AMRKK_v8&code_challenge_method=S256&nonce=2F7Fus8FmtjTIQDcqThsEFJcPc0&redirect_uri=http%3A%2F%2Flocalhost%3A61210%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=2F7Fur6rgHNP72W6fTGrduNN4SF Successfully verified SCT... using ephemeral certificate: -----BEGIN CERTIFICATE----- MIICoTCCAiegAwIBAgIUJxUJH9K0n8Ny0QZPogrFANOE06MwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIwOTIyMDgzMDQ2WhcNMjIwOTIyMDg0MDQ2WjAAMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAElNMrxlUpjhehAjo9v1F4mcrxReu7tNOzkUIW 27K3voNV/RMruAAuXVjc9BVqPPfYrJPMvGEnSztVe7zxrbP+4KOCAUYwggFCMA4G A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUbg1a x9w/9jcbajdkJynERn9YcMEwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y ZD8wIAYDVR0RAQH/BBYwFIESa25xeWYyNjNAZ21haWwuY29tMCwGCisGAQQBg78w AQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIE AgR8BHoAeAB2AAhgkvAoUv9oRdHRayeEnEVnGKwWPcM40m3mvCIGNm9yAAABg2RS HOQAAAQDAEcwRQIgFtbSQlZKSlRtv5eMOvQQ0aMQnht/MnXRCKYTfN9EkdMCIQD6 lsaVDdYpd2Wo1Qe8v0jD0MJFRBSO05DSzVAF3Ep/qTAKBggqhkjOPQQDAwNoADBl AjEArCuDiXb5RbFJGtCELNgRQBgbbWaawhZ08ey1N7Dt8NTjAYyTdi7x2WcV43dX GIQnAjA9AnUNJEkBwABgZsdsZUB/lTc4UZeEFpTrB5s55ECbnG6MKRW3sZcnARzQ cWE0bBk= -----END CERTIFICATE----- tlog entry created with index: 3732405 MEYCIQCoUlGiu9bPGMJcnFkl60s6T68sgRXNFHeHn9ZzrWMQbgIhANh5FutvBdewXjxhDv9L8uTJ/RBXdhJsRtJc7Dbwz3pt
ここまで読んできた人ならメッセージを見れば何をしているか分かると思いますが、使い捨ての鍵ペアを作ってFulcioに証明書を発行してもらっています。そのあとSCTの検証をして、署名と証明書をRekorにアップロードしています。最後に保存されたtlogのIDと署名が返って来ています。
Rekorに保存されたtlogは rekor-cli
コマンドで見ることができます。このCLIのインストール方法は上のRekorのドキュメントに書いてあります。rekor-cli get
に上のtlogのIDを指定します。
$ rekor-cli get --log-index 3732405 LogID: c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d Index: 3732405 IntegratedTime: 2022-09-22T08:30:47Z UUID: 34bd4d8bcd3b38b3ae4938bc8d56f9c38f29ab6592cebd2a7a391a3c98cd95d5 Body: { "HashedRekordObj": { "data": { "hash": { "algorithm": "sha256", "value": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" } }, "signature": { "content": "MEYCIQCoUlGiu9bPGMJcnFkl60s6T68sgRXNFHeHn9ZzrWMQbgIhANh5FutvBdewXjxhDv9L8uTJ/RBXdhJsRtJc7Dbwz3pt", "publicKey": { "content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNvVENDQWllZ0F3SUJBZ0lVSnhVSkg5SzBuOE55MFFaUG9nckZBTk9FMDZNd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09USXlNRGd6TURRMldoY05Nakl3T1RJeU1EZzBNRFEyV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVsTk1yeGxVcGpoZWhBam85djFGNG1jcnhSZXU3dE5PemtVSVcKMjdLM3ZvTlYvUk1ydUFBdVhWamM5QlZxUFBmWXJKUE12R0VuU3p0VmU3enhyYlArNEtPQ0FVWXdnZ0ZDTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVViZzFhCng5dy85amNiYWpka0p5bkVSbjlZY01Fd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lBWURWUjBSQVFIL0JCWXdGSUVTYTI1eGVXWXlOak5BWjIxaGFXd3VZMjl0TUN3R0Npc0dBUVFCZzc4dwpBUUVFSG1oMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5c2IyZHBiaTl2WVhWMGFEQ0JpZ1lLS3dZQkJBSFdlUUlFCkFnUjhCSG9BZUFCMkFBaGdrdkFvVXY5b1JkSFJheWVFbkVWbkdLd1dQY000MG0zbXZDSUdObTl5QUFBQmcyUlMKSE9RQUFBUURBRWN3UlFJZ0Z0YlNRbFpLU2xSdHY1ZU1PdlFRMGFNUW5odC9NblhSQ0tZVGZOOUVrZE1DSVFENgpsc2FWRGRZcGQyV28xUWU4djBqRDBNSkZSQlNPMDVEU3pWQUYzRXAvcVRBS0JnZ3Foa2pPUFFRREF3Tm9BREJsCkFqRUFyQ3VEaVhiNVJiRkpHdENFTE5nUlFCZ2JiV2Fhd2haMDhleTFON0R0OE5UakFZeVRkaTd4MldjVjQzZFgKR0lRbkFqQTlBblVOSkVrQndBQmdac2RzWlVCL2xUYzRVWmVFRnBUckI1czU1RUNibkc2TUtSVzNzWmNuQVJ6UQpjV0UwYkJrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" } } } }
data
にSHA256のハッシュ値が含まれています。これは署名した対象のハッシュ値となっています。
$ sha256sum foo.txt 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 foo.txt
そして signature.content
に署名が、 publicKey.content
に証明書が含まれています。これで署名の検証可能な状態です。一応試しておきます。
まず証明書を抜き出します。Base64エンコードされているのでデコードして cert.pem
に書き出します。
$ rekor-cli get --log-index 3732405 --format json | jq -r .Body.HashedRekordObj.signature.publicKey.content | base64 -d > cert.pem $ openssl x509 -in cert.pem -inform PEM -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 27:15:09:1f:d2:b4:9f:c3:72:d1:06:4f:a2:0a:c5:00:d3:84:d3:a3 Signature Algorithm: ecdsa-with-SHA384 Issuer: O = sigstore.dev, CN = sigstore-intermediate Validity Not Before: Sep 22 08:30:46 2022 GMT Not After : Sep 22 08:40:46 2022 GMT Subject: Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:94:d3:2b:c6:55:29:8e:17:a1:02:3a:3d:bf:51: 78:99:ca:f1:45:eb:bb:b4:d3:b3:91:42:16:db:b2: b7:be:83:55:fd:13:2b:b8:00:2e:5d:58:dc:f4:15: 6a:3c:f7:d8:ac:93:cc:bc:61:27:4b:3b:55:7b:bc: f1:ad:b3:fe:e0 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: 6E:0D:5A:C7:DC:3F:F6:37:1B:6A:37:64:27:29:C4:46:7F:58:70:C1 X509v3 Authority Key Identifier: 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:knqyf263@gmail.com 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 : Sep 22 08:30:46.500 2022 GMT Extensions: none Signature : ecdsa-with-SHA256 30:45:02:20:16:D6:D2:42:56:4A:4A:54:6D:BF:97:8C: 3A:F4:10:D1:A3:10:9E:1B:7F:32:75:D1:08:A6:13:7C: DF:44:91:D3:02:21:00:FA:96:C6:95:0D:D6:29:77:65: A8:D5:07:BC:BF:48:C3:D0:C2:45:44:14:8E:D3:90:D2: CD:50:05:DC:4A:7F:A9 Signature Algorithm: ecdsa-with-SHA384 Signature Value: 30:65:02:31:00:ac:2b:83:89:76:f9:45:b1:49:1a:d0:84:2c: d8:11:40:18:1b:6d:66:9a:c2:16:74:f1:ec:b5:37:b0:ed:f0: d4:e3:01:8c:93:76:2e:f1:d9:67:15:e3:77:57:18:84:27:02: 30:3d:02:75:0d:24:49:01:c0:00:60:66:c7:6c:65:40:7f:95: 37:38:51:97:84:16:94:eb:07:9b:39:e4:40:9b:9c:6e:8c:29: 15:b7:b1:97:27:01:1c:d0:71:61:34:6c:19
これはCosignが勝手にFulcioで発行した証明書です。OIDCで返されたメールアドレスなどが含まれています。この証明書から公開鍵を取り出します。ここまで真面目にやってきた人なら何も見ずにopensslのフラグを指定できるようになっていると思います。
$ openssl x509 -in cert.pem -pubkey -noout > pub.pem
次に署名を取り出します。これもBase64エンコードされているのでデコードします。
$ rekor-cli get --log-index 3732405 --format json | jq -r .Body.HashedRekordObj.signature.content | base64 -d > foo.sig
では公開鍵と署名が手に入ったので元のテキストファイル( foo.txt
)に対して署名検証してみます。
$ openssl dgst -sha256 -verify pub.pem -signature foo.sig foo.txt Verified OK
ということでCosignによって内部的に発行された証明書をRekorから取得して署名検証できました。ちなみに上のコマンドだけでは証明書のチェーンの検証も何もしていないですし、これだけでは検証としては当然不十分です。
手動で試す
いつも通りcurlでやっていくのですが、その前に rekor-cli
で署名をアップロードする方法を見てみます。
【参考】rekor-cliによるアップロード
署名をRekorに rekor-cli
でアップロードする方法が公式ドキュメントにあります。まずはこの通りやってみます。もしかしたら途中で突っ込みたくなる人もいるかもしれませんが一旦気にせず進めてください。
まず鍵ペアを作ります。
$ openssl ecparam -name prime256v1 -genkey -noout -out key.pem $ openssl ec -in key.pem -pubout -out pub.pem
次に署名します。ファイルは最初に作った foo.txt
をそのまま使います。
$ openssl dgst -sha256 -sign key.pem -out foo.sig foo.txt
そして署名を作ったのでこれを rekor-cli
でアップロードします。
$ rekor-cli upload --artifact foo.txt --signature foo.sig --pki-format=x509 --public-key=pub.pem Created entry at index 3737384, available at: https://rekor.sigstore.dev/api/v1/log/entries/b844f6e9407fec02ea89623655f26a092b42554efdcaa595a209551f2f565a28
無事にtlogが作成されました。このログの中身を見てみます。
$ rekor-cli get --log-index 3737384 LogID: c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d Index: 3737384 IntegratedTime: 2022-09-22T10:10:53Z UUID: b844f6e9407fec02ea89623655f26a092b42554efdcaa595a209551f2f565a28 Body: { "RekordObj": { "data": { "hash": { "algorithm": "sha256", "value": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" } }, "signature": { "content": "MEYCIQDc8y97Was9RTtlCxusn3/aHTQB0/qFwSmmJsNl9CThzgIhAIxDMYhMmuD8eQ3GGTmfT3POF4eeTU/w1BuH6qdpLs67", "format": "x509", "publicKey": { "content": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFbWd2RVVFMFBLTmo1WTlGeVpEbGMzNHg4NXV5Sgozdkp6ZlIyalJuZGtJWi9rVUxrWFBvZlFZODJHcEh0MmwzdDVPM2ZJR01oN09QSlUwdEJWVzVzdHhRPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" } } } }
確かに保存されています。めでたしめでたし...と言いたいところですがそうはいかないです。まず前提としてRekorは証明書を保存してくれるものだと思っていたのですが、単なるPEM形式の公開鍵のアップロードに成功しています。この公開鍵は何にも署名されていないので証明書のチェーンも作れません。
rekor-cli
が内部でいい感じに証明書を作っているのかも?と思ったので一応 signature.content.publicKey.content
を見てみました。
$ rekor-cli get --log-index 3737384 --format json | jq -r .Body.RekordObj.signature.publicKey.content | base64 -d > a.pem $ openssl ec -in a.pem -pubin -text -noout read EC key Private-Key: (256 bit) pub: 04:9a:0b:c4:50:4d:0f:28:d8:f9:63:d1:72:64:39: 5c:df:8c:7c:e6:ec:89:de:f2:73:7d:1d:a3:46:77: 64:21:9f:e4:50:b9:17:3e:87:d0:63:cd:86:a4:7b: 76:97:7b:79:3b:77:c8:18:c8:7b:38:f2:54:d2:d0: 55:5b:9b:2d:c5 ASN1 OID: prime256v1 NIST CURVE: P-256
やはりただの公開鍵です。公開鍵置き場としての役割も果たしたいのでしょうか。フィールド名も publicKey
となっているぐらいなので想定されている用途だとは思いますが知らなかったので驚きました。
さらに謎なのは、フォーマットがX.509になっていることです。これはCLIフラグで指定したからなのですが、ドキュメントもそうなっています。
"format": "x509",
自分の知る限りこれはX.509じゃないと思うのでもう何がなんだか分かりません。自分が知らないだけでX.509というのは単なる公開鍵も指すのでしょうか。ただGoのx509パッケージを見ると鍵のPEM<=>DER変換も含まれていたりするので、広義にはX.509なのかなと思うことにしました。
x509 package - crypto/x509 - Go Packages
公開鍵の場合は証明書チェーンの検証がスキップされるのかな?と思って一応Cosignで verify-blob
をしてみましたが普通に失敗しました。
$ COSIGN_EXPERIMENTAL=1 cosign verify-blob foo.txt --signature foo.sig WARNING: No valid entries were found in rekor to verify this blob. Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists. We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature]. Error: verifying blob [foo.txt]: could not find a valid tlog entry for provided blob, found 20 invalid entries main.go:62: error during command execution: verifying blob [foo.txt]: could not find a valid tlog entry for provided blob, found 20 invalid entries
謎すぎます。ただの公開鍵ストレージとしての機能なのでしょうか。ソースコードを確認したら公開鍵はやはり使えないようでした。
cosign/verify_blob.go at 63fe87501660595b7da9467078781e745272aa1b · sigstore/cosign · GitHub
curlでアップロード
参考として rekor-cli
でのアップロードを試したらよく分からないことになりましたが、気を取り直してCosignにおけるRekorに対するリクエストを見てみます。rekor.sigstore.dev に対して以下のリクエストを投げています。
> POST /api/v1/log/entries HTTP/1.1 > Host: rekor.sigstore.dev > Accept: application/json > Content-Length: 1610 > Content-Type: application/json { "apiVersion": "0.0.1", "spec": { "data": { "hash": { "algorithm": "sha256", "value": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" } }, "signature": { "content": "MEUCIQDLQvJl41+IK3o9eEEy7C+9VBxZfWzLUeHClKaJMZN/egIgVP15XLu0UUcdwnQwM2SMtgJ3gYWrHnkUzz4kHV2geOs=", "publicKey": { "content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNuekNDQWlhZ0F3SUJBZ0lVUFVGNitVQ3MrZGNtK1NiazM3Q0VNZ2kzK1lVd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09USXlNRGt5TWpBM1doY05Nakl3T1RJeU1Ea3pNakEzV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVvcUplZnQ3NnpLNWhtY2JNZ09LU1FSOXpPTEF5NjlDeG94NDIKazdGanpFOFdEeENWL3pOVlRTUmMwMDVMUmcxMGZUR2ZUM2NvWjV4bURUZFAvN2dvZktPQ0FVVXdnZ0ZCTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVQU0QyCktrbW5ZZW9qcStjcjlwblZLV2lUbk13d0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lBWURWUjBSQVFIL0JCWXdGSUVTYTI1eGVXWXlOak5BWjIxaGFXd3VZMjl0TUN3R0Npc0dBUVFCZzc4dwpBUUVFSG1oMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5c2IyZHBiaTl2WVhWMGFEQ0JpUVlLS3dZQkJBSFdlUUlFCkFnUjdCSGtBZHdCMUFBaGdrdkFvVXY5b1JkSFJheWVFbkVWbkdLd1dQY000MG0zbXZDSUdObTl5QUFBQmcyU0IKSWVvQUFBUURBRVl3UkFJZ0NyWkk1Snp6WEJKZW1BZm9xNjc5WVZiL3hvVWVWNDRmVEdPem5IVy8wODhDSURQTwpSb054WTJNekdDSE1XTEp1RlZGakIvRXVWeWF2NDkvN25JQ0ZnL0N2TUFvR0NDcUdTTTQ5QkFNREEyY0FNR1FDCk1CWjFOclJnZnhpZ0UvNkI3TUJrTytsNzZyQ0t0bWJzRk5yZVZ2N01Mb0FDYytHOWVHM3lHSHhpSFZnUFI0SE4KZ2dJd1NOLzNBY3pLYjduQjdoSlAwVlNCNnUyTlNSdElhcklQQU1RNS9hMlpxKzlzSnFWQW1QNnZ6dXRwZXl1UwpoVHBvCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" } } }, "kind": "hashedrekord" } * TLS connection using TLS 1.3 / TLS_AES_256_GCM_SHA384 * ALPN: h2 accepted * Server certificate: * subject: CN=api.sigstore.dev * start date: Sun Jul 31 15:11:23 UTC 2022 * expire date: Sat Oct 29 15:11:22 UTC 2022 * issuer: CN=R3,O=Let's Encrypt,C=US * TLS certificate verify ok. < HTTP/2.0 201 Created < Content-Type: application/json < Date: Thu, 22 Sep 2022 09:22:09 GMT < Etag: 0b362725bee7d55998856d59b88be6cccea49c6c1d42700e31a3c6d655b0bed9 < Location: /api/v1/log/entries/0b362725bee7d55998856d59b88be6cccea49c6c1d42700e31a3c6d655b0bed9 < Strict-Transport-Security: max-age=15724800; includeSubDomains < Vary: Origin { "0b362725bee7d55998856d59b88be6cccea49c6c1d42700e31a3c6d655b0bed9": { "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ODkxYjViNTIyZDVkZjA4NmQwZmYwYjExMGZiZDlkMjFiYjRmYzcxNjNhZjM0ZDA4Mjg2YTJlODQ2ZjZiZTAzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUURMUXZKbDQxK0lLM285ZUVFeTdDKzlWQnhaZld6TFVlSENsS2FKTVpOL2VnSWdWUDE1WEx1MFVVY2R3blF3TTJTTXRnSjNnWVdySG5rVXp6NGtIVjJnZU9zPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVla05EUVdsaFowRjNTVUpCWjBsVlVGVkdOaXRWUTNNclpHTnRLMU5pYXpNM1EwVk5aMmt6SzFsVmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVU1hsTlJHdDVUV3BCTTFkb1kwNU5ha2wzVDFSSmVVMUVhM3BOYWtFelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ2Y1VwbFpuUTNObnBMTldodFkySk5aMDlMVTFGU09YcFBURUY1TmpsRGVHOTRORElLYXpkR2FucEZPRmRFZUVOV0wzcE9WbFJUVW1Nd01EVk1VbWN4TUdaVVIyWlVNMk52V2pWNGJVUlVaRkF2TjJkdlprdFBRMEZWVlhkblowWkNUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZRVTBReUNrdHJiVzVaWlc5cWNTdGpjamx3YmxaTFYybFViazEzZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsQldVUldVakJTUVZGSUwwSkNXWGRHU1VWVFlUSTFlR1ZYV1hsT2FrNUJXakl4YUdGWGQzVlpNamwwVFVOM1IwTnBjMGRCVVZGQ1p6YzRkd3BCVVVWRlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEJpYVRsMldWaFdNR0ZFUTBKcFVWbExTM2RaUWtKQlNGZGxVVWxGQ2tGblVqZENTR3RCWkhkQ01VRkJhR2RyZGtGdlZYWTViMUprU0ZKaGVXVkZia1ZXYmtkTGQxZFFZMDAwTUcwemJYWkRTVWRPYlRsNVFVRkJRbWN5VTBJS1NXVnZRVUZCVVVSQlJWbDNVa0ZKWjBOeVdrazFTbnA2V0VKS1pXMUJabTl4TmpjNVdWWmlMM2h2VldWV05EUm1WRWRQZW01SVZ5OHdPRGhEU1VSUVR3cFNiMDU0V1RKTmVrZERTRTFYVEVwMVJsWkdha0l2UlhWV2VXRjJORGt2TjI1SlEwWm5MME4yVFVGdlIwTkRjVWRUVFRRNVFrRk5SRUV5WTBGTlIxRkRDazFDV2pGT2NsSm5abmhwWjBVdk5rSTNUVUpyVHl0c056WnlRMHQwYldKelJrNXlaVloyTjAxTWIwRkRZeXRIT1dWSE0zbEhTSGhwU0ZablVGSTBTRTRLWjJkSmQxTk9Mek5CWTNwTFlqZHVRamRvU2xBd1ZsTkNOblV5VGxOU2RFbGhja2xRUVUxUk5TOWhNbHB4S3pselNuRldRVzFRTm5aNmRYUndaWGwxVXdwb1ZIQnZDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==", "integratedTime": 1663838528, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", "logIndex": 3734692, "verification": { "signedEntryTimestamp": "MEUCIGhslYC6ITobDZMuRbE0Z2E4bCuh3KBBIXPJbrPphaNBAiEA6ggtcnBCRMV8+J7NOkULyv5r0lBKzG5Qn7IccwkSpqU=" } } }
該当コードは以下です。
cosign/tlog.go at 0baa044bea61e7c16d56023be20ead3d9204b24a · sigstore/cosign · GitHub
リクエストとしては data
と signature
を作れば良さそうです。
リクエストの作成
data
部分だけ取り出すと以下です。
"data": { "hash": { "algorithm": "sha256", "value": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" } },
これは署名対象のデータのハッシュ値です。既に計算済みですがもう一度やっておきます。
$ sha256sum foo.txt 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 foo.txt
signature
部分を抜き出すと以下です。
"signature": { "content": "MEUCIQDLQvJl41+IK3o9eEEy7C+9VBxZfWzLUeHClKaJMZN/egIgVP15XLu0UUcdwnQwM2SMtgJ3gYWrHnkUzz4kHV2geOs=", "publicKey": { "content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNuekNDQWlhZ0F3SUJBZ0lVUFVGNitVQ3MrZGNtK1NiazM3Q0VNZ2kzK1lVd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09USXlNRGt5TWpBM1doY05Nakl3T1RJeU1Ea3pNakEzV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVvcUplZnQ3NnpLNWhtY2JNZ09LU1FSOXpPTEF5NjlDeG94NDIKazdGanpFOFdEeENWL3pOVlRTUmMwMDVMUmcxMGZUR2ZUM2NvWjV4bURUZFAvN2dvZktPQ0FVVXdnZ0ZCTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVQU0QyCktrbW5ZZW9qcStjcjlwblZLV2lUbk13d0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lBWURWUjBSQVFIL0JCWXdGSUVTYTI1eGVXWXlOak5BWjIxaGFXd3VZMjl0TUN3R0Npc0dBUVFCZzc4dwpBUUVFSG1oMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5c2IyZHBiaTl2WVhWMGFEQ0JpUVlLS3dZQkJBSFdlUUlFCkFnUjdCSGtBZHdCMUFBaGdrdkFvVXY5b1JkSFJheWVFbkVWbkdLd1dQY000MG0zbXZDSUdObTl5QUFBQmcyU0IKSWVvQUFBUURBRVl3UkFJZ0NyWkk1Snp6WEJKZW1BZm9xNjc5WVZiL3hvVWVWNDRmVEdPem5IVy8wODhDSURQTwpSb054WTJNekdDSE1XTEp1RlZGakIvRXVWeWF2NDkvN25JQ0ZnL0N2TUFvR0NDcUdTTTQ5QkFNREEyY0FNR1FDCk1CWjFOclJnZnhpZ0UvNkI3TUJrTytsNzZyQ0t0bWJzRk5yZVZ2N01Mb0FDYytHOWVHM3lHSHhpSFZnUFI0SE4KZ2dJd1NOLzNBY3pLYjduQjdoSlAwVlNCNnUyTlNSdElhcklQQU1RNS9hMlpxKzlzSnFWQW1QNnZ6dXRwZXl1UwpoVHBvCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" } }
まず signature.content
を作ります。これは単なる署名のBase64エンコードです。一旦署名をファイルに書き出してあとでBase64エンコードします。
$ openssl dgst -sha256 -sign key.pem -out foo.sig foo.txt
次に signature.publicKey.content
ですが、これはFulcioから貰った証明書です。以前の記事で説明したので自分で試したい方はそちらを参照してください。以下では cert.pem
に書き込まれている想定です。
ではJSONを作ります。
$ cat << EOF > req.json { "apiVersion": "0.0.1", "spec": { "data": { "hash": { "algorithm": "sha256", "value": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" } }, "signature": { "content": "$(cat foo.sig | base64)", "publicKey": { "content": "$(base64 -w0 cert.pem)" } } }, "kind": "hashedrekord" } EOF
完成したJSONを投げます。ちなみのこの証明書も10分で有効期限が切れてしまうので、ちゃんとやりたい場合は証明書の発行をやり直す必要があります。Rekor自体は有効期限が切れていても関係なく受け入れるので試しに署名や証明書を登録したいだけならやり直さなくて良いです。
$ curl -s -X POST https://rekor.sigstore.dev/api/v1/log/entries -H "Content-Type: application/json" -d @req.json curl -X POST https://rekor.sigstore.dev/api/v1/log/entries -H "Content-Type: application/json" -d @req.json | jq . { "24296fb24b8ad77aa715cdfd264ce34c4d544375d7bd7cd029bf5a48ef25217a13fdba562e0889ca": { "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ODkxYjViNTIyZDVkZjA4NmQwZmYwYjExMGZiZDlkMjFiYjRmYzcxNjNhZjM0ZDA4Mjg2YTJlODQ2ZjZiZTAzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQUdRT3BaK0svbVpjSEZEMUpnSzBtZ3JNZENBZlRUWS9SQmUyV29aU2Z2SEFpRUF3Ky8wVDVpajVtMkJWdHpsY3JySlFoUzE4WGlwcDJwY1hKVEpFek56RG1NPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZWRU5EUVdsbFowRjNTVUpCWjBsVlVVc3hOREZhVldzeU9WaENURFpYUlhwVWFFdDJVR0ZUUnk5TmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVU1RWTlJHc3dUbFJSZDFkb1kwNU5ha2wzVDFSSk5VMUVhekZPVkZGM1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ2WVRWVFVXUXlTWE5QV0RKamVqRjNkSGh6VFVORVZtRlBhMnhDYjJoWVpWWmpjRk1LY0VoR1R5OWhiM05rTkZReFQwOXJiR3d3YkdSQ1JXTlplRkpyTDBWME1IWjRZMUpFTWtkcWRYQk1jRGxJVkhJMGMyRlBRMEZWV1hkblowWkRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZHSzFkSkNrdEpWR1p1Y1drNE5rbEZTazE0VUdGVFUyWm5ibk5CZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsQldVUldVakJTUVZGSUwwSkNXWGRHU1VWVFlUSTFlR1ZYV1hsT2FrNUJXakl4YUdGWGQzVlpNamwwVFVOM1IwTnBjMGRCVVZGQ1p6YzRkd3BCVVVWRlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEJpYVRsMldWaFdNR0ZFUTBKcFoxbExTM2RaUWtKQlNGZGxVVWxGQ2tGblVqaENTRzlCWlVGQ01rRkJhR2RyZGtGdlZYWTViMUprU0ZKaGVXVkZia1ZXYmtkTGQxZFFZMDAwTUcwemJYWkRTVWRPYlRsNVFVRkJRbWMwYVdvS1RrVTBRVUZCVVVSQlJXTjNVbEZKWjBnd01EUjFUVzR6T1dzM1dqWTFkR3BLYm1FcmRHRlZXRnBxT1VaalF6WXpRVmRNYUUxa09WcHRiMjlEU1ZGRGVBbzRMMWhtUWxwbVYwTTNObXRtTUc5aFltcDRhVWQwUWtvemRFSnBSSGR2TmpGck5YTlZORmRaWVZSQlMwSm5aM0ZvYTJwUFVGRlJSRUYzVG05QlJFSnNDa0ZxUlVGMVFqSnhia0pUUVcxR2JtOWxiek52UVhCRVJIbHBNRlZaWkRSNk5sSXJOVWd6UVU4MFozbE1ZMXBIUlRJMGJGSlpSelpIUlVKWGRWSTRSemdLZG1keGRrRnFRVFJUVmtZNVdtbzJiMVY2Y0ZWRFR6RjFhbkprUnpFNGJ6QkZLMkYzT0RaS1JFUkpNekpsYVZKdVRVUTNWR2syUm5FeVpHMW1WMnhKWXdweFJFSkhibFpyUFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19", "integratedTime": 1664451604, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", "logIndex": 4215471, "verification": { "inclusionProof": { "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n52041\n8XyMdDqdv/cS0idaNcLRAahDKaxecZIY0eyHEQT9iFs=\nTimestamp: 1664451605152835877\n\n— rekor.sigstore.dev wNI9ajBFAiEAyEA/+Jrqmpp3RSlJcvVACQdWcFLpQupa7ws3Nvfn+TICIC2waHRzaGF60EZ1BTz/eXSdvbBEJWRjSZVabWbyh3tK\n", "hashes": [ "7cbfd51112b902a865bfddbb03846815ca2f83ccbd11cbbc7e55150f8443e762", "f28f0e5cdd8ba0206fc63e06300a8045a4df720eb2232da0b45a8df3b0facb6d", "48e2551406feb46ca38676189b6d660b0ea3f6bb39513638ee16394cc4f00f4b", "a145e2ef3b678e1a12456c052caa2847a84209c60b48ba513dedf675810ee386", "2d9ed8909dc7e1c5c12d736e17441e97800a8c3b76d1158e2a91880d8925b8c1", "f015b45ef62857687ecc78cbdbd39506cc5c46efc45ed6a8814f17bb88fd8e6b", "3972997e9a268e76f88943ed840463d9aea69ab4b8c270ddd6c2c0287896b7b6" ], "logIndex": 52040, "rootHash": "f17c8c743a9dbff712d2275a35c2d101a84329ac5e719218d1ec871104fd885b", "treeSize": 52041 }, "signedEntryTimestamp": "MEYCIQDnxNLe8hpmTLjdY3ASHqO/dlLfozzNKruPeNZAMMYoHAIhAJ3Q/bon3FK+q+rspp2RLffalJZsdiT4A/grirCjWC0g" } } }
Signed Entry Timestampの検証
レスポンスを見ると signedEntryTimestamp
という値が含まれています。これはSigned Entry Timestamp (SET) のことで、Certificate TransparencyにおけるSigned Certificate Timestamp (SCT)とほぼ同じアイディアです。SCTについては前回のブログで説明しましたが、SCTは確かにCTログサーバにログが保存されましたよという証明のための時刻への署名で、SETも同様に確かにRekorにログが保存されましたよということを証明するための時刻への署名です。このSETをどのように検証するのかソースコードを見てみます。
cosign/verify.go at 80b79ed8b4d28ccbce3d279fd273606b5cddcc25 · sigstore/cosign · GitHub
cbundle.RekorPayload
をJSONにシリアライズしたあとにCanonicalization (JSON Canonicalization Schema, JCS)を行っています。これは同じJSONでもキーの順序が違ったり改行の有無だったりインデントの数だったり表現方法が複数あり署名に不便なので、必ず同じJSONが同じバイト列になるようにフォーマットの方法を定義したものです。詳細は以下に定義されています。
draft-rundgren-json-canonicalization-scheme-02
bundle.RekorPayload
は以下に定義されています。
cosign/rekor.go at a7bd67c0a7314200ed72e5d9870911e6149d76bf · sigstore/cosign · GitHub
body
, integratedTime
, logIndex
, logID
の4つが bundle.RekorPayload
を組み立てるために必要ということがわかります。
ということでまとめるとSETの署名検証に必要なのは以下の3つです。署名元と公開鍵と署名が必要というだけなので通常通りです。
- 署名(
signedEntryTimestamp
) - Rekorの公開鍵
- 署名対象(
body
,integratedTime
,logIndex
,logID
)
署名対象の4つは全て上のレスポンスボディに含まれています。上からコピペしつつJSONを手で組み立ててみます。
{ "integratedTime": 1664451604, "logIndex": 4215471, "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ODkxYjViNTIyZDVkZjA4NmQwZmYwYjExMGZiZDlkMjFiYjRmYzcxNjNhZjM0ZDA4Mjg2YTJlODQ2ZjZiZTAzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQUdRT3BaK0svbVpjSEZEMUpnSzBtZ3JNZENBZlRUWS9SQmUyV29aU2Z2SEFpRUF3Ky8wVDVpajVtMkJWdHpsY3JySlFoUzE4WGlwcDJwY1hKVEpFek56RG1NPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZWRU5EUVdsbFowRjNTVUpCWjBsVlVVc3hOREZhVldzeU9WaENURFpYUlhwVWFFdDJVR0ZUUnk5TmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVU1RWTlJHc3dUbFJSZDFkb1kwNU5ha2wzVDFSSk5VMUVhekZPVkZGM1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ2WVRWVFVXUXlTWE5QV0RKamVqRjNkSGh6VFVORVZtRlBhMnhDYjJoWVpWWmpjRk1LY0VoR1R5OWhiM05rTkZReFQwOXJiR3d3YkdSQ1JXTlplRkpyTDBWME1IWjRZMUpFTWtkcWRYQk1jRGxJVkhJMGMyRlBRMEZWV1hkblowWkRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZHSzFkSkNrdEpWR1p1Y1drNE5rbEZTazE0VUdGVFUyWm5ibk5CZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsQldVUldVakJTUVZGSUwwSkNXWGRHU1VWVFlUSTFlR1ZYV1hsT2FrNUJXakl4YUdGWGQzVlpNamwwVFVOM1IwTnBjMGRCVVZGQ1p6YzRkd3BCVVVWRlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEJpYVRsMldWaFdNR0ZFUTBKcFoxbExTM2RaUWtKQlNGZGxVVWxGQ2tGblVqaENTRzlCWlVGQ01rRkJhR2RyZGtGdlZYWTViMUprU0ZKaGVXVkZia1ZXYmtkTGQxZFFZMDAwTUcwemJYWkRTVWRPYlRsNVFVRkJRbWMwYVdvS1RrVTBRVUZCVVVSQlJXTjNVbEZKWjBnd01EUjFUVzR6T1dzM1dqWTFkR3BLYm1FcmRHRlZXRnBxT1VaalF6WXpRVmRNYUUxa09WcHRiMjlEU1ZGRGVBbzRMMWhtUWxwbVYwTTNObXRtTUc5aFltcDRhVWQwUWtvemRFSnBSSGR2TmpGck5YTlZORmRaWVZSQlMwSm5aM0ZvYTJwUFVGRlJSRUYzVG05QlJFSnNDa0ZxUlVGMVFqSnhia0pUUVcxR2JtOWxiek52UVhCRVJIbHBNRlZaWkRSNk5sSXJOVWd6UVU4MFozbE1ZMXBIUlRJMGJGSlpSelpIUlVKWGRWSTRSemdLZG1keGRrRnFRVFJUVmtZNVdtbzJiMVY2Y0ZWRFR6RjFhbkprUnpFNGJ6QkZLMkYzT0RaS1JFUkpNekpsYVZKdVRVUTNWR2syUm5FeVpHMW1WMnhKWXdweFJFSkhibFpyUFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19", "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" }
これだとJCSでフォーマットされていないので、変換します。JCSはjqでサポートされているのかと思ったのですが見つけられなかったので勝手にオレオレ実装でやります。キーを並び替えて改行やスペースを消すだけの簡易なものでも一応検証は通りました。ちゃんとやる場合は何かライブラリを使うなどしたほうが良いです。
$ jq --sort-keys . set.json | tr -d "\n " | tee set.canonical.json {"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ODkxYjViNTIyZDVkZjA4NmQwZmYwYjExMGZiZDlkMjFiYjRmYzcxNjNhZjM0ZDA4Mjg2YTJlODQ2ZjZiZTAzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQUdRT3BaK0svbVpjSEZEMUpnSzBtZ3JNZENBZlRUWS9SQmUyV29aU2Z2SEFpRUF3Ky8wVDVpajVtMkJWdHpsY3JySlFoUzE4WGlwcDJwY1hKVEpFek56RG1NPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnZWRU5EUVdsbFowRjNTVUpCWjBsVlVVc3hOREZhVldzeU9WaENURFpYUlhwVWFFdDJVR0ZUUnk5TmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlVU1RWTlJHc3dUbFJSZDFkb1kwNU5ha2wzVDFSSk5VMUVhekZPVkZGM1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ2WVRWVFVXUXlTWE5QV0RKamVqRjNkSGh6VFVORVZtRlBhMnhDYjJoWVpWWmpjRk1LY0VoR1R5OWhiM05rTkZReFQwOXJiR3d3YkdSQ1JXTlplRkpyTDBWME1IWjRZMUpFTWtkcWRYQk1jRGxJVkhJMGMyRlBRMEZWV1hkblowWkRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZHSzFkSkNrdEpWR1p1Y1drNE5rbEZTazE0VUdGVFUyWm5ibk5CZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsQldVUldVakJTUVZGSUwwSkNXWGRHU1VWVFlUSTFlR1ZYV1hsT2FrNUJXakl4YUdGWGQzVlpNamwwVFVOM1IwTnBjMGRCVVZGQ1p6YzRkd3BCVVVWRlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEJpYVRsMldWaFdNR0ZFUTBKcFoxbExTM2RaUWtKQlNGZGxVVWxGQ2tGblVqaENTRzlCWlVGQ01rRkJhR2RyZGtGdlZYWTViMUprU0ZKaGVXVkZia1ZXYmtkTGQxZFFZMDAwTUcwemJYWkRTVWRPYlRsNVFVRkJRbWMwYVdvS1RrVTBRVUZCVVVSQlJXTjNVbEZKWjBnd01EUjFUVzR6T1dzM1dqWTFkR3BLYm1FcmRHRlZXRnBxT1VaalF6WXpRVmRNYUUxa09WcHRiMjlEU1ZGRGVBbzRMMWhtUWxwbVYwTTNObXRtTUc5aFltcDRhVWQwUWtvemRFSnBSSGR2TmpGck5YTlZORmRaWVZSQlMwSm5aM0ZvYTJwUFVGRlJSRUYzVG05QlJFSnNDa0ZxUlVGMVFqSnhia0pUUVcxR2JtOWxiek52UVhCRVJIbHBNRlZaWkRSNk5sSXJOVWd6UVU4MFozbE1ZMXBIUlRJMGJGSlpSelpIUlVKWGRWSTRSemdLZG1keGRrRnFRVFJUVmtZNVdtbzJiMVY2Y0ZWRFR6RjFhbkprUnpFNGJ6QkZLMkYzT0RaS1JFUkpNekpsYVZKdVRVUTNWR2syUm5FeVpHMW1WMnhKWXdweFJFSkhibFpyUFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19","integratedTime":1664451604,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":4215471}
Rekorの公開鍵はRekorのAPIで公開されているので取ってきます。
$ curl -O https://rekor.sigstore.dev/api/v1/log/publicKey
これで全て揃いました。あとは signedEntryTimestamp
がBase64エンコードされているのでデコードしておきます。
$ echo -n "MEYCIQDnxNLe8hpmTLjdY3ASHqO/dlLfozzNKruPeNZAMMYoHAIhAJ3Q/bon3FK+q+rspp2RLffalJZsdiT4A/grirCjWC0g" | base64 -d > set.sig
これを --signature
に渡します。
$ openssl dgst -sha256 -verify publicKey --signature set.sig set.canonical.json Verified OK
ということで検証に成功しました。
ちなみに logIndex
と logID
って違い何?と気になるかもしれませんが、 logIndex
はいわゆるIDでログの登録される際に採番される連番の数値です。 logID
はログ保存時のRekorの公開鍵のDERエンコードのハッシュ値となっています。
$ curl -s https://rekor.sigstore.dev/api/v1/log/publicKey | openssl ec -outform der -pubin | shasum -a 256 read EC key writing EC key c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d -
で確かに上の値と一致します。正直名前分かりにくすぎない...?と思っています。以下のコメントを読んで知りました。
rekor/log_entry.go at 7f3a2561dd94d48ca3554fdf07340b76ba275149 · sigstore/rekor · GitHub
余談
上のRekorへのリクエストの中で対象データのハッシュ値をアップロードしていましたが、この spec.data.hash.value
の値がおかしいと弾かれました。サーバ側で署名検証に成功するかどうかを見てから保存しているようです。
# spec.ddata.hash.valueの値がおかしい $ cat req.json { "apiVersion": "0.0.1", "spec": { "data": { "hash": { "algorithm": "sha256", "value": "0a5005357763a89c7a36b43d19a41ab534d86f5346e1d1a64791279409f0688e" } }, "signature": { "content": "MEUCIAGQOpZ+K/mZcHFD1JgK0mgrMdCAfTTY/RBe2WoZSfvHAiEAw+/0T5ij5m2BVtzlcrrJQhS1 8Xipp2pcXJTJEzNzDmM=", "publicKey": { "content": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNvVENDQWllZ0F3SUJBZ0lVUUsxNDFaVWsyOVhCTDZXRXpUaEt2UGFTRy9Nd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09USTVNRGswTlRRd1doY05Nakl3T1RJNU1EazFOVFF3V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVvYTVTUWQySXNPWDJjejF3dHhzTUNEVmFPa2xCb2hYZVZjcFMKcEhGTy9hb3NkNFQxT09rbGwwbGRCRWNZeFJrL0V0MHZ4Y1JEMkdqdXBMcDlIVHI0c2FPQ0FVWXdnZ0ZDTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVGK1dJCktJVGZucWk4NklFSk14UGFTU2ZnbnNBd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lBWURWUjBSQVFIL0JCWXdGSUVTYTI1eGVXWXlOak5BWjIxaGFXd3VZMjl0TUN3R0Npc0dBUVFCZzc4dwpBUUVFSG1oMGRIQnpPaTh2WjJsMGFIVmlMbU52YlM5c2IyZHBiaTl2WVhWMGFEQ0JpZ1lLS3dZQkJBSFdlUUlFCkFnUjhCSG9BZUFCMkFBaGdrdkFvVXY5b1JkSFJheWVFbkVWbkdLd1dQY000MG0zbXZDSUdObTl5QUFBQmc0aWoKTkU0QUFBUURBRWN3UlFJZ0gwMDR1TW4zOWs3WjY1dGpKbmErdGFVWFpqOUZjQzYzQVdMaE1kOVptb29DSVFDeAo4L1hmQlpmV0M3NmtmMG9hYmp4aUd0QkozdEJpRHdvNjFrNXNVNFdZYVRBS0JnZ3Foa2pPUFFRREF3Tm9BREJsCkFqRUF1QjJxbkJTQW1Gbm9lbzNvQXBERHlpMFVZZDR6NlIrNUgzQU80Z3lMY1pHRTI0bFJZRzZHRUJXdVI4RzgKdmdxdkFqQTRTVkY5Wmo2b1V6cFVDTzF1anJkRzE4bzBFK2F3ODZKRERJMzJlaVJuTUQ3VGk2RnEyZG1mV2xJYwpxREJHblZrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" } } }, "kind": "hashedrekord" } $ curl -X POST https://rekor.sigstore.dev/api/v1/log/entries -H "Content-Type: application/json" -d @req.json {"code":400,"message":"Error processing entry: verifying signature: invalid signature when validating ASN.1 encoded signature"}
上述した通り、 publicKey
の部分を公開鍵にしても通ります。
$ cat pubkey.pem | base64 -w0 LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFb2E1U1FkMklzT1gyY3oxd3R4c01DRFZhT2tsQgpvaFhlVmNwU3BIRk8vYW9zZDRUMU9Pa2xsMGxkQkVjWXhSay9FdDB2eGNSRDJHanVwTHA5SFRyNHNRPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
署名検証できない公開鍵は弾かれました。つまり証明書であれ公開鍵であれ検証はしているようです。
さらに自己署名証明書も通りました。Fulcioで発行されたもののみ通るような仕様にはなっていないようです。
$ openssl req -new -x509 -days 3650 -key key.pem -sha512 -out server.crt $ curl -X POST https://rekor.sigstore.dev/api/v1/log/entries -H "Content-Type: application/json" -d @req.json {"907a8e725e8358c0ee2d050568fbc442472149d2d701a45828db074f941db393":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI1ODkxYjViNTIyZDVkZjA4NmQwZmYwYjExMGZiZDlkMjFiYjRmYzcxNjNhZjM0ZDA4Mjg2YTJlODQ2ZjZiZTAzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJRUdrd2NCaWppVDhWTFIyUUkyNDF4NXRvV3l4Zy9acmdweWVYbXZuRnRZeEFpRUFxamVhUWRhaUZORmIyT0tHRnM1ODhvM3p1UE1tUlZIaFU2OGVWZjN5RHNvPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVSkVSRU5DZEVGSlNrRktPVVJKZVZod2JVUkJkazFCYjBkRFEzRkhVMDAwT1VKQlRVVk5RVGg0UkZSQlRFSm5UbFpDUVUxTlFrWlNiR016VVhjS1NHaGpUazFxU1hkUFZFbDVUVlJGZDA5VVRYcFhhR05PVFhwSmQwOVVSVFZOVkVWM1QxUk5lbGRxUVZCTlVUQjNRM2RaUkZaUlVVUkVRVkpWV2xoT01BcE5SbXQzUlhkWlNFdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlRVVVJuZW1Gc1JIWjJSRlJXTms5UlZtcHFiR3hKYTNsV1pXTkNDbFU1ZDA5emFUbDNTRTVYVmpkVlJFMUJieXRGU1cxTGNGY3JjSFpTTVcxME9GZG1jM2xZWTJ4bk1uTjVURzV0UTNOM1lVVkJkRzByYkVSQlMwSm5aM0VLYUd0cVQxQlJVVVJDUVU1SVFVUkNSVUZwUW5aemRraDZMelU0UXk5eGExWldSR2RWSzNGalFtWlNjV3gzUTJ0WFZWZHhkRlpPU2pGWGQzSmlNRUZKWndwVVRXZFlTbkZuU1hWWVNFcDZLM0p1V1VrclRWWkxWbXBqZUVsRVNFRnZaREppTnpWM1FtUjJVRzkzUFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19","integratedTime":1663845107,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":3740608,"verification":{"signedEntryTimestamp":"MEQCIFUFVxQ3+5/+N+qPoa/KeIcxoU0Xp4YNTzryQ9+ck1AvAiBGx5rLZ8FntXinOhDhzqK0YLJ8Eh6RKO+8mWweJ2Zg+Q=="}}}
つまりRekorから取ってきた証明書は無条件に信頼できるわけではなく、Keyless Signingの検証をするためにはまずFulcioの証明書チェーンを検証する必要があります。この検証は次の記事でやってみます。
まとめ
署名と証明書をRekorに手動で保存してみました。Rekorは他にもattestationと呼ばれるものも保存できたりします。in-toto attestationについては以下のブログに詳しく書かれています。
今回までで一通りKeylessな署名が完了しました。次のブログでは検証で行われていることを見ていきます。