2024年1月9日火曜日

Verifiable Credentialsの署名を壊さずに選択的開示を行うSD-JWTを体感してみる

こんにちは、富士榮です。


少し毛色を変えて今日はみんな大好き選択的開示(Selective Disclosure)です。

特にVerifiable Credentialsを使うユースケースの典型的なシナリオに「バーに入る際の年齢証明に免許証を見せる」というものがあります。しかしながら年齢が21歳以上であることを証明したいだけなのに運転免許証を見せてしまうと住所や氏名などの本来不要な情報まで提示することになってしまいます。そこでSelective Disclosureが大事、という話になってきます。

一見簡単そうに見えますが、署名が施されたデータの中身を改竄する(実際は不要なデータを間引く)ことになるのでデジタル署名の目標である真正性の担保という話と相反することになってしまいます。

これまでもBBS+など仕組みは出てきていますが今日はJSON Web Tokenを対象としたSD-JWTの話をしたいと思います。


簡単な仕組み

従来の署名月JWTは対象となるデータ(JSON)をBase64Urlエンコードして署名アルゴリズムなどの情報をヘッダに入れ、署名を施して".(ピリオド)"で連結したものでした。OpenID ConnectにおけるIDトークンもこのフォーマットですね。

例えば、こんなデータを対象としたいと思います。

{

  "iss": "http://server.example.com",

  "sub": "248289761001",

  "aud": "s6BhdRkqt3",

  "nonce": "n-0S6_WzA2Mj",

  "exp": 1311281970,

  "iat": 1311280970,

  "name": "Jane Doe",

  "given_name": "Jane",

  "family_name": "Doe",

  "gender": "female",

  "birthdate": "0000-10-31",

  "email": "janedoe@example.com",

  "picture": "http://example.com/janedoe/me.jpg"

}

これをRS256で署名してJWTとする場合、ヘッダにはアルゴリズムの情報などを入れます。

{

  "kid": "1e9gdk7",

  "alg": "RS256"

}

最後にヘッダ、ペイロードをそれぞれBase64Urlエンコードしてデジタル署名の値を最後に追加します。(ヘッダ・ペイロード・署名の間を"."で連結します)

すると結果、こんな感じになります。

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJmZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9leGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNnspA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcipR2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2macAAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOYu0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl6cQQWNiDpWOl_lxXjQEvQ

jwt.ioなどのサイトでデコードすると中身がわかります。


一方でSD-JWTではデジタル署名を維持しつつ、このペイロードの中に記載されているクレーム(属性)の一部を隠したいわけです。

そのため、実はSD-JWTで選択的開示をする際はペイロード自体には何の加工もしません(やっぱりペイロードを改竄しちゃうと署名はこわれるので)。その代わりにSD-JWTを作成する際に以下の工夫を施します。

  • 選択的開示の対象となる属性のハッシュをペイロードに入れる
  • 従来のJWTの最後(署名の後)にDisclosureと言われる各属性に対応する値を追加する(~(チルダ)で連結する)

このDisclosureがあればペイロード内のハッシュ化された値を元に戻せるので開示したくない属性についてはDisclosureを削除してしまうことで受け取り側は値を知ることができなくなる、という仕組みです。

先ほどのJWTのペイロードをSD-JWTにする場合はこんな感じになります。要するに隠す可能性がある属性について"_sd"という要素にハッシュ化した値を入れる感じです。

{

  "iss": "http://server.example.com",

  "aud": "s6BhdRkqt3",

  "nonce": "n-0S6_WzA2Mj",

  "iat": 1516239022,

  "exp": 1735689661,

  "_sd": [

    "5G_krZtMTMPltAWWNMhdDxJMt15SA1KFkd2gIlSvLtQ",

    "5p6bt53D9p4MfkVYQciL4pWXAkIjz0PbLXGD0NHii2w",

    "6Jt5lxreiTK8olTRzyHoHYYPmu0G-ZVvWpacVmYYT8M",

    "Ew-dKRmA2uSgw9EmGwNIfQksPpPIs0qA4OE-86DQ3_Y",

    "Hz-BYPQJmHsFlIfZ_9CJ6IboqisoZNtwzkb0Z3JAlF4",

    "J3P_gNA3LECIzTttBuX62cLVICDFzR-rroiCQkbBbuQ",

    "O2bDUHaELZIsyOXTyutWunAgm37QTkbCTRw73bi55xE",

    "Q4vtraDvprqBkjrPoLW_9FZDetFKKAGcya05rIwq4xg"

  ],

  "_sd_alg": "sha-256"

}

