knqyf263's blog

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

curlでKeyless Signingする (1) - OpenID Connect編

確実に忘れるであろう将来の自分と、Keyless Signingに異常な興味を持つ日本に数人しかいないであろう人達のための記事です。

背景

以前sigstoreのソフトウェア署名についてブログを書きました。

knqyf263.hatenablog.com

その中で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本立て(予定)にしています。

前提

@otameshi61 さんによる以下の素晴らしいブログをベースに書いているので、まず最初にこちらに目を通してください。

otameshi61.hatenablog.com

ソフトウェア署名については上の拙作ブログを参照してください。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の流れはざっくり以下です。

  1. OpenID Connectで署名者のIDトークンを取得
  2. 鍵ペアを生成し、ルート認証局であるFulcioで署名してもらって証明書を得る
    • このとき1で作ったIDトークンも一緒に送る
  3. 2で作った秘密鍵を使ってソフトウェア等に署名する
  4. Rekorに署名と証明書を保存する
  5. (2で作った鍵ペアや証明書を破棄する)

Cosign CLI を使ってKeyless Signingを使う場合のフローを図にすると以下です。

まず概要を掴むということで細かいフローを省いているので厳密でない箇所が複数あります。各要素の詳細な内容に関しては今後深堀りしていくので、この図はあくまで雰囲気を理解するためのものという点だけ注意してください。

このKeyless Signingにはいくつか特徴的な点があります。

  1. OpenID Connectで署名者の認証を行う
  2. 有効期限の短いIDトークンや証明書を使う
  3. 鍵ペアを保存しておく必要がない

Fulcioで証明書を発行する際、IDトークンも同時に送ることで誰が署名したのか?というのを検証します。その署名者のアイデンティティ(メールアドレスなど)は証明書のX.509拡張に含まれ改ざんできないようになります。

また、この生成された鍵ペアは1回使ったらすぐ破棄します。つまりソフトウェアに署名するときは毎回異なる鍵ペアが使われるということです。ソフトウェアのメンテナが鍵を保管する必要がないというのがKeylessと呼ばれる所以です。内部では鍵を使っています。

さらに証明書の有効期限が短く設定されているため、攻撃者に鍵ペアや証明書が後ほど盗まれても再度署名することは出来ません。これはRekorに登録されたTransparency Logの時間を見るのですが、細かい話なので別の記事にします。IDトークンの有効期限も短いので攻撃者がIDトークンを盗んで再度Fulcioで証明書を発行しようとしても弾かれます。

とにかく全てを使い捨てにして証明書や署名のログをRekorに残しておくことで鍵の管理を不要にしようというのがKeyless Signingの基本アイディアかなと思います。概要だけ知りたい人はここまでで十分かもしれません。

この記事では上記のうちOpenID Connect(OIDC)のフローを手動で確かめます。

OIDCのフロー

上では簡単にOIDCでIDトークンを取得、と書きましたが細かくは2つのステップがあります。

  1. 認可コードの取得
  2. IDトークンの取得

これは普通のOIDCのCode Grantフローなので、そもそもOIDCって何?という場合は以下などを見ると理解できると思います。

qiita.com

qiita.com

認可コードの取得

Cosign CLIによる認可コードの取得部分の詳細は以下のようになっています。少しややこしいのはアイデンティティプロバイダ(IdP)が2つ登場する点です。こういうIdPのリレーというかプロキシみたいなものは一般的なのかよく分かりませんでした。有識者によるコメントをお待ちしています。

  1. IdPである oauth2.sigstore.dev のAuthorization Endpointにアクセス
  2. GitHub/Google/Microsoftの中からIdPを選びリダイレクトされる
  3. ユーザが選択したIdPで認証・認可を行う
  4. 選択したIdPから認可コードがコールバックで返ってくるので oauth2.sigstore.dev に送る
  5. oauth2.sigstore.dev が認可コードを生成する
  6. 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_challengecode_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-configurationOpenIDプロバイダー(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_verifiercode_challenge を送る必要があります。詳しくは以下に書かれています。

qiita.com

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&amp;redirect_uri=https%3A%2F%2Foauth2.sigstore.dev%2Fauth%2Fcallback&amp;response_type=code&amp;scope=user%3Aemail&amp;state=zkmdz6gtmla2yymmriekppt7z">Found</a>.

GitHubにリダイレクトされました。 redirect_urloauth2.sigstore.dev になっていてGitHubが認可コードを生成したら oauth2.sigstore.dev にコールバックされます。そして oauth2.sigstore.dev がさらに生成した認可コードが最終的にクライアントにコールバックされます。

curlGitHubの認証しても良いですが、ここは少しサボってブラウザから上記の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": "knqyf263@gmail.com",
  "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トークンの検証もしてみます。正直これに関しては自分でも何でこんなこと手動でやってんの?と思いながらやってました。調べても他にやってる人全然見つからなくて手探りでやりました。

まず isshttps://oauth2.sigstore.dev/auth であり、 audsigstore になっていることを確かめます。ここでは目視で終わりとします。

公開鍵の取得

次に署名を検証します。最初に見た .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のヘッダに書いてあった kidaeefb6c2fc062f48cfa510e209e0493fbff8e596 だったので1つ目の鍵であることが分かります。

公開鍵の生成

ne があるので m ≡ (signature)^e (mod n) で署名を復号して検証可能な状態なのですが、CLIだとうまくmodpowする方法が見つかりませんでした。プログラムを書くのであれば以下が参考になります。

OpenID ConnectのIDトークンの内容と検証 - sambaiz-net

何とかCLIだけでやりたいという謎の縛りプレーをしているので、openssl コマンドを使って公開鍵を作ってみます。 asn1parse-genconf フラグで作れそうです。ドキュメントにサンプルもあります。

/docs/manmaster/man3/ASN1_generate_nconf.html

まず nBase64エンコードされているので、デコードしてから16進数に変換します。

$ curl -s https://oauth2.sigstore.dev/auth/keys | jq -r '.keys[0].n' | base64 -d
ԦjQ\ base64: invalid input

パディングがないのでうまくデコードできませんでした。自分はGNUbase64使っているせいでエラーになっているのですが、macOSbase64はパディングがなくても勝手にデコードしてくれます。ただその場合も結局最後の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の原理については以下の記事で細かく書きました。

knqyf263.hatenablog.com

ということで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

無事に ne から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とのやり取りがありますが、それは次の記事で解説します。

参考

まとめ

Keyless Signingについての概要は当然把握していたのですが、実際に手動でやってみると色々と勉強になりますしハマりまくったおかげで忘れなくなったので、大変ですがやってみて良かったです。とりあえず今回はOIDC部分の理解を深めました。