はじめに
本記事はKeyless Signing連載の二本目です。
- 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編
前回 はKeyless SigningのうちOpenID Connect(OIDC)によるIDトークンの発行までを行いました。この記事ではFulcioに証明書を発行してもらうところまで行います。
Fulcioはコード署名のためのルート認証局(CA)という位置づけです。自分で作った公開鍵を投げると証明書を発行してくれます。ただしOIDCのIDトークンを要求するところが少しユニークな点です。このIDトークンにより誰が証明書を発行したのかを担保します。
Fulcioはコード署名のLet's Encryptを自称しているだけあってSSL/TLSサーバ証明書の発行のフローとよく似ています。
“The Let's Encrypt of Code Signing"
証明書発行の流れ
まず始めにFulcioで証明書を発行するまでに行われることを見ていきます。以下のドキュメントに7つの手順があると書かれています。
- Certificate Request Input
- Authentication
- Verifying the challenge
- Constructing a certificate
- Signing the certificate
- Certificate Transparency log inclusion
- Return certificate to client
検証などを一旦忘れて大雑把にやることを列挙すると
- IDトークンと公開鍵をFulcioに送る
- FulcioがIDトークン内のメールアドレスなどを埋め込んで署名してPrecertificateを作る
- Certificate TransparencyログサーバにPrecertificateを送りSigned Certificate Timestamp (SCT)を得る
- SCTを証明書に埋め込んで再度署名してクライアントに返す
という流れです。PrecertificateやSCTも忘れてしまえば、"IDトークンと公開鍵を送ると証明書を返してくれる"サービスです。誰かが審査するとかはなくて、有効なIDトークンさえ送れば自動で証明書が発行されます。FulcioはOSSなので自前でホストすることも出来ますし、パブリックインスタンス( fulcio.sigstore.dev
)を使うことも出来ます。
以下でそれぞれのフローを詳しく見ていきますが、この部分はドキュメントを読めば分かることなので後半の手動で試すところまでスキップしても良いです。
Certificate Request Input
クライアントは以下の3つをFulcioに送ります。
- OIDC IDトークン
- 公開鍵
- チャレンジ
まずIDトークンを含めます。 前回の記事 では主にメールアドレスを署名者のIDとして用いていましたが、GitHub ActionsなどのIdPに対応しているCI/CD上でIDトークンを発行する場合はworkflow identityも利用可能です。GitHub Actionsの場合はworkflowを定義しているYAMLファイルのファイルパスになります( .github/workflows/release.yaml
など)。
公開鍵は秘密鍵と共に手元で作ったものです。
そして概要の説明時にはIDトークンと公開鍵を送ると言いましたが、実際にはもう一つチャレンジというものを送ります。OIDCトークンの sub
クレームの値を秘密鍵で署名したものになります。このチャレンジの署名検証によって公開鍵の正当な保持者であることが証明できます。と言っておいてなんですが、これは実は嘘です。
A signed challenge. This challenge proves the client is in possession of the private key that corresponds to the public key provided. The challenge is created by signing the subject (sub) of the OIDC identity token.
確かにドキュメントにも上のように書いてあるのですが、実際にIDトークンの中を見ると sub
にはBase64エンコードされた文字列が入っており、メールアドレスは実際には email
に入っています。自分が何か勘違いしているのかと思いソースコードを読みましたが、 sub
は email
がない場合だけ使われるようになっていました。
sigstore/flow.go at 390675bb3335540929df00e05fdd01ce50b7224e · sigstore/sigstore · GitHub
やはりソースコードが正義ということです。ドキュメントを常に疑ってかかる姿勢を大事にしましょう。
これらの代わりにCertificate Signing Request (CSR) を送ることも出来るようですが、今回は上の3つを送ります。あとで実際にやってみます。
Authentication
ここから先はFulcio内部で行われることです。
次に受け取ったIDトークンの認証を行います。これは 前回の記事 で手元で行ったIDトークンの認証と同じことをFulcioで行うだけです。正しいIssuer(iss
) によって発行されていることをまず確認し、次にIdPの公開鍵を使って署名検証をします。これで署名を行った人間・またはworkflowの身元確認が出来ます。
Verifying the challenge
リクエスト内に含まれていたチャレンジの検証を行います。チャレンジを公開鍵で復号し、IDトークン内の sub
または email
クレームのハッシュ値と一致するかを確認します。これで秘密鍵を持っている=正しい公開鍵の保持者であることが確認できます。
Constructing a certificate
そして証明書を作ります。IDトークン内のiss
とsub
(または email
)や公開鍵を埋め込みます。他にもGitHub ActionsのworkflowのメタデータもIDトークン内に存在する場合はX.509の拡張領域に入れます。
Signing the certificate
Fulcioの秘密鍵を使ってこの証明書に署名をします。署名のバックエンドとしてKMSやTinkなどが選べます。
Certificate Transparency log inclusion
証明書を発行したらCertificate Transparency (CT) ログサーバに証明書を追加します。このCTログサーバは改ざんできないようになっています。CTログサーバに証明書をアップロードする際、"Poison"というX.509拡張を入れることで一般的には使えない証明書にしています。これをPrecertificateと呼んでいます。このフローはSSL/TLSサーバ証明書と同じなので、Certificate Transparencyについて調べてみるとFulcioにおけるCTも理解できると思います。
そしてCTログサーバからSigned Certificate Timestamp (SCT)というものが返されます。このSCTの検証だけで長くなったので次の記事で説明します。
このSCTはCTログが保存されたということの証明なので、これを証明書に埋め込みます。SCTを埋め込んだことで証明書のハッシュ値が変わってしまうので再度署名をし直します。つまりCTログに追加するためのPrecertificateとSCTを埋め込んだあとの証明書の2つあるということです。この辺も通常のCTログと同じです。
ドキュメントに書いてありますが、このFulcioのCTログはRekorとは別に保存されています。FulcioのCTログは発行された証明書だけ保存するのに対し、Rekorは署名やattestationも保存するためもう少し汎用的です。
Return certificate to client
あとはIDトークンの情報や公開鍵、そしてSCTを含んだ証明書をクライアントに返します。
手動で発行する
ここまででFulcioの挙動は何となく理解できたと思います。ここからは実際に手で証明書の発行を行ってみます。
Certificate Request Input
手元で鍵ペアを作り、IDトークンと一緒にFulcioに投げます。さらに先程説明したようにメールアドレス(または sub
)を秘密鍵で署名した値をチャレンジとして一緒に投げます。ということで以下の3つを生成していきます。
IDトークン
これは 前回の記事 で発行したので参照してください。
鍵ペアの作成
Cosignのソースコードを覗くとECDSAを使って秘密鍵を生成しているのが分かります。
cosign/keys.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub
ということでopensslコマンドで秘密鍵を生成します。
$ openssl ecparam -genkey -name prime256v1 -outform PEM -out key.pem
そして公開鍵を生成します。Cosignでは公開鍵に対して x509.MarshalPKIXPublicKey
を呼んでいます。
cosign/fulcio.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub
この関数はDER形式に変換するようです。
x509 package - crypto/x509 - Go Packages
ということでDER形式にした公開鍵を生成します(-outform DER
)。
$ openssl ec -in key.pem -pubout -outform DER -out pubkey.der read EC key writing EC key
これで鍵ペアが手に入りました。
チャレンジの作成
今回はIDトークンを自分のメールアドレスで発行したので、IDトークン内の sub
ではなく email
クレームを使います。メールアドレスをSHA256でハッシュ計算しその値に対して先ほど作成した秘密鍵で署名します。Cosignでは以下の箇所でメールアドレスに対する署名が行われています。
cosign/fulcio.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub
IDトークンのemail
の knqyf263@gmail.com
に署名してみます。自分でハッシュ計算して署名してもよいのですが、opensslで一発で行えます。ASN.1でエンコードされた値が返ってくるのでBase64エンコードします。
$ echo -n "knqyf263@gmail.com" | openssl dgst -sha256 -sign key.pem | base64 -w0 > email.sig # 一応 asn1parse して確認 $ base64 -d email.sig | openssl asn1parse -inform der 0:d=0 hl=2 l= 70 cons: SEQUENCE 2:d=1 hl=2 l= 33 prim: INTEGER :DA2CF1A68EA1609999AF922E15E4BF23E72E425731A7D73B112E46FE66262D8E 37:d=1 hl=2 l= 33 prim: INTEGER :A58C320026FAE4EC6D9B563FF66B361CC234882EABE47CA641647D8B74113965
これでチャレンジの完成です。
リクエストの作成
上で作ったメールアドレスの署名と公開鍵をJSONに入れます。公開鍵は publicKey
の content
に入れて algorighm
も指定します。チャレンジは signedEmailAddress
に入れます。
cosign/fulcio.go at 7ba521444f9fcfdf2e1e5936c05834597674e6c9 · sigstore/cosign · GitHub
jqとかで賢くできそうですが、ここでは力技で作ります。
$ echo "{ \"publicKey\": { \"content\": \"$(base64 -w0 pubkey.der)\", \"algorithm\": \"ecdsa\" }, \"signedEmailAddress\": \"$(cat email.sig)\" }" > req.json
完成形は以下です。
{ "publicKey": { "content": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/5L2Un/ibLlDCUSsu80/yzoTpsy0rRaaBTN/kPyemVEo6jasVBVJtGieY41kDu7oODiEn0gG69N7Ci2JLiQJJQ==", "algorithm": "ecdsa" }, "signedEmailAddress": "MEYCIQDaLPGmjqFgmZmvki4V5L8j5y5CVzGn1zsRLkb+ZiYtjgIhAKWMMgAm+uTsbZtWP/ZrNhzCNIguq+R8pkFkfYt0ETll" }
リクエストをFulcioに送る
JSONが出来上がったのでFulcioにIDトークンと一緒に投げます。ちなみにIDトークンの有効期限は59秒なので、ここまでゆっくり手で確かめてきた場合はほぼ間違いなく有効期限が切れているので再発行します。認可コードも再取得が必要です。前回の記事 を参考に再発行してください。今回は取得したJWTを jwt.json
に書き出しています。
$ curl -X POST https://oauth2.sigstore.dev/auth/token -H "Authorization: Basic c2lnc3RvcmU6" -d "code=for6hqtcd7vy7ypsb3hmbfbzi&code_verifier=ir5shaejohr1piu8eicei2aipieMeej3al6ou9Chies&grant_type=authorization_code&nonce=nonce&redirect_uri=http%3A%2F%2Flocalhost%3A60000%2Fauth%2Fcallback" | jq -r .id_token > token.jwt
ではリクエストを送ります。59秒以内に行う必要があるのでここは時間との勝負です。絶対に自動化せずに全部手でやってやるという強い意志が必要です。IDトークンは Authorization
ヘッダに入れています。
$ curl -X POST https://fulcio.sigstore.dev/api/v1/signingCert \ -H "Authorization: Bearer $(cat token.jwt)" \ -H "Content-Type: application/json" \ -d @req.json -----BEGIN CERTIFICATE----- MIICoDCCAiegAwIBAgIUZgHjBZrbK3kC14OpgRVvC4yOX4AwCgYIKoZIzj0EAwMw NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl cm1lZGlhdGUwHhcNMjIwOTIwMDc1NzM4WhcNMjIwOTIwMDgwNzM4WjAAMFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAE/5L2Un/ibLlDCUSsu80/yzoTpsy0rRaaBTN/ kPyemVEo6jasVBVJtGieY41kDu7oODiEn0gG69N7Ci2JLiQJJaOCAUYwggFCMA4G A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUIZZM GQRZVtbjt0Ndr01jlyYbifgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y ZD8wIAYDVR0RAQH/BBYwFIESa25xeWYyNjNAZ21haWwuY29tMCwGCisGAQQBg78w AQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIE AgR8BHoAeAB2AAhgkvAoUv9oRdHRayeEnEVnGKwWPcM40m3mvCIGNm9yAAABg1nn Da4AAAQDAEcwRQIhANKNWBHWXaZ0b5quoyflm5HxkOjQp9bHqrWi+Og7sObcAiBo lf691xSgqCKQ4LgWpP9bpR5eq2K+SMbSuuWONlVmbTAKBggqhkjOPQQDAwNnADBk AjBwCAIBzOjVJNbF0esc8RqUIMw9t+ISCgaTM2xSoQo+2qvzoFjbas7FnXR0Wv2L VJcCMHwvUWwSj2DQyLHe8dDFOMSCy2VLMrzzZVk5WyIkq6jteQ95r29/Ik3Oe897 B9Y3uw== -----END CERTIFICATE----- -----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----- -----BEGIN CERTIFICATE----- MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ -----END CERTIFICATE-----
ということで証明書を発行できました。
証明書の中身確認
3つ返ってきたうちの一番上の証明書がユーザの公開鍵に対応した証明書なので、ファイル( cert.pem
)に書き出し中身を見てみます。
$ openssl x509 -in cert.pem -pubin -outform PEM -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 66:01:e3:05:9a:db:2b:79:02:d7:83:a9:81:15:6f:0b:8c:8e:5f:80 Signature Algorithm: ecdsa-with-SHA384 Issuer: O = sigstore.dev, CN = sigstore-intermediate Validity Not Before: Sep 20 07:57:38 2022 GMT Not After : Sep 20 08:07:38 2022 GMT Subject: Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:ff:92:f6:52:7f:e2:6c:b9:43:09:44:ac:bb:cd: 3f:cb:3a:13:a6:cc:b4:ad:16:9a:05:33:7f:90:fc: 9e:99:51:28:ea:36:ac:54:15:49:b4:68:9e:63:8d: 64:0e:ee:e8:38:38:84:9f:48:06:eb:d3:7b:0a:2d: 89:2e:24:09:25 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: 21:96:4C:19:04:59:56:D6:E3:B7:43:5D:AF:4D:63:97:26:1B:89:F8 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 20 07:57:38.094 2022 GMT Extensions: none Signature : ecdsa-with-SHA256 30:45:02:21:00:D2:8D:58:11:D6:5D:A6:74:6F:9A:AE: A3:27:E5:9B:91:F1:90:E8:D0:A7:D6:C7:AA:B5:A2:F8: E8:3B:B0:E6:DC:02:20:68:95:FE:BD:D7:14:A0:A8:22: 90:E0:B8:16:A4:FF:5B:A5:1E:5E:AB:62:BE:48:C6:D2: BA:E5:8E:36:55:66:6D Signature Algorithm: ecdsa-with-SHA384 Signature Value: 30:64:02:30:70:08:02:01:cc:e8:d5:24:d6:c5:d1:eb:1c:f1: 1a:94:20:cc:3d:b7:e2:12:0a:06:93:33:6c:52:a1:0a:3e:da: ab:f3:a0:58:db:6a:ce:c5:9d:74:74:5a:fd:8b:54:97:02:30: 7c:2f:51:6c:12:8f:60:d0:c8:b1:de:f1:d0:c5:38:c4:82:cb: 65:4b:32:bc:f3:65:59:39:5b:22:24:ab:a8:ed:79:0f:79:af: 6f:7f:22:4d:ce:7b:cf:7b:07:d6:37:bb
Subject Alternative Name (SAN) にはIDトークン内のメールアドレスが含まれています。
X509v3 Subject Alternative Name: critical email:knqyf263@gmail.com
1.3.6.1.4.1.57264.1
で始まるOIDはFulcioのものとして登録されているようで、GitHubのWorkflowに関するメタデータなども入れられるようになっています。
fulcio/oid-info.md at main · sigstore/fulcio · GitHub
1.3.6.1.4.1.57264.1.1
はIssuerとして定義されています。IDトークンの iss
を入れただけかと思いきや、よくIDトークンを見返すと iss
は以下になっています。
"iss": "https://oauth2.sigstore.dev/auth",
ですが上のIssuerはGitHubになっており、異なる値です。どうやら iss
ではなくIDトークン内の federated_claims.connector_id
を使っているようです。これは元々のGitHubのIDトークンの iss
の値と一致するはずです。IdPとしてGoogleを使えば https://accounts.google.com
になります。ちなみにこの federated_claims
については仕様が見つけられなかったのですがsigstore独自のクレームなのでしょうか。
"federated_claims": { "connector_id": "https://github.com/login/oauth", "user_id": "2253692" }
そして上述したSigned Certificate Timestamp (SCT)も含まれています。SCTについてはこの次の記事で細かく解説します。
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 20 07:57:38.094 2022 GMT Extensions: none Signature : ecdsa-with-SHA256 30:45:02:21:00:D2:8D:58:11:D6:5D:A6:74:6F:9A:AE: A3:27:E5:9B:91:F1:90:E8:D0:A7:D6:C7:AA:B5:A2:F8: E8:3B:B0:E6:DC:02:20:68:95:FE:BD:D7:14:A0:A8:22: 90:E0:B8:16:A4:FF:5B:A5:1E:5E:AB:62:BE:48:C6:D2: BA:E5:8E:36:55:66:6D
二番目と三番目の証明書はそれぞれFulcioの中間証明書とルート証明書です。発行してもらった証明書はこれらとチェーンになっています。@otameshi61さんのブログ でも証明書の検証をしていますが、一応ここでも同様の内容を書いておきます。
まず上の2つの証明書をCAファイルに書き出します。リモートからも取ってこれます。
$ curl https://sigstore-tuf-root.storage.googleapis.com/targets/fulcio_v1.crt.pem > p.crt.pem $ echo >> p.crt.pem $ curl https://sigstore-tuf-root.storage.googleapis.com/targets/fulcio_intermediate_v1.crt.pem >> p.crt.pem
そしてこれらによって証明書チェーンが作られていることを確認します。
$ openssl verify -verbose -CAfile p.crt.pem cert.pem cert.pem: error 10 at 0 depth lookup:certificate has expired OK
確かにFulcioによって発行された証明書のようです。
Authentication
ここまでで証明書は発行できたのですが、Fulcioで行っていることも一応確認していきます。 まずAuthenticationはIDトークンの検証ですが、 前回の記事 で検証したのと同じ手順なので省略します。
Verifying the challenge
Fulcioでは送られてきたチャレンジを、一緒に送られてきた公開鍵で検証します。
# まずBase64デコードする $ cat email.sig | base64 -d > email.sig.bin # 検証する $ echo -n "knqyf263@gmail.com" | openssl dgst -sha256 -verify pubkey.der -signature email.sig.bin Verified OK
ということで検証終わりです。あとは証明書を作成してCTログに登録し、クライアントに証明書を返します。このフローの一部は次の記事で検証します。
まとめ
OIDCのIDトークンと手元で作成した公開鍵を使ってFulcioで証明書の発行ができました。このあとは秘密鍵を使ってソフトウェアに署名をし、その署名と証明書をRekorにアップロードします。RekorにアップロードさえしてしまえばFulcioから得た証明書は破棄可能なところが面白い点です。ですがその前に上のCTログサーバによって返されたSCTの検証を次の記事でやってみます。