qredential

SD-JWT · Token Status List · base45

答えはすでにコードのにある

自分自身を証明する資格情報。署名、有効期限、失効状態、クレームのすべてを、端末から一度もリクエストを出さずに確認します。

TypeScript、実行時依存ゼロ、WebCrypto のみ。Node、ブラウザ、React Native に対応します。検証する側はたいてい携帯電話だからです。

デモ用の資格情報に署名しています…
 
 
ネットワーク要求· 検証所要時間·

これが実際にしていること

クレデンシャルとは、誰かが署名した一文です。「この人は18歳以上である」「この人は車を運転できる」。その一文に価値を与えるのが署名です。署名は、ある機関がその内容を保証していること、そして後から誰も書き換えていないことを示します。

紙でもPDFでも、署名は文書全体にかかります。たった一行を証明するために、全部を渡すことになります。年齢を示すためだけに運転免許証を見せると、相手は住所も免許番号も正確な生年月日も知ることになります。相手は何も求めていません。それでも手に入ってしまいます。

選択的開示はその取引をなくします。発行者は一つひとつの属性を別々に署名し、クレデンシャルはそれぞれの指紋だけを運びます。開示する属性はあなたが選び、残りは中身を何も語らない指紋のままです。署名は分割できない一塊にかけられたものではないので、そのまま検証を通ります。

これらはすべて、検証者が読み取るコードの中に収まります。署名も、指紋も、あなたが選んで見せた属性もです。検証中に何も取りに行かないため、ドアでも、バスの中でも、地方の診療所でも、巡航中の機内でも、ネットワークなしで確認できます。

最後にもう一つ、見落とされがちな部分があります。読み取れるものは撮影もできます。だから提示する人は、端末から出ることのない秘密鍵で、その場のチャレンジに署名する必要があります。この手順がなければ、他人のクレデンシャルのスクリーンショットでも通ってしまいます。このライブラリは、それを欠いた提示を必ず拒否します。

電波がない瞬間のために作られています

私は eCNH、ブラジルのデジタル運転免許証を作りました。4000 万人以上が使っています。いちばん多くを教えてくれたのはアプリではありません。路上でした。電波が一本、あるいは圏外の幹線道路で免許証を読み取り、一秒以内に可否を出す必要がある現場です。

その答えに必要なものはすべてコードに収まります。署名が発行者を証明し、クレームはその場にあり、外から必要なのは発行者の公開鍵だけです。それはめったに変わらないので、アプリに同梱して週に一度更新すれば済みます。

このためのライブラリは存在します。ただし企業向け SDK です。重く、ひとつの国のプロファイルに縛られ、読者がすでに識別情報業界で働いている前提で書かれています。これは、製品エンジニアが火曜日に追加できる版です。

三者、そのどれもサーバーではありません

01 · 発行

各クレームに一度だけ署名する

発行者は後から伏せてよいクレームを決め、そのそれぞれにダイジェストを署名します。

02 · 提示

求められたものだけ送る

ウォレットは手元に残すクレームを落とします。残ったものに対して発行者の署名は依然として有効です。

03 · 検証

誰にも尋ねずに答える

署名、有効期限、失効、そしてすべての開示を、端末に固定された鍵と照合します。

生年月日を渡さずに 18 歳以上だと証明する

年齢確認の法規制は道具より速く到着しています。ありがちな実装は、客に身分証の写真を第三者へアップロードさせます。それはプライバシーの惨事であり、起きるのを待っているだけの漏えいです。

選択的開示はこれを正しくやります。検証者は望んでも生年月日を知りえません。その値はウォレットを出ていないからです。この性質は暗号的なものであり、プライバシーポリシー上の約束ではありません。

// 資格情報には氏名、住所、生年月日、書類番号が入っています。
// 店が受け取るのは真偽値ひとつです。
const presentation = await present(credential, { disclose: ['over_18'] })

const result = await verify(presentation, { trust })
result.claims             // { over_18: true }
result.claims.birth_date  // undefined。送信されたことはありません
result.withheld           // 4。ただしどの四つかは分かりません

実測値、居心地の悪いものも含めて