そしてこのペイロードを通常通りデジタル署名付きのJWTにしたものがこちらの文字列です。

eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MzU2ODk2NjEsIl9zZCI6WyI1R19rclp0TVRNUGx0QVdXTk1oZER4Sk10MTVTQTFLRmtkMmdJbFN2THRRIiwiNXA2YnQ1M0Q5cDRNZmtWWVFjaUw0cFdYQWtJanowUGJMWEdEME5IaWkydyIsIjZKdDVseHJlaVRLOG9sVFJ6eUhvSFlZUG11MEctWlZ2V3BhY1ZtWVlUOE0iLCJFdy1kS1JtQTJ1U2d3OUVtR3dOSWZRa3NQcFBJczBxQTRPRS04NkRRM19ZIiwiSHotQllQUUptSHNGbElmWl85Q0o2SWJvcWlzb1pOdHd6a2IwWjNKQWxGNCIsIkozUF9nTkEzTEVDSXpUdHRCdVg2MmNMVklDREZ6Ui1ycm9pQ1FrYkJidVEiLCJPMmJEVUhhRUxaSXN5T1hUeXV0V3VuQWdtMzdRVGtiQ1RSdzczYmk1NXhFIiwiUTR2dHJhRHZwcnFCa2pyUG9MV185RlpEZXRGS0tBR2N5YTA1ckl3cTR4ZyJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.LAlXQnPK8rOhxBhbg_FU-hiBJJqCX5CYWYTp0YcbbIxnBZPN1ZhRgE_SLh0rEg1L559GxmG1WrpGMyOl1rDoAA

このJWTに"_sd"に入れた各属性に対応するDisclosureを~で連結して追加していきます。赤字の部分が追加したものです。

eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MzU2ODk2NjEsIl9zZCI6WyI1R19rclp0TVRNUGx0QVdXTk1oZER4Sk10MTVTQTFLRmtkMmdJbFN2THRRIiwiNXA2YnQ1M0Q5cDRNZmtWWVFjaUw0cFdYQWtJanowUGJMWEdEME5IaWkydyIsIjZKdDVseHJlaVRLOG9sVFJ6eUhvSFlZUG11MEctWlZ2V3BhY1ZtWVlUOE0iLCJFdy1kS1JtQTJ1U2d3OUVtR3dOSWZRa3NQcFBJczBxQTRPRS04NkRRM19ZIiwiSHotQllQUUptSHNGbElmWl85Q0o2SWJvcWlzb1pOdHd6a2IwWjNKQWxGNCIsIkozUF9nTkEzTEVDSXpUdHRCdVg2MmNMVklDREZ6Ui1ycm9pQ1FrYkJidVEiLCJPMmJEVUhhRUxaSXN5T1hUeXV0V3VuQWdtMzdRVGtiQ1RSdzczYmk1NXhFIiwiUTR2dHJhRHZwcnFCa2pyUG9MV185RlpEZXRGS0tBR2N5YTA1ckl3cTR4ZyJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.LAlXQnPK8rOhxBhbg_FU-hiBJJqCX5CYWYTp0YcbbIxnBZPN1ZhRgE_SLh0rEg1L559GxmG1WrpGMyOl1rDoAA~WyJ5OWVMRmI3azdXZnd4ZkllQWp5eUt3Iiwic3ViIiwiMjQ4Mjg5NzYxMDAxIl0~WyJUNkVlZzRsaVJyN3VtOUZxOFRYekR3IiwibmFtZSIsIkphbmUgRG9lIl0~WyJUUnRocWdUc29kZzVJbnBBR2ZvNjJnIiwiZ2l2ZW5fbmFtZSIsIkpvbmUiXQ~WyJmRWx5VzFIdlVjX1dYTnFLeUdRcEJ3IiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI1blgwRmZTNHUtVjN3aFNORUpPQndnIiwiZ2VuZGVyIiwiZmVtYWxlIl0~WyJhSzhJYWc2dzB1Mno1WVpxMWlXOC13IiwiYmlydGhkYXRlIiwiMDAwMC0xMC0zMSJd~WyIwR0pLdnN2aDNpbnFkeGlUM3MxbVJRIiwiZW1haWwiLCJqb25lZG9lQGV4YW1wbGUuY29tIl0~WyJXMHpCa1lfLThqUVhNY3JnYTJTak9BIiwicGljdHVyZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyJd

