knqyf263's blog

自分のためのメモとして残しておくためのブログ。

curlでKeyless Signingする (4) - Rekor編

はじめに

手動でKeyless Signingしてみようという誰にも望まれていないシリーズの4本目です。

前回までの記事で手元で鍵ペアを作ってFulcionから証明書を発行するところまでを行いました。今回は実際にソフトウェアに署名を行い、その署名と証明書をRekorにアップロードするところまでをやってみます。ここまで終わればKeyless Signingは完了です。

Rekorは Software Supply Chain Transparency Log という説明から分かるように、ソフトウェアサプライチェーンのためのTransparency Logサーバです。

docs.sigstore.dev

前回の記事で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 でアップロードする方法が公式ドキュメントにあります。まずはこの通りやってみます。もしかしたら途中で突っ込みたくなる人もいるかもしれませんが一旦気にせず進めてください。

docs.sigstore.dev

まず鍵ペアを作ります。

$ 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

リクエストとしては datasignature を作れば良さそうです。

リクエストの作成

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.RekorPayloadJSONシリアライズしたあとに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

これで全て揃いました。あとは signedEntryTimestampBase64エンコードされているのでデコードしておきます。

$ 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

ということで検証に成功しました。

ちなみに logIndexlogID って違い何?と気になるかもしれませんが、 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については以下のブログに詳しく書かれています。

otameshi61.hatenablog.com

今回までで一通りKeylessな署名が完了しました。次のブログでは検証で行われていることを見ていきます。