現実的な運転免許証。クレーム八つ、有効期間五年、ステータスリストへのポインタ。推定ではなく実測です。

資格情報文字数QR バージョン
すべて可視~74018、問題なく読める
八つすべて開示可能~159027、密すぎる
presenting only over_18~111522、まだ密
選択的開示は資格情報をほぼ倍にします。 開示可能なクレームごとにソルトと署名済みダイジェストがかかり、しかもダイジェストは保持者がそのクレームを見せるかどうかに関わらずペイロードに残ります。これは意図的です。見せた内容に応じてダイジェストの個数が減れば、何を伏せたかが漏れてしまうからです。その結果、提示時の節約はここでは三割であって、直感が約束する八割ではありません。開示可能にするのは二つか三つにとどめ、何かを印刷する前に fits() に現在地を教えてもらってください。

私の言葉を信じないでください

プレイグラウンドはライブラリ全体をブラウザで動かし、実際の攻撃を八つ投げつけます。それぞれが期待する拒否コードを表示するので、README を信じる代わりに、ライブラリを自らの主張と突き合わせて確かめられます。

ペイロードのクレームを書き換える
bad_signature
発行されていないクレームを捏造する
digest_mismatch
同じ開示を二度送る
digest_mismatch
違う鍵で署名する
bad_signature
別の当局を名乗る
unknown_issuer
十年後に使う
expired
失効後に使う
revoked
古いステータスリストに隠れる
status_list_stale
記録した提示を再生する
holder_proof_invalid
他人のコードの写真を提示する
holder_proof_missing

これを選ぶ理由と、選ばない場合

選ぶ理由を五つ、そのあとに正直な話を。

  • 読み通せる約1,300行エンジニア一人が午後のうちに全部読み、何をしているか把握できる大きさです。読めないセキュリティは、信じるしかないセキュリティです。
  • 依存関係ゼロ実行時にnpmから何も持ってきません。監査すべきサプライチェーンはこれ一つだけで、来週いつのまにか変わるものもありません。
  • 完全に確認できないものは拒否する部分的な合格も、無視できる警告もありません。失敗はすべて、記録して対処できる名前付きの理由として返ります。素っ気ないfalseでは返しません。
  • 検証が行われる場所で動くNode、Deno、Bun、ブラウザ、React Native、エッジワーカー。どこでも同じコードが、プラットフォームに元から入っている標準の暗号の上で動きます。
  • 三通りのテストユニットテスト、RFC 9901とともに公開された公式テストベクター、そしてランダムな不正入力をぶつけて抜け穴を探すプロパティテストです。

そして、これが適さない場合。

  • ISO 18013-5のモバイル運転免許証が必要な場合あちらはCBORとCOSEという別のエンコーディングで、標準そのものが違います。このライブラリが扱うのはSD-JWTで、欧州のウォレットとOpenIDの仕様が使う形式です。近い課題ですが、仕事が違います。
  • ウォレットそのものが欲しい場合これは検証と提示を行います。クレデンシャルの保管も、端末管理も、オンボーディングもしません。あなたの製品の中に収まる部品です。
  • 規制下の導入に第三者監査が必要な場合まだありません。テストもRFCのテストベクターも、そして全行が公開されていますが、規制下で使うなら自前のレビュー費用を見込んでください。

インストール

npm i qredential

Node 20 以降、現行のあらゆるブラウザ、React Native。実行時依存なしでおよそ 1,300 行。信頼する前に読むことが現実的であるように、あえてそうしてあります。

発明ではなく、規格

  • SD-JWT, RFC 9901選択的開示と鍵バインディング、入れ子と再帰にも対応。欧州のアイデンティティウォレットが使う仕組みです
  • SD-JWT VC資格情報の形
  • Token Status Listキャッシュした写しから動く失効確認
  • base45, RFC 9285QR のエンベロープ。読み取り機との相性で選びました

ISO 18013-5 mDL ではありません。あちらは JWT ではなく CBOR と COSE です。予定には入っており、未対応のものを使う資格情報は半分だけ理解されるのではなく拒否されます。適合規格を半分だけ満たしたと称するのは、何も称さないより悪いことです。