DID とは何か

DID(Decentralized Identifier、分散識別子)は、W3C が標準化した識別子の書式です。形はこうなっています。

did:web:nakamura196.github.io:did-log
~~~ ~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 │   │            └ その方式ごとの中身
 │   └ メソッド名(どうやって鍵に辿り着くか)
 └ 固定の接頭辞

真ん中のメソッド名が全てを決めます。「この識別子から公開鍵をどう取ってくるか」という手続きが、メソッドごとに違うからです。

「分散」と名前に入っていますが、分散台帳が必須なわけではありません。実際、以下の3方式のうちブロックチェーンを使うものはひとつもありません。

3方式を並べる

試作1では3つの方式を実装して、同じ証明書を同じコードで検証できるようにしました。

did:keydid:webdid:webvh
解決の方法識別子を復号するだけhttps://…/did.json を取得did.jsonl を取得し連鎖を検証
ホスト不要1つ(自機関)1つ(自機関)
CA への依存なしあり(TLS 証明書)あり(TLS 証明書)
鍵の更新できないできる(過去は消える)できる(過去も辿れる
失効できないできるできる
「これは例示大学だ」の保証誰もしないドメインの所有ドメインの所有
過去の書き換えの検知できないできる(限界あり)

実装上、3方式で違うのは resolve(did) → DID文書 という1つの関数だけでした。署名の検証も証明書の組み立ても完全に共通で、上位のコードは1行も変わりません。

did:key — 識別子そのものが鍵

一番単純な方式です。

did:key:zDnaeWNNgXuukD9gLno2NXJRm9UR176khLx9zx82DZzPXCwvq

この文字列を Base58 という方式で復号すると、公開鍵のバイト列がそのまま出てきます。だから解決にネットワークもホストも要りません。オフラインで完結します。

[→] did:key   (ホスト不要 / CA不要 / 鍵の更新も失効も不可)
  did:key:zDnaeWNNgXuukD9gLno2NXJRm9UR176khLx9zx82DZzPXCwvq
  公開鍵 x=WDqGHl1w2d-KvldG… y=7ke4ACW8Yz2Gbum5…
  信用しているもの:
    - なし (識別子そのものが鍵。ネットワークもホストも不要)

「信用しているもの: なし」と出ています。これは本当に何も信用していません。代償として、この鍵が例示大学のものだと誰も保証しません。

さらに、鍵の更新も失効もできません。識別子と鍵が一体なので、鍵を変えれば識別子も変わってしまうからです。

識別子は、鍵からどう作られるのか

「識別子そのものが鍵」と言われても、did:key:zDnae… という文字列と公開鍵がどう繋がるのかは見えません。1段ずつ、実際の値で追ってみます。以下は試作の中村の公開鍵から作ったものです。

1. 公開鍵は楕円曲線上の点で、x と y の座標を持ちます(DID Document に載っている形)

x = r7eLJv-0TUPv0-xhS0FtqI6qU_KfCmZmDlz2dISvDz8
y = NmmKkFEdmUfdgCeO3RPMKnlMVCaZA-xLiLAlFuWGVPE

2. それぞれ 32 バイトの数です

x = afb78b26ffb44d43efd3ec614b416da88eaa53f29f0a66660e5cf67484af0f3f
y = 36698a90511d9947dd80278edd13cc2a794c54269903ec4b88b02516e58654f1

3. y を捨てて 33 バイトに圧縮します。 曲線の式 y² = x³ − 3x + b から、x が決まれば y は2つの候補に絞れます。どちらかを示すには偶奇が分かれば足りるので、1バイトの接頭辞(偶数なら 02、奇数なら 03)に畳みます。上の y は末尾が f1(十進 241)で奇数なので 03 です。

03 afb78b26ffb44d43efd3ec614b416da88eaa53f29f0a66660e5cf67484af0f3f
└┘
偶奇

4. 「これは P-256 の公開鍵だ」と示す接頭辞を足します。 multicodec という登録簿で P-256 公開鍵は 0x1200 と決まっています。これを varint(可変長整数)で符号化すると 2 バイトの 80 24 になります。

8024 03afb78b26ffb44d43efd3ec614b416da88eaa53f29f0a66660e5cf67484af0f3f
└──┘
multicodec

5. 全体を base58btc で符号化し、multibase の接頭辞 z を付けます

did:key:zDnaeuVD6Dd2sFQvPQtW12TZjzCTafDPyrtygfTHUHHzTepop

zDnae で始まるのは、8024 という先頭バイト列が base58 でこの並びになるためです(P-256 の鍵は必ずこの形になります)。

解決はこれを逆にたどるだけです。base58 を復号し、先頭が 8024 であることを確かめ、残り 33 バイトの偶奇と x から y を計算し直して公開鍵に戻します。ネットワークには一切出ません。 試作では lib/didkey.mjslib/p256.mjs がこの往復を行っています。

一次情報: multicodec のコード表は multiformats/multicodec の table.csv、方式の定義は The did:key Method v0.9。なお did:key 仕様は W3C 勧告ではなく CCG の草案です。

では、どこで使うのか

機関の恒久的な ID には向きません。 ただ、使われないわけではなく、向いている場面がはっきりしています。

向く向かない
短命な主体(1回限りの署名者、セッション鍵)何十年も同じ識別子を名乗る機関
ホストを持てない相手(端末・機器)鍵の更新が要る主体
試験・デモで登場人物を増やしたいとき失効が要る主体

第6章では、名簿の説明のために例示大学のほかに機関を増やしますが、増える側は全部 did:key です。鍵を1組作れば1秒で識別子ができるので、試作で人数を増やすには最も手軽です。そしてその手軽さが、同じ章で扱う穴の原因にもなります。

資料そのものの ID には使えません。 did:key は「鍵を持つ主体」の識別子であって、資料は鍵を持たないからです。資料の同一性は、第5章で見る通り URI + 内容のハッシュで表します。DID と資料 ID は、そもそも別の問題を解く道具です。

この「誰も保証しない」は、あとで具体的な被害の形で戻ってきます。ホストも認証局も要らないということは、誰でも1秒で「発行者」になれるということでもあります。自分で鍵を作り、他人の資料に署名すれば、検証は数学的には完璧に通ってしまう。第6章で、その様子を実際に見ます。

did:web — 静的ファイル1枚

識別子を URL に読み替えて、そこに置いてある JSON を取ってくる方式です。

did:web:nakamura196.github.io            → https://nakamura196.github.io/.well-known/did.json
did:web:nakamura196.github.io:did-log    → https://nakamura196.github.io/did-log/did.json
did:web:…:did-log:staff:nakamura         → https://nakamura196.github.io/did-log/staff/nakamura/did.json

.well-known が入るのはドメイン直下のときだけという特例があります。パスがある場合は入りません。ここは実装時に一度は踏む落とし穴です。

利用者の追加はディレクトリを1つ足して git push するだけ、失効は revoked.json に1行足すだけで済みます。フォームからの自己登録のような動的な書き込みだけができませんが、管理者がコミットする運用でよければ問題ありません。

did:web を使っても CA からは自由にならない

ここは強調しておきたい点です。

[→] did:web   (静的ファイル1枚。TLS証明書=CAに依存)
  信用しているもの:
    - nakamura196.github.io のDNSとTLS証明書 (= CA)
    - nakamura196.github.io のホスト。過去のdid.jsonを黙って差し替えられても検知できない

https:// で取りに行く以上、その接続が本物のドメインに繋がっている保証は TLS 証明書、つまり認証局(CA)が出しています。DID は CA を消したのではなく、Web の公開鍵基盤の上に一段乗せた形です。

本当に CA が不要なのは did:key だけですが、その代償は「その鍵が例示大学のものだと誰も保証しない」ことでした。CA が担っていたのはまさにそこなので、捨てれば当然その機能も消えます。

「ブロックチェーンで中央集権をなくす」という言い方をよく見ますが、実際に手を動かすと、この3方式の範囲では、何かを捨てると必ず何かが消えました

誰が何をキャッシュするのか

ここは混乱しやすいので、整理しておきます。取りに行くのは証明書ではなく、公開鍵です。

何を持つ / 取りに行くかどこから
保持者(中村)証明書(VC)そのもの発行時に一度もらう。以後ずっと自分の端末
検証者(機関C)発行者の公開鍵did.json発行者のサーバ。ここだけが通信

証明書は提示のたびに保持者が持ってくるので、検証者が発行者に取りに行くことはありません。検証者が発行者に触るのは、署名を確かめるための公開鍵を取るときだけです。

ただし、通信が完全に消えるわけではありません。残るものを並べます。

  • 鍵を毎回取りに行くと、発行者は「誰かが自分の発行した証明書を検証している」ことを観測できます
  • 発行者に分かるのはアクセスがあった事実・時刻・検証者の IP です。取りに行っているのが発行者自身の公開鍵1つだけなら、そこに利用者の情報は含まれません
  • ただし、保持者の鍵も同じドメインに置いていると、「誰の証明書か」まで分かります。 第2章で実測した通りで、この試作はまさにそうなっていました。パスに保持者の名前が入るからです
  • 鍵は滅多に変わらないので、検証者が一度取ってキャッシュすれば通信はゼロになります

つまり漏れるとしても「照会 API 方式」とは粒度が違います。

発行者に分かること
照会 API 方式誰が・いつ・どの機関に申請したか(利用者単位で全部)
この方式(鍵を毎回取得・発行者の鍵だけ)誰かが検証している、という事実と時刻だけ
この方式(鍵を毎回取得・保持者の鍵も同じドメイン)誰の証明書を検証しているかまで(第2章で実測)
この方式(鍵をキャッシュ)何も分からない

キャッシュするのは検証者です。保持者は最初から証明書を持っているので、キャッシュという概念が要りません。

did:webvh — 追記ログを足す

vhVerifiable History(検証できる履歴) の略です。名前がそのまま「did:web + 検証できる履歴」を表しています。もともと did:tdw(Trust DID Web)という名前でしたが、v0.5 で改称されました。仕様は Decentralized Identity Foundation にあります。

did:web には、もうひとつ弱点があります。did.json は「今の鍵」しか語りません。

鍵を更新すると、古い鍵はファイルから消えます。すると、更新より前に発行した証明書が検証できなくなります。

試作1で実際に鍵を #key-1 から #key-2 に更新してみた結果です。

この結果を再現するには順序が要ります。 #key-1 で VC を発行し、その後で鍵を #key-2 に回した状態でなければなりません。

$ node scripts/10-log-init.mjs      # ログを作る (#key-1)
$ node scripts/03-issue.mjs         # #key-1 で VC を発行
$ node scripts/11-log-rotate.mjs    # #key-2 に更新

この順で作った VC を検証すると、こうなります。

$ node scripts/05-verify.mjs nakamura --method=web
  ✘ 発行者の署名  検証鍵が見つからない: …#key-1

$ node scripts/05-verify.mjs nakamura --method=webvh
  ✔ 発行者の署名  did:web:… の鍵で検証OK
  判定: 受理

did:webvh は、did.json の代わりに 追記ログ(did.jsonl を置きます。1行1エントリの JSON Lines で、各行が「その時点の DID Document 全体」を含みます。

実際の1行目です。

{
  "versionId": "1-7514b061f4b5f42631e649616c69f40a14b1c686f67c61c18270a8070d103260",
  "versionTime": "2026-04-01T00:00:00Z",
  "prev": null,
  "state": {
    "id": "did:web:nakamura196.github.io:did-log",
    "verificationMethod": [{
      "id": "did:web:nakamura196.github.io:did-log#key-1",
      "publicKeyJwk": {
        "kty": "EC",
        "x": "7Q4Tpwo8e9ZbK_rpK7YpspQGsaqDgSYAQ-jzyvDiS3o",
        "y": "436ktnJGOkpD3LdKiJsZ2gGxZCQt_zbq4U7IU0MJwtM",
        "crv": "P-256"
      }
    }]
  },
  "proof": "eyJhbGciOiJFUzI1NiIs…"
}

仕掛けは2つあります。

versionId にハッシュが埋まっています。 1-7514b061f4b5… の後半は、そのエントリ全体(prevversionTimestate)のハッシュです。中身を1文字でも書き換えれば、再計算した値がズレます。

prev が直前のエントリを指しています。 だから途中の1行を書き換えると、以降の行の versionId全部ズレます。これを「ハッシュ連鎖」と呼びます。

そして署名は「直前のエントリで有効だった鍵」で行われます。鍵を持たない者は、Web サーバに書き込めてもエントリを捏造できません。

こうして「発行当時どの鍵が有効だったか」を後から辿れるようになり、過去に出した証明書が生き続けます。長期保存が前提の資料では、ここが効いてきます。

GitHub Pages で足りる

ここまでの全方式が、静的ファイルの配信だけで動きます

docs/
├── did.json                 発行者の公開鍵
├── did.jsonl                追記ログ
├── revoked.json             失効した証明書の id
└── staff/
    ├── nakamura/did.json
    └── user-b/did.json

このディレクトリをそのまま公開すれば動きます。特別なサーバも、データベースも、認証機構も要りません。書き込みは git commit そのものなので、追記ログの API を自作せずに済みます。

副産物として、Git のコミット履歴が第二の記録になります。force push しない限り、ログを作り直しても履歴に痕跡が残ります。

ただし「独立した」とまでは言えません。リポジトリは機関の管理下にあるので、force push もリポジトリの作り直しもできます。Git の履歴が効くのは、誰か他人がクローンを持っているときだけです。この点は第4章で扱います。

……という書き方をしたのは、次章で「ログを作り直す」話をするからです。ここまでは順調に見えていますが、実はこの時点で、かなり大きな穴が空いています。