確実に忘れるであろう将来の自分と、Keyless Signingに異常な興味を持つ日本に数人しかいないであろう人達のための記事です。
背景
以前sigstoreのソフトウェア署名についてブログを書きました。
その中でKeyless Signingについては別ブログにすると言っていたのですがサボり続けた結果、全て忘れ去り再び調べる羽目になりました。これはまた忘れるだろうなということで今回はちゃんと書いておきます。ただ概要ではなく理解を深めるために、sigstoreにより提供されているCosignコマンドを使わずに、自分でcurlなどのCLIを使って署名をしてみます。タイトルにはcurlで〜と書きましたが実際にはopensslなど他のコマンドのほうが多分多いです。
curlでやってみるシリーズは過去にいくつかあります。
curlでdocker pullをする - knqyf263's blog
curlで始めるDockerコンテナからの脱出 - knqyf263's blog
コンテナイメージのlazy pullingをcurlで試してみる - knqyf263's blog
Keyless Signingと言っても構成要素は既存技術を用いているので、Keyless Signingを通してOpenID ConnectやCertificate Transparencyへの理解を深めることが出来てお得です。ただ理解を深めるためとはいえCLIだけで頑張るのは少しやりすぎた気がします(疲れた)。
あまりに長くなりすぎたので6本立てにしています。
- 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編
前提
@otameshi61 さんによる以下の素晴らしいブログをベースに書いているので、まず最初にこちらに目を通してください。
ソフトウェア署名については上の拙作ブログを参照してください。Cosign/Rekor/Fulcioって何?というところはそちらに書いてあるので今回は説明しません。
今回の一連のブログでは以下の処理をなぞる形になります。
$ COSIGN_EXPERIMENTAL=1 cosign sign-blob --output foo.txt.sig foo.txt -d -y
一度実行してみると何となく流れがわかると思います。 --output
で署名をファイルに書き出しています。
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
コンテナイメージの署名の話を混ぜるとややこしくなるので今回は単なるファイルやバイナリへの署名( cosign sign-blob
)について見ていきます。こっちが理解できればコンテナイメージの署名もすぐ理解できると思います。
Cosignの処理をなぞると言いましたが、ソースコードを全て解説するのは大変すぎるので重要な部分だけに絞ります。
あとPKIって何?とかOpenID Connectって何?とかそういう基礎的なところも説明しません。
Keyless Signing全体のフロー
Keyless Signingの流れはざっくり以下です。
- OpenID Connectで署名者のIDトークンを取得
- 鍵ペアを生成し、ルート認証局であるFulcioで署名してもらって証明書を得る
- このとき1で作ったIDトークンも一緒に送る
- 2で作った秘密鍵を使ってソフトウェア等に署名する
- Rekorに署名と証明書を保存する
- (2で作った鍵ペアや証明書を破棄する)
Cosign CLI を使ってKeyless Signingを使う場合のフローを図にすると以下です。
まず概要を掴むということで細かいフローを省いているので厳密でない箇所が複数あります。各要素の詳細な内容に関しては今後深堀りしていくので、この図はあくまで雰囲気を理解するためのものという点だけ注意してください。
このKeyless Signingにはいくつか特徴的な点があります。
Fulcioで証明書を発行する際、IDトークンも同時に送ることで誰が署名したのか?というのを検証します。その署名者のアイデンティティ(メールアドレスなど)は証明書のX.509拡張に含まれ改ざんできないようになります。
また、この生成された鍵ペアは1回使ったらすぐ破棄します。つまりソフトウェアに署名するときは毎回異なる鍵ペアが使われるということです。ソフトウェアのメンテナが鍵を保管する必要がないというのがKeylessと呼ばれる所以です。内部では鍵を使っています。
さらに証明書の有効期限が短く設定されているため、攻撃者に鍵ペアや証明書が後ほど盗まれても再度署名することは出来ません。これはRekorに登録されたTransparency Logの時間を見るのですが、細かい話なので別の記事にします。IDトークンの有効期限も短いので攻撃者がIDトークンを盗んで再度Fulcioで証明書を発行しようとしても弾かれます。
とにかく全てを使い捨てにして証明書や署名のログをRekorに残しておくことで鍵の管理を不要にしようというのがKeyless Signingの基本アイディアかなと思います。概要だけ知りたい人はここまでで十分かもしれません。
この記事では上記のうちOpenID Connect(OIDC)のフローを手動で確かめます。
OIDCのフロー
上では簡単にOIDCでIDトークンを取得、と書きましたが細かくは2つのステップがあります。
- 認可コードの取得
- IDトークンの取得
これは普通のOIDCのCode Grantフローなので、そもそもOIDCって何?という場合は以下などを見ると理解できると思います。
認可コードの取得
Cosign CLIによる認可コードの取得部分の詳細は以下のようになっています。少しややこしいのはアイデンティティプロバイダ(IdP)が2つ登場する点です。こういうIdPのリレーというかプロキシみたいなものは一般的なのかよく分かりませんでした。有識者によるコメントをお待ちしています。
- IdPである oauth2.sigstore.dev のAuthorization Endpointにアクセス
- GitHub/Google/Microsoftの中からIdPを選びリダイレクトされる
- ユーザが選択したIdPで認証・認可を行う
- 選択したIdPから認可コードがコールバックで返ってくるので oauth2.sigstore.dev に送る
- oauth2.sigstore.dev が認可コードを生成する
- oauth2.sigstore.devの認可コードが手元にコールバックで返ってくる
上の図ではGitHubをIdPとして選択した例になっています。図の2と4は oauth2.sigstore.dev とGitHubが直接通信しているように見えますが、実際にはリダイレクトなのでCosignを経由します。リダイレクトを厳密に書くと図が爆発したので上のように書いていますが、脳内で置換してください。
4で返ってくる認可コードはGitHubのものです。そしてそれを oauth2.sigstore.dev に渡すと oauth2.sigstore.dev の認可コードが生成されてコールバックで手元に返ってきます。実際に自分で使うのは oauth2.sigstore.dev の認可コードだけなので、GitHubの認可コードについては気にする必要はないです。
また、sigstoreのIdPではRFC 7636で定義されているPKCEに対応しています。そのため code_challenge
や code_verifier
というパラメータをリクエストに含める必要があります。この点については後ほどリクエストの生成時に解説します。
IDトークンの取得
認可コードが手に入ったのでこれを使ってToken Endopointに問い合わせIDトークンを取得します。
先程同様、2はGitHubのIDトークンで5は oauth2.sitstore.dev のIDトークンであるという点がややこしいぐらいで、あとは特に複雑なところはないかと思います。
認可コードの取得時と違ってリダイレクトはなかったので oauth2.sigstore.dev が直接GitHubのToken Endpointを叩いていると推察しています。上の図では2にしていますが、どのタイミングでGitHubへのリクエストが発行されているかはわからないです。
手動で試す
@otameshi61さんのブログと重複する部分もありますが、理解を深めるために一通り手動でやっていきます。
OpenIDプロバイダーの情報取得
.well-known/openid-configuration
でOpenIDプロバイダー(IdP)の情報が取得可能なので最初に叩いておきます。このエンドポイントが生えていることからもoauth2.sigstore.dev
がIdPになっていることが分かります。
$ curl https://oauth2.sigstore.dev/auth/.well-known/openid-configuration { "issuer": "https://oauth2.sigstore.dev/auth", "authorization_endpoint": "https://oauth2.sigstore.dev/auth/auth", "token_endpoint": "https://oauth2.sigstore.dev/auth/token", "jwks_uri": "https://oauth2.sigstore.dev/auth/keys", "userinfo_endpoint": "https://oauth2.sigstore.dev/auth/userinfo", "device_authorization_endpoint": "https://oauth2.sigstore.dev/auth/device/code", "grant_types_supported": [ "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code" ], "response_types_supported": [ "code" ], "subject_types_supported": [ "public" ], "id_token_signing_alg_values_supported": [ "RS256" ], "code_challenge_methods_supported": [ "S256", "plain" ], "scopes_supported": [ "openid", "email", "groups", "profile", "offline_access" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "claims_supported": [ "iss", "sub", "aud", "iat", "exp", "email", "email_verified", "locale", "name", "preferred_username", "at_hash" ] }
認可コードの取得
Authorization Endpointの仕様については詳しく説明しません。ただし先程述べたようにPKCEのために code_verifier
や code_challenge
を送る必要があります。詳しくは以下に書かれています。
code_verifierの生成
Authorization Endpointにリクエストを投げるときに code_verifier
が必要なのでまず適当に生成します。43〜128文字なら良いようなので43文字にします。ランダム文字列を作るために pwgen
というコマンドを使っていますが、 opessnl rand
でも何でも良いです。
$ pwgen 43 1 ir5shaejohr1piu8eicei2aipieMeej3al6ou9Chies
code_challengeの生成
次に code_challenge
を生成します。PKCEの仕様として策定されている RFC 7636 によると以下のようなシンプルな方法で code_challenge
は生成可能とのことです。
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
まずSHA256でハッシュ値を計算します。
$ echo -n | sha256sum - dc4db41f5d3f59d43ca16cd19498b6f7827bfa908336dfa0d0834b118d6fd96a
これを16進数表記の文字列として扱い、Base64エンコードします。
$ echo -n 'dc4db41f5d3f59d43ca16cd19498b6f7827bfa908336dfa0d0834b118d6fd96a' | xxd -r -p | base64
URLとして扱えるように =
を削除, +
=> -
, /
=> _
などの置換を行います。
$ echo -n '3E20H10/WdQ8oWzRlJi294J7+pCDNt+g0INLEY1v2Wo=' | sed -e 's/=//g' | sed -e 's/+/-/g' | sed -e 's/\//_/g' 3E20H10_WdQ8oWzRlJi294J7-pCDNt-g0INLEY1v2Wo
ということで code_challenge
を得ました。
一気にやりたければ以下のような感じ。
$ echo -n 'ir5shaejohr1piu8eicei2aipieMeej3al6ou9Chies' | sha256sum - | cut -d " " -f 1 | xxd -r -p | base64 | sed -e 's/=//g' | sed -e 's/+/-/g' | sed -e 's/\//_/g' 3E20H10_WdQ8oWzRlJi294J7-pCDNt-g0INLEY1v2Wo
以下を参考にしました。
PKCEにおけるcode_challenge生成について - Qiita
Authorization Endpointへのアクセス
今回はただ試したいだけなのでstateとnonceは適当に指定してます。実運用するときは当然ランダムな値にしてください。
$ open "https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=3E20H10_WdQ8oWzRlJi294J7-pCDNt-g0INLEY1v2Wo&code_challenge_method=S256&nonce=nonce&redirect_uri=http%3A%2F%2Flocalhost%3A60000%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=state"
open
コマンドを使いましたが普通にブラウザで上記URLにアクセスすれば良いです。 code_challenge
に先ほど生成した値を入れています。
また、 redirect_uri
として http://localhost:60000
を指定しています。ここにcodeが飛んでくるので別ターミナルを開いて適当にncなどで待ち構えておきます。
$ nc -kl 60000
この oauth2.sigstore.dev
もIdPとして動いていますが、上で説明したように実態は他のIdPにリクエストを送りIDトークンを取得後に再度sigstoreのIDトークンを生成しているだけに見えます。
ブラウザで上記のリンクを開くと以下の画面になります。GitHub, Google, MicrosoftのIdPに対応している事がわかります。
"Login with GitHub"をクリックした場合の挙動をcurlで確かめておきます。
$ curl "https://oauth2.sigstore.dev/auth/auth/https:%252F%252Fgithub.com%252Flogin%252Foauth?access_type=online&client_id=sigstore&code_challenge=3E20H10_WdQ8oWzRlJi294J7-pCDNt-g0INLEY1v2Wo&code_challenge_method=S256&nonce=nonce&redirect_uri=http%3A%2F%2Flocalhost%3A60000%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=state" <a href="https://github.com/login/oauth/authorize?client_id=e8bef66f2cde64c23f47&redirect_uri=https%3A%2F%2Foauth2.sigstore.dev%2Fauth%2Fcallback&response_type=code&scope=user%3Aemail&state=zkmdz6gtmla2yymmriekppt7z">Found</a>.
GitHubにリダイレクトされました。 redirect_url
が oauth2.sigstore.dev
になっていてGitHubが認可コードを生成したら oauth2.sigstore.dev
にコールバックされます。そして oauth2.sigstore.dev
がさらに生成した認可コードが最終的にクライアントにコールバックされます。
curlでGitHubの認証しても良いですが、ここは少しサボってブラウザから上記のGitHubのリンクにアクセスします。 "Authorize"ボタンを押すとlocalhostに認可コードが飛んできます。
$ nc -kl 60000 GET /auth/callback?code=d4bfn24c7bepxlz6c2u4sa2ev&state=state HTTP/1.1 Host: localhost:60000 Connection: keep-alive Upgrade-Insecure-Requests: 1
今回だと d4bfn24c7bepxlz6c2u4sa2ev
です。これはIDトークンの取得で一度使うと無効になります。
IDトークンの取得
認可コードが手に入ったので、IDトークンを取得します。基本的には認可コードをToken Endpointに投げるだけです。Basic認証が必要なようなので Authorization
ヘッダを足しています。ユーザ名が sigstore
でパスワードは空のようです。
$ echo -n c2lnc3RvcmU6 | base64 -d sigstore:
code_verifier
には最初に計算した値を入れます。 nonce
は相変わらず適当にしています。
$ curl -X POST https://oauth2.sigstore.dev/auth/token -H "Authorization: Basic c2lnc3RvcmU6" -d "code=bp7gnogke3ea5nqtjwjeotg2t&code_verifier=ir5shaejohr1piu8eicei2aipieMeej3al6ou9Chies&grant_type=authorization_code&nonce=nonce&redirect_uri=http%3A%2F%2Flocalhost%3A60000%2Fauth%2Fcallback" {"access_token":"eyJ...","token_type":"bearer","expires_in":59,"id_token":"eyJ..."}
ということでIDトークンが手に入りました。 expires_in
が59なので有効期限はとても短くなっています。このIDトークンは適当にファイルに保存しておきます。ちなみに上述したように認可コードは一度使って無効になっているので再度curlすると失敗します。
$ echo -n '{"access_token":"eyJ...","token_type":"bearer","expires_in":59,"id_token":"eyJ..."}' > token.jwt
IDトークンの中身は以下のようになっています。IDトークンはヘッダ・ペイロード・署名で構成されておりそれぞれBase64エンコードしたものを .
で連結しただけなのでデコードすれば値が見られます。
■ ヘッダ
{ "alg": "RS256", "kid": "aeefb6c2fc062f48cfa510e209e0493fbff8e596" }
■ ペイロード
{ "iss": "https://oauth2.sigstore.dev/auth", "sub": "CgcyMjUzNjkyEiZodHRwczolMkYlMkZnaXRodWIuY29tJTJGbG9naW4lMkZvYXV0aA", "aud": "sigstore", "exp": 1663697876, "iat": 1663697816, "nonce": "nonce", "at_hash": "60yD_C7sKjaUYa4VBv3_4g", "c_hash": "bqK-Q6w9EaY9CNQ6-Dk4Sw", "email": "[email protected]", "email_verified": true, "federated_claims": { "connector_id": "https://github.com/login/oauth", "user_id": "2253692" } }
■ 署名
peVaJbzFWmzG4ChHu0j1l_2uM9WLcnxmTdPxy9H_b1sFIcjks22Ij4GoTRF6pWGopVJyEE4Cx3Hl6woYDIix92KKXfCPZeimcaXO8y8B8uQUu5PyUTbt5z-hpipdY88Fgj3YGcLnl7Kh5j2aMXJt-f5ETOUdvVjwU4kstFCXs69yy60mPtBO5NdxF4m8Qi4jr2CjZ33hfdUm6jblf5Z3Jvk1KKHq3gXhrh6ZtEiD_fr007GQNKcLHw86uk8xDNsWIJNbmlpUKoutuMEzav0Qxsu8r8Q-x2kKQgNJ1oB7zS3eT8r4lvn0W8uCF0Kz9o-xRU-0KeqG3bFFzX1nyGDoIQ==
IDトークンの検証
せっかくなので入手したIDトークンの検証もしてみます。正直これに関しては自分でも何でこんなこと手動でやってんの?と思いながらやってました。調べても他にやってる人全然見つからなくて手探りでやりました。
まず iss
が https://oauth2.sigstore.dev/auth
であり、 aud
が sigstore
になっていることを確かめます。ここでは目視で終わりとします。
公開鍵の取得
次に署名を検証します。最初に見た .well-known/openid-configuration
内の jwks_uri
で公開鍵を取得可能なので、まず公開鍵一覧を取得してみます。
$ curl https://oauth2.sigstore.dev/auth/keys { "keys": [ { "use": "sig", "kty": "RSA", "kid": "aeefb6c2fc062f48cfa510e209e0493fbff8e596", "alg": "RS256", "n": "1KYfnZ9qUVwg-GVbWe_Idj-OomW5vflcwUvfLYLW6feWfo3oXK-9GYa0AMaMJZWTP888b35Gl9QbU8wIY2A-IAsz0S-0xtbdynL-KkAt6jGdDGqpVgdDj5FltVlh43Qt3m48tSBDfyCJATOicpNrDeEhPw9KOIvS2MK_1K67jfkFTc9oKdL_Viqeej-BP3tA2O07-BlA5R1xhDY0Ia2GiIJv3MyjIuqWtZxQl1L4yYyTKZXnZ9fN9TPrWhtW4vKEUzG2cnkbiqMmlolzq8GNMln0DPyPen6u45VlFQ1RP9NLwWPPpXa2VwdCM-Lk0FtnWB_fghHXZQhdbRvtkJL8Sw", "e": "AQAB" }, { "use": "sig", "kty": "RSA", "kid": "43bcf44f53aa199319ec058910182488dbd11058", "alg": "RS256", "n": "xdV0wU-If4svTGnO1dyDpm0ESfLiOncOglpu6ALlDqFZz5u4IPXInmOiSjHAbDa3_5FfFWurBUgU8K4xBXtz1pp4nKmQVs0c1u5BaTmm7PnHllSeuJyeHLJwspfe9xRApuXaSRpuAc_F9-GaNdIlwmZvxU0OfAtJUb5Ugx_VbeX8r99bO5DFYbiPCNnJTsjiWnmga8zFzOD87ikNZVOSQL28q7lOOa7ehb1bhNsdLviuyFJMfbr359u4I6cW2tgwDDg30oALKUpfu3FVsMRr5SBaWBmQiyx1G9Z2cuNPNtSMZF0hUPLtFbHNKgG7ARCAHLNtcbLGUKPzPcjGPAsaFw", "e": "AQAB" } ] }
2つあります。先程のJWTのヘッダに書いてあった kid
は aeefb6c2fc062f48cfa510e209e0493fbff8e596
だったので1つ目の鍵であることが分かります。
公開鍵の生成
n
と e
があるので m ≡ (signature)^e (mod n)
で署名を復号して検証可能な状態なのですが、CLIだとうまくmodpowする方法が見つかりませんでした。プログラムを書くのであれば以下が参考になります。
OpenID ConnectのIDトークンの内容と検証 - sambaiz-net
何とかCLIだけでやりたいという謎の縛りプレーをしているので、openssl コマンドを使って公開鍵を作ってみます。 asn1parse
の -genconf
フラグで作れそうです。ドキュメントにサンプルもあります。
/docs/manmaster/man3/ASN1_generate_nconf.html
まず n
はBase64エンコードされているので、デコードしてから16進数に変換します。
$ curl -s https://oauth2.sigstore.dev/auth/keys | jq -r '.keys[0].n' | base64 -d ԦjQ\ base64: invalid input
パディングがないのでうまくデコードできませんでした。自分はGNUのbase64使っているせいでエラーになっているのですが、macOSのbase64はパディングがなくても勝手にデコードしてくれます。ただその場合も結局最後の1バイトが欠けてしまいました(少しハマった)。Base64の原理的には行けるはずなのですが、何で欠けるのかは不明です。今回は仕方ないので自分でパディングします。最後に ==
を足します。
$ echo -n "1KYfnZ9qUVwg-GVbWe_Idj-OomW5vflcwUvfLYLW6feWfo3oXK-9GYa0AMaMJZWTP888b35Gl9QbU8wIY2A-IAsz0S-0xtbdynL-KkAt6jGdDGqpVgdDj5FltVlh43Qt3m48tSBDfyCJATOicpNrDeEhPw9KOIvS2MK_1K67jfkFTc9oKdL_Viqeej-BP3tA2O07-BlA5R1xhDY0Ia2GiIJv3MyjIuqWtZxQl1L4yYyTKZXnZ9fN9TPrWhtW4vKEUzG2cnkbiqMmlolzq8GNMln0DPyPen6u45VlFQ1RP9NLwWPPpXa2VwdCM-Lk0FtnWB_fghHXZQhdbRvtkJL8Sw==" | /usr/bin/base64 -d | xxd -p -c0 d4a61f9d9f6a515c20f8655b59efc8763f8ea265b9bdf95cc14bdf2d82d6e9f7967e8de85cafbd1986b400c68c2595933fcf3c6f7e4697d41b53cc0863603e200b33d12fb4c6d6ddca72fe2a402dea319d0c6aa95607438f9165b55961e3742dde6e3cb520437f20890133a272936b0de1213f0f4a388bd2d8c2bfd4aebb8df9054dcf6829d2ff562a9e7a3f813f7b40d8ed3bf81940e51d7184363421ad8688826fdccca322ea96b59c509752f8c98c932995e767d7cdf533eb5a1b56e2f2845331b672791b8aa326968973abc18d3259f40cfc8f7a7eaee39565150d513fd34bc163cfa576b657074233e2e4d05b67581fdf8211d765085d6d1bed9092fc4b
デコードしたものをxxdを使って16進数にしています。
ちなみにBase64の原理については以下の記事で細かく書きました。
ということでopensslの設定ファイルを作ります。e
も同様に取得しておいてください。多分 0x010001
です。
$ cat <<EOF > pub.conf # Start with a SEQUENCE asn1=SEQUENCE:pubkeyinfo # pubkeyinfo contains an algorithm identifier and the public key wrapped # in a BIT STRING [pubkeyinfo] algorithm=SEQUENCE:rsa_alg pubkey=BITWRAP,SEQUENCE:rsapubkey # algorithm ID for RSA is just an OID and a NULL [rsa_alg] algorithm=OID:rsaEncryption parameter=NULL # Actual public key: modulus and exponent [rsapubkey] n=INTEGER:0xd4a61f9d9f6a515c20f8655b59efc8763f8ea265b9bdf95cc14bdf2d82d6e9f7967e8de85cafbd1986b400c68c2595933fcf3c6f7e4697d41b53cc0863603e200b33d12fb4c6d6ddca72fe2a402dea319d0c6aa95607438f9165b55961e3742dde6e3cb520437f20890133a272936b0de1213f0f4a388bd2d8c2bfd4aebb8df9054dcf6829d2ff562a9e7a3f813f7b40d8ed3bf81940e51d7184363421ad8688826fdccca322ea96b59c509752f8c98c932995e767d7cdf533eb5a1b56e2f2845331b672791b8aa326968973abc18d3259f40cfc8f7a7eaee39565150d513fd34bc163cfa576b657074233e2e4d05b67581fdf8211d765085d6d1bed9092fc4b e=INTEGER:0x010001 EOF
あとは asn1parse
で公開鍵を作ります。
$ openssl asn1parse -genconf pub.conf -out idpkey.der 0:d=0 hl=4 l= 290 cons: SEQUENCE 4:d=1 hl=2 l= 13 cons: SEQUENCE 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption 17:d=2 hl=2 l= 0 prim: NULL 19:d=1 hl=4 l= 271 prim: BIT STRING
無事に n
と e
からIdPの公開鍵を作れました。idpkey.der
に書き出しています。
検証
JWT内の署名をBase64デコードしたものを一旦ファイルに書き出します。
$ echo -n "peVaJbzFWmzG4ChHu0j1l_2uM9WLcnxmTdPxy9H_b1sFIcjks22Ij4GoTRF6pWGopVJyEE4Cx3Hl6woYDIix92KKXfCPZeimcaXO8y8B8uQUu5PyUTbt5z-hpipdY88Fgj3YGcLnl7Kh5j2aMXJt-f5ETOUdvVjwU4kstFCXs69yy60mPtBO5NdxF4m8Qi4jr2CjZ33hfdUm6jblf5Z3Jvk1KKHq3gXhrh6ZtEiD_fr007GQNKcLHw86uk8xDNsWIJNbmlpUKoutuMEzav0Qxsu8r8Q-x2kKQgNJ1oB7zS3eT8r4lvn0W8uCF0Kz9o-xRU-0KeqG3bFFzX1nyGDoIQ==" | /usr/bin/base64 -d > token.sig
JWTの署名を除いた部分も同様にファイルに書き出します。これが署名対象です。
echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFlZWZiNmMyZmMwNjJmNDhjZmE1MTBlMjA5ZTA0OTNmYmZmOGU1OTYifQ.eyJpc3MiOiJodHRwczovL29hdXRoMi5zaWdzdG9yZS5kZXYvYXV0aCIsInN1YiI6IkNnY3lNalV6TmpreUVpWm9kSFJ3Y3pvbE1rWWxNa1puYVhSb2RXSXVZMjl0SlRKR2JHOW5hVzRsTWtadllYVjBhQSIsImF1ZCI6InNpZ3N0b3JlIiwiZXhwIjoxNjYzNjk3ODc2LCJpYXQiOjE2NjM2OTc4MTYsIm5vbmNlIjoibm9uY2UiLCJhdF9oYXNoIjoiNjB5RF9DN3NLamFVWWE0VkJ2M180ZyIsImNfaGFzaCI6ImJxSy1RNnc5RWFZOUNOUTYtRGs0U3ciLCJlbWFpbCI6ImtucXlmMjYzQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmZWRlcmF0ZWRfY2xhaW1zIjp7ImNvbm5lY3Rvcl9pZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aCIsInVzZXJfaWQiOiIyMjUzNjkyIn19" > token
あとはIdPの公開鍵(idpkey.der
)を使ってJWTのヘッダとペイロード部分の署名を検証します。
$ openssl dgst -sha256 -verify idpkey.der -signature token.sig token Verified OK
ということで検証できました。
ここまででOpenID Connectを使ってIDトークンを取得するというところは完了です。このあとにFulcioとのやり取りがありますが、それは次の記事で解説します。
参考
- https://otameshi61.hatenablog.com/entry/cosign-attest-internal
- https://www.sambaiz.net/article/136/#id%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%BD%B2%E5%90%8D%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B
- https://qiita.com/nannany_hey/items/b30ac759553661dfb265
- https://zenn.dev/zaki_yama/articles/oauth2-authorization-code-grant-and-pkce
- https://stackoverflow.com/questions/19850283/how-to-generate-rsa-keys-using-specific-input-numbers-in-openssl
- https://www.openssl.org/docs/manmaster/man3/ASN1_generate_nconf.html
まとめ
Keyless Signingについての概要は当然把握していたのですが、実際に手動でやってみると色々と勉強になりますしハマりまくったおかげで忘れなくなったので、大変ですがやってみて良かったです。とりあえず今回はOIDC部分の理解を深めました。