これでSD-JWTが出来上がりました。

これを渡されたVerifierなどはDisclosureの値を使ってペイロード内の"_sd"の値を復号して実際の属性値を取得する、ということをやります。また、開示したくない値があれば対応するDisclosureの値を消した状態でSD-JWTを渡せば良い、という理屈です。


体感してみる

といっても細かい実装の話をするのは疲れるので世の中に出てきているツールを触って体感してみたいと思います。

今回はこのツールを使ってみます。

https://github.com/christianpaquin/sd-jwt

詳細はREADMEに書いてある通りなのですが、取り合えずやってみましょう。

ざっくりこんな手順です。

  1. SD-JWTの署名に使う鍵を生成する
  2. jwt(Disclose対象じゃない部分)のjsonを用意する
  3. sd(Disclose対象としたい部分)のjsonを用意する


まずは鍵を生成します。

npm run generate-issuer-keys -- -k <jwksPath> -p <privatePath> -a <keyAlg>

- jwksPathは公開鍵のファイル名です(なければ作ってくれます)

- privatePathは秘密鍵のファイル名です(同上)

- keyAlgはアルゴリズムです。指定しないとES256になります


鍵の準備ができたらSD-JWTを作ってみます。まずはDisclose対象じゃない部分のJSONを用意します。今回はこんなファイルを用意しました。(先ほどの例の個人属性以外の部分ですね)

  {

    "iss": "http://server.example.com",

    "aud": "s6BhdRkqt3",

    "nonce": "n-0S6_WzA2Mj",

    "iat": 1516239022,

    "exp": 1735689661

  }

そして、Disclose対象の部分のJSONです。(先ほどの例の個人属性部分です)

{

    "sub": "248289761001",

    "name": "Jane Doe",

    "given_name": "Jone",

    "family_name": "Doe",

    "gender": "female",

    "birthdate": "0000-10-31",

    "email": "jonedoe@example.com",

    "picture": "http://example.com/janedoe/me.jpg"

  }

ではSD-JWTを生成してみます。

npm run create-sd-jwt -- -k {作成した秘密鍵} -t {作成したDisclose対象外のJSON} -h sha-256 -c {作成したDisclose対象のJSON} -o {出力したいSD-JWT}

こんな感じの書式です。では実際に生成してみます。

npm run create-sd-jwt -- -k private.jwk -t jwt.json -h sha-256 -c sdClaimsFlat.json -o sd-jwt.json

こちらが実行時の出力です。

% npm run create-sd-jwt -- -k private.jwk -t jwt.json -h sha-256 -c sdClaimsFlat.json -o sd-jwt.json

> sd-jwt@1.0.0 create-sd-jwt
> ts-node --files src/create-sd-jwt-cl.ts -k private.jwk -t jwt.json -h sha-256 -c sdClaimsFlat.json -o sd-jwt.json

Creating SD-JWT from the JWT jwt.json using the private key private.jwk, encoding selectively-disclosable claims from sdClaimsFlat.json

