STH ダイジェスト
このページでは、Signed Tree Head ダイジェストを第三者と照合し、スプリットビュー攻撃を緩和する仕組みを説明します。
ログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートを束縛するダイジェストにより、サーバーが異なるクライアントに異なるツリー状態を提示する攻撃を検出可能にします。
スプリットビュー攻撃に対する位置づけ
スプリットビュー攻撃(split-view attack)とは、悪意あるサーバーが異なる検証者に対して異なる掲示板の状態を提示する攻撃です。 STH ダイジェストは、掲示板の状態を(ログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュ)の組として束縛し、独立した第三者ソースとの合意確認を通じてこの攻撃を検出します。 具体的な攻撃と検出の流れは後述します。
flowchart TD
subgraph "STH ダイジェストの構成"
LID[ログ ID<br/>32 バイト]
TSZ[ツリーサイズ<br/>4 バイト]
TS[タイムスタンプ<br/>8 バイト]
BR[掲示板ルート<br/>32 バイト]
end
LID --> H[SHA-256]
TSZ --> H
TS --> H
BR --> H
H --> STH[STH ダイジェスト<br/>32 バイト]
STH --> J[zkVM ジャーナルに記録]
STH --> CS[close-statement.json に記録]
STH --> TP[第三者ソースの STH と照合]
本実装での検証対象: このページで言う「STH ダイジェスト」は
sthDigest自体です。 本実装は STH の署名検証は行いません(スコープの詳細は 合意ロジック を参照)。
ダイジェストフォーマット
sth_digest = SHA-256(
log_id ← 32 バイト
|| tree_size ← u32 リトルエンディアン (4 バイト)
|| timestamp ← u64 リトルエンディアン (8 バイト, Unix 時刻ミリ秒)
|| bulletin_root ← 32 バイト
)
SHA-256 への入力は合計 76 バイトです。
各フィールドの仕様
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ログ ID | 32 バイト | ハッシュ値 | 掲示板インスタンスの識別子 |
| ツリーサイズ | 4 バイト | u32 LE | 掲示板のリーフ数 |
| タイムスタンプ | 8 バイト | u64 LE | Unix 時刻(ミリ秒) |
| 掲示板ルート | 32 バイト | ハッシュ値 | Merkle ツリーのルート |
ログ ID
ログ ID は掲示板インスタンスを一意に識別する値です。 次の式で生成されます。
log_id = SHA-256("stark-ballot:bulletin-log|v1.0" || seed)
ドメインタグ "stark-ballot:bulletin-log|v1.0" と任意のシード値を連結し、SHA-256 でハッシュします。
ログ ID は掲示板のライフタイム中に変化しない固定値です。
ログ ID を STH ダイジェストに含めることで、異なる掲示板インスタンスの STH が偶然に衝突することを防止します。
スプリットビュー攻撃と検出メカニズム
攻撃シナリオ
sequenceDiagram
participant A as 投票者 A
participant S as 悪意あるサーバー
participant B as 投票者 B
S->>A: ツリー状態 X<br/>(64 票、ルート R₁)
S->>B: ツリー状態 Y<br/>(63 票、ルート R₂)
Note over A,B: A と B は互いに異なる<br/>ツリー状態を見ている
この攻撃では、サーバーは投票者 B に対して特定の票を除外したツリーを見せています。 投票者 B は自身に提示されたツリーに対する包含証明や整合性証明を検証できますが、投票者 A とは異なるツリーを見ていることに気づけません。
第三者合意による検出
検証 API は、VITE_STH_SOURCES に設定されたソースから STH evidence を取得し、ジャーナル内の STH ダイジェストと照合することでスプリットビューを検出できます。
sequenceDiagram
participant V as 検証者
participant S as 検証 API
participant T1 as 第三者ソース 1
participant T2 as 第三者ソース 2
V->>S: 検証リソースを要求
S->>S: ジャーナルから STH ダイジェスト D' を取得
S->>T1: STH を問い合わせ → D₁
S->>T2: STH を問い合わせ → D₂
S->>S: D' = D₁ = D₂ ?
alt 全て一致
S->>V: 合意成立を含む検証結果
else 不一致あり
S->>V: スプリットビューの疑い → 検証失敗
end
合意ロジック
合意判定の条件(最小一致数と全会一致の要求)は チェック一覧 > recorded_sth_third_party を正とします。
各ソースに対して次のフィールドを照合します。
| 照合フィールド | 条件 |
|---|---|
| STH ダイジェスト | 必須一致 |
| 掲示板ルート | 提供されている場合は一致 |
| ツリーサイズ | 提供されている場合は一致 |
外部 HTTPS ソースの応答は、sthDigest、bulletinRoot、treeSize、timestamp、logId
だけを許可する flat な canonical record として fail-closed に受理します。sthDigest は必須で、
その他のフィールドは任意ですが、指定された場合は型と形式を検証します。wrapper、未知のキー、
digest / root / log_id などの alias は受理しません。
設定済みソースの取得失敗、利用不能、または malformed response は、ほかの受理済みソースが
最小一致数を満たしていても合意失敗になります。
冒頭の注記のとおり応答署名は検証しません。外部アンカリングと STH の自動公開も本実装のスコープ外です。
zkVM との連携
zkVM ゲストプログラムは、入力として受け取ったログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートから STH ダイジェストを再計算し、ジャーナルにコミットします。
finalize 時には、同じツリー状態から close-statement.json も構築されます。
sthDigest は、配布対象アーカイブ bundle.zip に含まれる公開監査アーティファクトへ反映されます。
この仕組みにより、STARK 証明と bundle.zip 内の close-statement.json がともに特定のツリー状態へ束縛されます。
第三者はジャーナルの STH ダイジェストを独立ソースの値と照合することで、サーバーが証明と異なるツリー状態を提示していないかを確認できます。
検証パイプラインにおける役割
STH ダイジェストは 2 つの段階で利用されます。
| 段階 | チェック ID | 検証内容 |
|---|---|---|
| Recorded-as-Cast | recorded_sth_third_party | 独立ソースから取得した STH ダイジェストがジャーナルの値と一致するか |
| Counted-as-Recorded | counted_close_statement_consistent | close-statement.json の sthDigest が公開入力およびジャーナルの値と整合するか |
recorded_sth_third_party は既定では任意チェック(optional)ですが、STH ソースが設定されている場合は required に昇格します(昇格と最終判定の規則は ゲーティングロジック を参照)。
counted_close_statement_consistent は常に必須チェック(required)です。
close-statement.json がジャーナルと整合しない場合、検証は失敗します。
各チェックの判定ロジックは チェック一覧 > Recorded-as-Cast と チェック一覧 > Counted-as-Recorded を参照してください。
設定
第三者 STH 検証は環境変数で制御されます。
| 環境変数 | 説明 | コードフォールバック値 |
|---|---|---|
VITE_STH_SOURCES | カンマ区切りの same-origin template / 外部 HTTPS URL | 未設定(第三者照合を実行しない) |
VITE_STH_MIN_MATCHES | 必要な最小一致ソース数 | 2 |
STH ソースが未設定の場合、recorded_sth_third_party は not_run(未実行)となり、第三者照合は行いません。
開発用の .env.local.example では、必要な場合に有効化する例として次の値がコメントで示されています。
VITE_STH_SOURCES=/api/sessions/:sessionId/bulletin/sthVITE_STH_MIN_MATCHES=1
same-origin template と外部 HTTPS source の取り扱い
same-origin template: 相対 source として受理するのは exact template
/api/sessions/:sessionId/bulletin/sth だけです。任意で auditorId query を 1 つ指定できます。
検証 API は :sessionId を現在の session ID で解決し、認証済み session context から STH を
直接 project します。この内部取得では HTTP request や session capability header の転送を行いません。
同じ path を通常の HTTP endpoint として直接取得する場合は、same-origin request と path の
session ID に対応する X-Session-Capability が必要です。finalization が成功している session に限り、
{ data: { sth: { ... } }, meta } envelope を返します。
外部 HTTPS source: absolute HTTPS URL は、app と同じ origin を指す場合でも外部 source として 取得します。repository の session capability や同等の認証情報は送信しません。そのため、独立した 第三者 source は session 認証に依存しない公開 STH endpoint である必要があります。
same-origin projection の timestamp: same-origin projection が返す timestamp はジャーナル内の
canonical な時刻ではなく session.voting.lastActivityMs です。
そのため、第三者合意の一致判定で実際に照合するのは必須の sthDigest と、ソースが返した場合の bulletinRoot / treeSize です。
PoC における制約
本 PoC の開発用テンプレート(.env.local.example)では、STH ソースとして同一サーバー上の
session-scoped template(/api/sessions/:sessionId/bulletin/sth)を使う例を示しています。
同一サーバー上のソースのみでは防御力が限定的であるため、独立した組織が運営する複数ソースを VITE_STH_MIN_MATCHES >= 2 で構成することを推奨します。