公開 JSON の改ざん検知
🔗 改ざん検知の鎖 — 公開した JSON が後から変えられていないことを、誰でも再計算できるか
当サイトは毎日、その日の最後の日次反映の直後に、公開している JSON ファイル全件の SHA-256 一覧を書き出し、前日の一覧と連鎖(ハッシュチェーン)させ、 その一覧ファイルの SHA-256 を OpenTimestamps に送ってビットコインのブロックチェーンに刻印しています。 外に出るのは 32 バイトのハッシュだけで、ファイルの中身は送りません。連鎖の初日は 2026-10-03 です。過去の日付の一覧は作りません(後から作った一覧は改ざんの証明になりません)。
このページは観測対象の世界について何も判定しません。できるのは「ある日に存在した JSON がそのままか」を、公開ファイルだけから第三者が再計算することです。
ファイル
- ・/api/integrity/latest.json — 最新の日:date・その日の一覧ファイル・その file_sha256(刻印したダイジェスト)・chain_hash・OpenTimestamps 証明の場所と状態(ビットコインのブロックに取り込まれるまで pending、取り込まれたら complete)
- ・/api/integrity/YYYY-MM-DD.json — その日の一覧:files[](path・bytes・sha256。path 順)・list_hash・prev(前日の日付・ファイルのダイジェスト・chain_hash)・chain_hash
- ・/api/integrity/YYYY-MM-DD.json.ots — その日の一覧ファイル(配信されたバイト列そのまま)の SHA-256 に対する OpenTimestamps 証明
一覧に含めないもの:この仕組み自身のファイルと、名前が _ で始まるファイル。HTML ページは CDN が解析用スクリプトを差し込むため含めません(JSON はバイト単位で同一に配信されます)。
再計算の手順(誰でもできます)
- 1. 1 つのファイル。確かめたい JSON と、確かめたい日の一覧ファイルを取得します。JSON の SHA-256 を計算し、一覧の files[] で同じ path の sha256 と比べます。一致すれば、その日の一覧が書かれた時点のファイルとバイト単位で同一です。
- 2. 一覧。list_hash = files[] の正規化 JSON(キー順ソート・区切りは , と : で空白なし・非 ASCII はエスケープ・UTF-8)の SHA-256 を再計算します。Python なら hashlib.sha256(json.dumps(files, sort_keys=True, separators=(',',':'), ensure_ascii=True).encode('utf-8')).hexdigest()。
- 3. 連鎖。chain_hash = UTF-8 文字列 prev.chain_hash + list_hash(16 進小文字 2 つの連結。初日は prev が null なので空文字)の SHA-256 を再計算します。次に前日の一覧ファイルを開き、その chain_hash が prev.chain_hash と、そのファイルの SHA-256 が prev.file_sha256 と一致することを確かめます。2026-10-03 まで繰り返します。
- 4. 時刻。OpenTimestamps の証明をその日の一覧ファイルに対して検証します:ots verify YYYY-MM-DD.json.ots -f YYYY-MM-DD.json(クライアントは pip install opentimestamps-client。ブロックの解決にはビットコインノードか公開カレンダーを使います)、または opentimestamps.org に両ファイルをアップロードします。完全な証明にはビットコインのブロック高が示され、その一覧ファイル(=その中の全ハッシュ)がそのブロックより前に存在したことが分かります。
# Python 3 — 最新の日について手順 1〜3
import hashlib, json, urllib.request
B = 'https://ai-lifeline.org'
get = lambda p: urllib.request.urlopen(B + p).read()
latest = json.loads(get('/api/integrity/latest.json'))
day = json.loads(get(latest['file']))
assert hashlib.sha256(get(latest['file'])).hexdigest() == latest['file_sha256']
lh = hashlib.sha256(json.dumps(day['files'], sort_keys=True, separators=(',', ':'), ensure_ascii=True).encode('utf-8')).hexdigest()
assert lh == day['list_hash']
prev = (day.get('prev') or {}).get('chain_hash') or ''
assert hashlib.sha256((prev + lh).encode('utf-8')).hexdigest() == day['chain_hash']
for f in day['files']: # 公開 JSON 全件をバイト単位で
assert hashlib.sha256(get(f['path'])).hexdigest() == f['sha256'], f['path']
print('ok', day['date'], day['file_count'], 'files')
その後の日次反映で更新されたファイルは、古い日の一覧とは一致せず、新しい日の一覧とは一致します(同じ日の一覧と比べてください)。公開後のどの日の一覧とも一致しないファイルは、日次バッチの外で変更されたことになります。
証明できること・できないこと
- ・証明できること:ある JSON が、その日の証明を含むビットコインのブロックより前にバイト単位でそのまま存在していたこと。日次の一覧が 2026-10-03 から途切れずに連鎖していること。
- ・証明できないこと:中の数値が正しいこと。正しさは各ページと /api/series.json に載せた一次資料で確かめるものです。
- ・一覧を書くのはファイルを公開するのと同じバッチです。サイトを操作できる者はファイルを変えて新しい一覧を書けますが、日付を過去に戻すことはできません(証明のブロック高は公開情報です)。日をまたいで一覧を比べてください。
- ・日次の処理が失敗した日(カレンダー不達・転送失敗)は一覧がありません。失敗は当サイトの運用記録に残り、翌日の一覧は最後に成功した日から連鎖を続けます。
方針:運営者・編集方針(一次証拠の保全)。英語版:Integrity chain。