DID とは何か
DID(Decentralized Identifier、分散識別子)は、W3C が標準化した識別子の書式です。形はこうなっています。
did:web:nakamura196.github.io:did-log
~~~ ~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
│ │ └ その方式ごとの中身
│ └ メソッド名(どうやって鍵に辿り着くか)
└ 固定の接頭辞
真ん中のメソッド名が全てを決めます。「この識別子から公開鍵をどう取ってくるか」という手続きが、メソッドごとに違うからです。
「分散」と名前に入っていますが、分散台帳が必須なわけではありません。実際、以下の3方式のうちブロックチェーンを使うものはひとつもありません。
3方式を並べる
試作1では3つの方式を実装して、同じ証明書を同じコードで検証できるようにしました。
did:key | did:web | did: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.mjs と lib/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 — 追記ログを足す
vh は Verifiable 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… の後半は、そのエントリ全体(prev と versionTime と state)のハッシュです。中身を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章で扱います。
……という書き方をしたのは、次章で「ログを作り直す」話をするからです。ここまでは順調に見えていますが、実はこの時点で、かなり大きな穴が空いています。