この章がこの本の折り返し地点です。ここまでの話が、かなりの部分ひっくり返ります。
前章までで、証明書は署名で守られ、鍵の履歴はハッシュ連鎖で守られる、というところまで来ました。では実際に壊してみます。試作1には改竄を試すスクリプトが入っていて、3つのシナリオを順に実行します。
A. 証明書の中身を書き換える
中村の役職を Faculty から Professor Emeritus に書き換えてみます。
[A] VC の主張を書き換える (Faculty → Professor Emeritus)
✔ 検知した 署名が一致しない
当然ですが、落ちます。署名は証明書全体のハッシュに対して作られているので、1文字でも触れば合いません。
紙の押印との決定的な違いはここです。 押印は「押した」ことしか示さず、その後で本文を書き換えられても押印自体は無傷です。電子署名は本文と不可分に結びついているので、本文を触れば署名が壊れます。
B. 追記ログの途中を書き換える
次に、ログの1行目の日付(versionTime)を前倒ししてみます。
[B] 追記ログの1行目を書き換える (versionTime を前倒し)
✔ 検知した #1 ハッシュ連鎖が壊れている
(記録 7514b061f4b5… / 再計算 5a9b5094d77d…)
前章で見た仕掛けが働いています。versionId に記録されていたハッシュは 7514b061f4b5… でしたが、書き換わった中身から再計算すると 5a9b5094d77d… になり、一致しません。
しかも prev で鎖になっているので、2行目以降の versionId も全部ズレます。1行だけこっそり直す、ということができません。
ここまでは設計通りです。
C. ログを丸ごと作り直す
3つ目が本題です。大学自身が、ログを最初から作り直すとどうなるか。
やり方はこうです。偽の鍵を用意し、「最初からこの鍵が大学の鍵だった」ということにして、1行目から全部書き直します。ハッシュ連鎖も、当然ながら整合するように再計算します。そのうえで、過去日付の偽の所属証明を発行します。
[C] 鍵を持つ本人 (=大学自身) がログを最初から作り直す
✔ 連鎖の検証は通ってしまう head=2-(実行のたびに変わる)
✘ 過去日付の偽 VC が検証を通る issuanceDate=2026-05-01T00:00:00Z
✘ 本物の head と一致しない 本物 2-cc13dd95c3de50… /
差し替え後 2-(実行のたびに変わる)
差し替え後の値を伏せているのは、偽の鍵を毎回その場で作っているからです。同じ手順を踏んでも、出てくる head は毎回違います。ここで大事なのは値そのものではなく、本物と違う値になるという一点です。
通ってしまいます。
連鎖の検証は「各行のハッシュが正しく計算されているか」を見ているだけです。最初から作り直せば、当然すべて正しく計算されています。矛盾はどこにもありません。
そして 2026-05-01 付の、実際には存在しなかった所属証明が、did:webvh の検証を通過します。
なぜこれが起きるのか
ログを自分でホストしているからです。
ハッシュ連鎖が防げるのは「一部の書き換え」だけです。全体を置き換えられてしまえば、内部的には何の矛盾も残りません。改竄の痕跡は、比較対象があって初めて痕跡になります。
そして比較対象は、この構成のどこにもありません。検証者は「いま取得したログ」しか見ておらず、それが以前と同じものかを知る手段がないからです。
head とは何か
先に、この後ずっと出てくる head を説明しておきます。
追記ログの最終行の versionId のことです。第3章で見た通り、versionId にはそのエントリ全体のハッシュが埋まっていて、各行が prev で前の行を指しています。だから最終行の値ひとつで、それまでの全履歴が確定します。
did.jsonl
1行目 versionId: 1-7514b061f4b5… prev: null
2行目 versionId: 2-cc13dd95c3de… prev: 1-7514b061f4b5…
~~~~~~~~~~~~~~~
これが head。この1個で全体が決まる
途中を1文字でも変えれば以降の versionId が全部ズレるので、64文字の文字列を1つ控えておくだけで「あの時点のログ」を丸ごと指し示せます。
検知できる唯一の条件
検知できる場合が1つだけあります。以前の状態を控えていた誰かがいるときです。
試作1では、機関Cが過去に観測した head を控えておく仕組みを入れました。これを witness(証人)と呼びます。
誰が、どこに、どう書くのか
控えるのは検証者です。 発行者ではありません。発行者に控えさせても、書き換える当人なので意味がありません。
試作では、機関Cが自分の手元に1枚のファイルを持ちます。
{
"did": "did:webvh:nakamura196.github.io:did-log",
"observations": [
{
"head": "1-7514b061f4b5f42631e649616c69f40a14b1c686f67c61c18270a8070d103260",
"entries": 1,
"observedAt": "2026-04-01T00:00:00Z"
}
]
}
書くのは head と、観測した時刻と、そのとき何行あったか。それだけです。 鍵も署名も要りません。
やっているのはこれだけです。
// 検証のついでに、いまの head を書き足す
const prior = exists(witnessPath) ? readJson(witnessPath).observations : [];
prior.push({ head: r.head, entries: r.entries.length, observedAt: … });
writeJson(witnessPath, { did: ISSUER.didwebvh, observations: prior });
node scripts/21-witness.mjs record # 控える(検証のたびに走らせる想定)
node scripts/21-witness.mjs check # 突き合わせる
突き合わせで何を見るか
控えておいた head が、いまのログの中にまだ在るか、それだけを見ます。
const stillThere = r.entries.some((e) => e.versionId === last.head);
正当な追記なら、過去の head はログの途中の行として残り続けます。作り直された場合は、その versionId はどこにも存在しません。だから消えていれば差し替えです。
実運用ではどこに置くか
試作ではローカルのファイルですが、置き場所は「機関が単独で書き換えられない場所」であれば何でも構いません。
| 置き場所 | 効き方 |
|---|---|
| 検証者自身の記録 | 最も素直。ただし検証者が1者だと、その1者を信じる話になる |
| 複数の機関が持ち合う | 突き合わせで多数決ができる。図書館の LOCKSS と同じ形 |
| 学術誌や紀要に印字 | 紙は書き換えられない。更新頻度は落ちる |
| 第三者のタイムスタンプ局 | RFC 3161(RFC 5816 で更新)。ETSI EN 319 421 と eIDAS 第42条まで繋がっていて、制度としては確立している |
| 公開チェーンに書く | 参加者を事前に決めなくてよい。第10章で扱う |
どれも「64文字を、機関の手の届かないところに置く」という一点だけをやっています。 違うのは、誰に預けるかという信頼の配り方です。実装の手間は同じではなく、第9章以降で見る通り、公開チェーンが最も重くなります。
差し替えを実際に検知する
では、作り直されたログに対して突き合わせを走らせます。
21 witness — check
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✔ ログ自体の連鎖 健全
✘ 控えておいた head がログ内に残っているか 1-7514b061f4b5f42631e649…
ログが差し替えられている。 連鎖は整合しているが、以前観測した版が消えている。
連鎖だけを見ていたら気づけない。控えを持つ第三者がいて初めて分かる。
「ログ自体の連鎖 ✔ 健全」と「差し替えられている」が同時に出ているのがこの出力の要点です。ログの内部は完全に整合していて、外から見て初めて矛盾が分かります。
Git の履歴は witness になるか
ここで、第3章に書いた一文が引っかかります。追記ログは Git で管理していて、コミット履歴が第二の記録になるはずでした。ならばシナリオ C も、Git の履歴を見れば気づけるのではないか。
気づけません。git push --force すれば履歴は書き換わり、リポジトリごと削除して作り直せば消えます。追記ログを自分でホストしているのと同じ理由で、そのリポジトリも機関の管理下にあります。
そして、問題はもっと手前にあります。
Git の履歴が witness として働くのは、「誰か他人がクローンを持っている」ときだけです。
つまり、これは witness の代わりではなく、witness そのものです。条件は何も変わっていません。
Git が足してくれるのは「気づきやすさ」です。
| 起きること | |
|---|---|
| 誰もクローンを持っていない | 書き換えても誰も気づかない |
| 誰かがクローンを持っている | 次の git fetch が non-fast-forward で失敗する。騒がしく壊れる |
黙って壊れるか、騒がしく壊れるかの差は実務上は大きいので、Git を使う価値はあります。ただし「壊れたことが分かる」のは、やはり控えを持っている誰かがいる場合に限られます。条件は増えても減ってもいません。
GitHub のような第三者のホストを使えば、機関が単独では消しきれない痕跡も多少残ります(削除したコミットが API から一定期間見える、fork がオブジェクトを保持する、など)。ただしこれらは保証されている挙動ではないので、検証の手続きとして組み立てることはできません。「たまたま残っているかもしれない」は、証拠にはなりません。
ここで初めてブロックチェーンが出てくる
この一点が、ブロックチェーンが解こうとしている問題です。
ブロックチェーンは、ホスティングを不要にする技術ではありません。 特定のホストを信用する必要を消す技術です。
チェーン上に置かれた記録は、その機関のサーバにも、ドメインにも、管理下のログにもありません。だから機関が自分の記録を作り直しても、チェーン上のものは消えません。チェーンが witness を務めるわけです。
図書館界には同じ発想がある
「独立したコピーを複数の機関が持ち合えば改竄は検知できる」という構造は、電子ジャーナルの長期保存で使われる LOCKSS(Lots Of Copies Keep Stuff Safe)とまったく同じ発想です。
複数の機関が同じ内容を保持し、定期的に投票(opinion poll)で突き合わせる。圧倒的多数と食い違った機関は、一致した機関からコピーを取り寄せて自ら修復します。賛否が割れて決着しない場合は自動で判定せず、人間に警報を上げる。検出だけでなく修復まで含んでいるのが LOCKSS の要点です。ブロックチェーンは、この突き合わせを自動化して、参加者が互いを信頼していなくても成立するようにしたもの、と読むと位置づけがはっきりします。
それでも、まだ入れない
ここで注意しておきたいのは、この問題はブロックチェーンなしでも部分的に解けることです。
先の表に挙げた選択肢のうち、公開チェーン以外はすべてブロックチェーン抜きです。複数の機関で控え合う、公開の場に掲示する、タイムスタンプ局に預ける。いずれも「独立したコピー」を作る手段で、どれも実際に使われています。ブロックチェーンはその選択肢の1つであって、唯一の答えではありません。
では何が違うのか。参加者を事前に決めておかなくてよいことです。上記の方法はどれも「誰に控えてもらうか」を先に決める必要がありますが、公開チェーンにはその調整が要りません。
代償もあります。全世界から見えます。 この点は最終章で扱います。
この章のまとめ
| 攻撃 | 防いだもの | 結果 |
|---|---|---|
| A. 証明書を書き換える | 電子署名 | 検知 |
| B. ログの一部を書き換える | ハッシュ連鎖 | 検知 |
| C. ログを丸ごと作り直す | — | 通ってしまう |
| C + witness | 独立したコピー | 検知 |
試作1は、C の手前で止まっています。次章では証明の対象を人から資料に移しますが、この限界はそのまま引き継がれます。解決に入るのは第10章です。