JWT: {"iss":"http://server.example.com","aud":"s6BhdRkqt3","nonce":"n-0S6_WzA2Mj","iat":1516239022,"exp":1735689661,"_sd":["5G_krZtMTMPltAWWNMhdDxJMt15SA1KFkd2gIlSvLtQ","5p6bt53D9p4MfkVYQciL4pWXAkIjz0PbLXGD0NHii2w","6Jt5lxreiTK8olTRzyHoHYYPmu0G-ZVvWpacVmYYT8M","Ew-dKRmA2uSgw9EmGwNIfQksPpPIs0qA4OE-86DQ3_Y","Hz-BYPQJmHsFlIfZ_9CJ6IboqisoZNtwzkb0Z3JAlF4","J3P_gNA3LECIzTttBuX62cLVICDFzR-rroiCQkbBbuQ","O2bDUHaELZIsyOXTyutWunAgm37QTkbCTRw73bi55xE","Q4vtraDvprqBkjrPoLW_9FZDetFKKAGcya05rIwq4xg"],"_sd_alg":"sha-256"}

JWS payload: 7B22697373223A22687474703A2F2F7365727665722E6578616D706C652E636F6D222C22617564223A227336426864526B717433222C226E6F6E6365223A226E2D3053365F577A41324D6A222C22696174223A313531363233393032322C22657870223A313733353638393636312C225F7364223A5B2235475F6B725A744D544D506C744157574E4D686444784A4D7431355341314B466B643267496C53764C7451222C2235703662743533443970344D666B56595163694C34705758416B496A7A3050624C584744304E4869693277222C22364A74356C78726569544B386F6C54527A79486F485959506D7530472D5A567657706163566D595954384D222C2245772D644B526D41327553677739456D47774E4966516B735070504973307141344F452D38364451335F59222C22487A2D425950514A6D4873466C49665A5F39434A3649626F7169736F5A4E74777A6B62305A334A416C4634222C224A33505F674E41334C4543497A5474744275583632634C56494344467A522D72726F6943516B6242627551222C224F326244554861454C5A4973794F585479757457756E41676D333751546B62435452773733626935357845222C225134767472614476707271426B6A72506F4C575F39465A446574464B4B4147637961303572497771347867225D2C225F73645F616C67223A227368612D323536227D

JWS: eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwiYXVkIjoiczZCaGRSa3F0MyIsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MzU2ODk2NjEsIl9zZCI6WyI1R19rclp0TVRNUGx0QVdXTk1oZER4Sk10MTVTQTFLRmtkMmdJbFN2THRRIiwiNXA2YnQ1M0Q5cDRNZmtWWVFjaUw0cFdYQWtJanowUGJMWEdEME5IaWkydyIsIjZKdDVseHJlaVRLOG9sVFJ6eUhvSFlZUG11MEctWlZ2V3BhY1ZtWVlUOE0iLCJFdy1kS1JtQTJ1U2d3OUVtR3dOSWZRa3NQcFBJczBxQTRPRS04NkRRM19ZIiwiSHotQllQUUptSHNGbElmWl85Q0o2SWJvcWlzb1pOdHd6a2IwWjNKQWxGNCIsIkozUF9nTkEzTEVDSXpUdHRCdVg2MmNMVklDREZ6Ui1ycm9pQ1FrYkJidVEiLCJPMmJEVUhhRUxaSXN5T1hUeXV0V3VuQWdtMzdRVGtiQ1RSdzczYmk1NXhFIiwiUTR2dHJhRHZwcnFCa2pyUG9MV185RlpEZXRGS0tBR2N5YTA1ckl3cTR4ZyJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.LAlXQnPK8rOhxBhbg_FU-hiBJJqCX5CYWYTp0YcbbIxnBZPN1ZhRgE_SLh0rEg1L559GxmG1WrpGMyOl1rDoAA~WyJ5OWVMRmI3azdXZnd4ZkllQWp5eUt3Iiwic3ViIiwiMjQ4Mjg5NzYxMDAxIl0~WyJUNkVlZzRsaVJyN3VtOUZxOFRYekR3IiwibmFtZSIsIkphbmUgRG9lIl0~WyJUUnRocWdUc29kZzVJbnBBR2ZvNjJnIiwiZ2l2ZW5fbmFtZSIsIkpvbmUiXQ~WyJmRWx5VzFIdlVjX1dYTnFLeUdRcEJ3IiwiZmFtaWx5X25hbWUiLCJEb2UiXQ~WyI1blgwRmZTNHUtVjN3aFNORUpPQndnIiwiZ2VuZGVyIiwiZmVtYWxlIl0~WyJhSzhJYWc2dzB1Mno1WVpxMWlXOC13IiwiYmlydGhkYXRlIiwiMDAwMC0xMC0zMSJd~WyIwR0pLdnN2aDNpbnFkeGlUM3MxbVJRIiwiZW1haWwiLCJqb25lZG9lQGV4YW1wbGUuY29tIl0~WyJXMHpCa1lfLThqUVhNY3JnYTJTak9BIiwicGljdHVyZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyJd

SD-JWT written to sd-jwt.json

出来上がったJWSがsd-jwt.jsonに書き込まれています。

jwt.ioでデコードしてみるとちゃんとSD-JWTになっていることがわかります。

(ちなみにjwt.ioではDisclosure部分は見えません)



次は一部の属性を取り除いた状態で検証できるかどうかの確認です。

出力されたsd-jwt.jsonのDisclosureを一部取り除いてみましょう。なお、Disclosureは上から順番に対象の属性と対応しているので何番目の属性を隠すのかでどのDisclosureを削除すれば良いのかが決まります。

例えば今回7番目の属性がemailなので7番目のDisclosureを削除してみます。ちなみに私はMacのTerminalのviでファイルの編集をしたのですが、保存をするとLine terminator(0a)が行末に追加されてしまうので、awkで削る必要がありました。

awk '{printf("%s", $0)}' sd-jwt.json > user-sd-jwt.json

こんな感じです。

% file user-sd-jwt.json 

user-sd-jwt.json: ASCII text, with very long lines (1260), with no line terminators

fileコマンドで確認するとwith no line terminatorsとなっているのでこれで準備完了です。


早速ですが検証をするためのコマンドを叩きます。

npm run verify-sd-jwt -- -t {インプットとなるSD-JWTファイル} -k {公開鍵} -o {出力されるJWT} -d {開示された属性}

という書式です。

実際に実行するとこんな感じになり、email属性が消えていることがわかります。

% npm run verify-sd-jwt -- -t user-sd-jwt.json -k publicKey.jwks -o outJwt.json -d disclosedClaims.json

> sd-jwt@1.0.0 verify-sd-jwt
> ts-node --files src/verify-sd-jwt-cl.ts -t user-sd-jwt.json -k publicKey.jwks -o outJwt.json -d disclosedClaims.json

Verifying SD-JWT from user-sd-jwt.json
payload: {"iss":"http://server.example.com","aud":"s6BhdRkqt3","nonce":"n-0S6_WzA2Mj","iat":1516239022,"exp":1735689661,"_sd":["10nhdtWZB3RXMn2AxXsK3eJJFf2T0UosLfFVYDg6nN8","2OFg4dmWIuCvX1O7XvvNEbiR30setroa4Y0_JXLUyBY","2nB9Dno76pMtIo3EUWo-4ckypEhW2MM4fXXLLi5use4","9x8lAtlY7dhnojZ2Qv8HRH5JO7oW3PnPK8Z6xe79Y-w","AclFaDd78h1fnbxZ4mtqRMdmQxrDxH9QhEO1QgT0FME","D3B9GFLw9Zn8-7JLWo9WXlrR04OtAPCFiUPPL9GYDNg","ZA-Bh-ucVIOm9RjmV6El7ym7CQ_gwe5ACDzeV7KSGx0","bnqv7hEmIi_3WWLFhwPSAEsoMU0T2rTFZdcmYAnBKxA"],"_sd_alg":"sha-256"}

disclosedClaims: {"sub":"248289761001","name":"Jane Doe","given_name":"Jone","family_name":"Doe","gender":"female","birthdate":"0000-10-31","picture":"http://example.com/janedoe/me.jpg"}

JWT written to outJwt.json
JWT written to disclosedClaims.json


ということでみんな大好きなVerifiable CredentialsとSelective Disclosureでした。


0 件のコメント: