Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4 段階検証モデル

E2E 検証可能投票の 4 段階検証モデルの設計と各段階の保証を解説します。

各段階は独立した暗号的保証を提供し、いずれかが失敗した場合でも、どの段階で何が問題かを特定可能にします。

段階間の依存関係

概念モデルとしては 4 段階を順に評価しますが、実装では GET /api/verify がチェック群をまとめて計算します。STARK ステータスによって Counted チェックが pending / not_run / failed にゲートされる(zkGate)ため、後段の評価可否に影響します。また、UI は STARK 解決後に段階表示を順次進めます。

現行実装では、各段階のチェック判定そのものは主に GET /api/verify 側で計算され、クライアントは結果の表示とシーケンス制御を担当します。validateVotingIntegrity は補助判定として存在しますが、通常の /verify 応答では journal が省略されるため常時実行される経路ではありません。

flowchart LR
  subgraph "証拠の生成"
    V["投票時<br/>レシート発行"]
    F["集計時<br/>zkVM 実行"]
  end

  subgraph "4 段階検証"
    S1["Stage 1<br/>Cast-as-Intended"]
    S2["Stage 2<br/>Recorded-as-Cast"]
    S3["Stage 3<br/>Counted-as-Recorded"]
    S4["Stage 4<br/>STARK Verification"]
  end

  V --> S1
  V --> S2
  F --> S3
  F --> S4

  S1 ~~~ S2
  S2 ~~~ S3
  S3 ~~~ S4

Stage 1: Cast-as-Intended

目的

投票者が意図した選択肢のコミットメントが、サーバーから返却されたレシートと一致することを確認します。これにより、サーバーが投票者の選択を差し替える攻撃を検出します。

検証する内容

現行実装では、GET /api/verify が投票時にセッションへ保存された 3 つの値(選挙 ID、選択肢、乱数)からコミットメントを再計算し、投票時のレシートに含まれるコミットメント値と照合します。暗号式自体は、投票者がローカルで再計算する Cast-as-Intended と同一です。

flowchart LR
  subgraph "投票時に確定したデータ"
    EID[選挙 ID]
    CH[選択肢]
    RND[乱数]
  end

  subgraph "再計算"
    HASH["SHA-256<br/>(ドメインタグ || 選挙ID || 選択肢 || 乱数)"]
  end

  subgraph "照合"
    CMP{"一致?"}
    REC[レシートの<br/>コミットメント]
  end

  EID --> HASH
  CH --> HASH
  RND --> HASH
  HASH --> CMP
  REC --> CMP

必要な証拠

証拠保管場所説明
選挙 IDサーバーセッションセッション作成時に確定した UUID
選択肢サーバーセッション投票時に保存された投票者の選択肢(A〜E)
乱数サーバーセッション投票時にクライアントが生成し、投票データとして保存された 32 バイト乱数
レシートサーバーセッション投票受理時に返却されたレシート(commitment, voteId, bulletinIndex)

失敗モード

症状原因深刻度
セッション証拠の欠落セッション期限切れ・破損などで投票時データを復元できない検証不能
コミットメント不一致投票時データとレシートの不整合、またはエンコーディングの不整合重大
選択肢の範囲外不正な入力(AE の範囲外)重大
乱数フォーマット不正32 バイト hex でない重大

限界

現行の /api/verify 経路では Cast-as-Intended はセッション保存済み証拠に依存します。セッション証拠を復元できない場合、この段階は not_run になり最終判定は Verified になりません。


Stage 2: Recorded-as-Cast

目的

投票者のコミットメントが、追記専用の掲示板(CT Merkle ツリー)に正しく記録されていることを確認します。さらに、掲示板が追記専用性を維持していること(過去のエントリが削除・改変されていないこと)を検証します。

検証する内容

この段階では 3 種類の検証を行います。

flowchart TB
  subgraph "2a: 包含証明"
    IP["包含証明の検証<br/>自分のコミットメントが<br/>ツリーに存在するか"]
  end

  subgraph "2b: 整合性証明"
    CP["整合性証明の検証<br/>投票時のルートから<br/>最終ルートへの追記専用性"]
  end

  subgraph "2c: 第三者 STH 検証"
    STH["STH 合意の検証<br/>独立したソース間で<br/>ツリー状態が一致するか"]
  end

  IP --> RESULT{"全て成功?"}
  CP --> RESULT
  STH --> RESULT

2a: 包含証明(Inclusion Proof)

RFC 6962 の PATH 関数に基づく CT スタイルの Merkle 包含証明を検証し、投票者のコミットメントが掲示板のツリーに含まれていることを確認します。検証者はリーフハッシュと監査パスからルートハッシュを再計算し、期待されるルートと照合します。

2b: 整合性証明(Consistency Proof)

投票時点のツリー状態(ルートとサイズ)から、最終的なツリー状態への遷移が追記のみで行われたことを検証します。RFC 6962 の SUBPROOF アルゴリズムに基づく整合性証明により、サーバーが過去のエントリを密かに削除したり順序を変更したりするスプリットビュー攻撃を検出します。

2c: 第三者 STH 検証(オプション)

複数の独立した STH(Signed Tree Head)ソースに問い合わせ、比較可能な応答間で合意が成立しているかを確認します。照合の必須項目は STH ダイジェストで、bulletinRoottreeSize は各ソースが返した場合に追加照合されます。これにより、サーバーが検証者ごとに異なるツリー状態を提示するスプリットビュー攻撃を検出します。

必要な証拠

証拠取得元説明
包含証明の検証結果/api/verifyサーバー側で RFC 6962 包含証明を評価したチェック結果
整合性証明の検証結果/api/verifyサーバー側で RFC 6962 整合性証明を評価したチェック結果
投票時のルートハッシュレシート投票受理時のツリールート
投票時のツリーサイズ(oldSize)/api/verifyuserVote.proof.treeSize または voteReceipt.bulletinIndex + 1整合性証明の oldSize(投票時点の木サイズ)
最終ルートハッシュ/最終ツリーサイズ/api/verify集計時の最終状態
独立検証用の証明材料(任意)/api/bulletin/:voteId/proof, /api/bulletin/consistency-proofクライアント外で個別に包含証明/整合性証明を再検証するための材料
STH スナップショット設定済み STH ソース(例: /api/sth + 外部)第三者ソースの照合対象(必須は digest、root/treeSize は返却時のみ)

失敗モード

症状原因深刻度
包含証明の検証失敗ツリーサイズ/インデックスの不一致、掲示板のリセット重大
整合性証明の検証失敗追記専用性の違反(スプリットビュー攻撃の可能性)重大
第三者 STH 合意の不成立サーバーが検証者ごとに異なるツリーを提示重大(有効時)
ルートが履歴に存在しないルート履歴の不整合重大

Stage 3: Counted-as-Recorded

目的

掲示板に記録された全投票が、zkVM の集計処理に正しく含まれたことを確認します。投票の除外、欠落、重複がないことを検証し、集計結果の完全性を保証します。

検証する内容

flowchart TD
  START["Stage 3 開始"]
  G1["counted_missing_indices_zero<br/>excludedCount == 0"]
  G2["counted_expected_vs_tree_size<br/>totalExpected == treeSize"]
  G3["counted_input_commitment_match<br/>公開入力とジャーナルが一致"]
  G4["counted_my_vote_included<br/>ビットマップ証明で自票を確認"]
  OTHERS["残り required チェック<br/>counted_input_sanity<br/>counted_unique_indices<br/>counted_unique_commitments<br/>counted_tally_consistent"]
  PASS["Counted-as-Recorded: success"]
  FAIL["Counted-as-Recorded: failed"]

  START --> G1
  G1 -->|NG| FAIL
  G1 -->|OK| G2
  G2 -->|NG| FAIL
  G2 -->|OK| G3
  G3 -->|NG| FAIL
  G3 -->|OK| G4
  G4 -->|NG| FAIL
  G4 -->|OK| OTHERS
  OTHERS -->|all success| PASS
  OTHERS -->|any failed| FAIL
flowchart TD
  PUB["公開系チェック (4)<br/>counted_input_sanity<br/>counted_unique_indices<br/>counted_unique_commitments<br/>counted_input_commitment_match"]
  ZK["zk 系チェック (4)<br/>counted_tally_consistent<br/>counted_missing_indices_zero<br/>counted_expected_vs_tree_size<br/>counted_my_vote_included"]
  RESULT{"全 8 required チェックが success?"}
  PASS["Stage 3 を通過"]
  FAIL["Verified をブロック"]

  PUB --> RESULT
  ZK --> RESULT
  RESULT -->|Yes| PASS
  RESULT -->|No| FAIL

必要な証拠

証拠取得元説明
zkVM ジャーナル(詳細)/api/verify?includeJournal=1集計結果、除外情報、入力コミットメントの詳細
集計サマリー(通常応答)/api/verifymissingIndices / invalidIndices / excludedCount などの上位値
公開入力サマリー/api/verifyzkVM に渡された入力の公開可能部分
ビットマップ証明/api/bitmap-proof自分のインデックスが集計に含まれた証明
ビットマップルートジャーナルzkVM ゲストが計算したビットマップ Merkle ルート

通常の UI フローでは includeJournal=1 を付けないため、Counted チェックは上位フィールドと publicInputSummary を主に使用します。

重要な判定: excludedCount

excludedCount(除外された投票の数)は、本システムにおいて最も重要な不変条件の一つです。

  • excludedCount == 0: 全投票が集計に含まれた(正常)
  • excludedCount > 0: 一部の投票が集計から除外された(即座に検証失敗

この値が 0 でない場合、counted_missing_indices_zerofailed になり、最終サマリー判定は「Verified」を表示しません。

失敗モード

症状原因深刻度
excludedCount > 0投票が集計から除外された重大(即座にブロック)
欠落インデックスの検出一部の投票が zkVM 入力に含まれなかった重大
集計合計の不一致選択肢ごとの合計が有効投票数と一致しない重大
入力コミットメント不一致公開入力と zkVM 実行で使用された入力が異なる重大
ビットマップ証明の欠落ビットマップデータが利用不可、またはルート不一致警告
ツリーサイズの不一致totalExpectedtreeSize が異なる(暗黙の除外)重大(必須チェック失敗)

Stage 4: STARK Verification

目的

zkVM の実行が正しく行われたことを STARK 証明(レシート)の暗号学的検証により確認します。レシートの検証に成功すれば、ジャーナルの内容(集計結果、除外情報、入力コミットメント等)がゲストプログラムの正しい実行の結果であることが保証されます。

検証する内容

flowchart LR
  subgraph "入力"
    RCP[レシート<br/>Seal + Journal]
    EID[期待 Image ID]
  end

  subgraph "Rust 検証サービス"
    VF["Receipt::verify(imageId)"]
  end

  subgraph "結果"
    OK["success<br/>暗号学的に検証済み"]
    NG["failed<br/>証明が無効"]
    DM["dev_mode<br/>フェイクレシート"]
  end

  RCP --> VF
  EID --> VF
  VF --> OK
  VF --> NG
  VF --> DM

検証の 2 段階

STARK Verification では 2 つのチェックを実行します。

Image ID 照合: レシートに記録された Image ID が、デプロイ済みの期待 Image ID と一致するかを確認します。Image ID はゲストプログラムのバイナリから導出される暗号的識別子であり、ゲストプログラムが改変されていないことを保証します。

レシート検証: RISC Zero の Receipt::verify() を呼び出し、Seal(STARK 証明)がジャーナルと Image ID に対して暗号学的に正当であることを検証します。この検証は計算量が多いため、サーバー側の Rust 検証サービスで実行されます。

必要な証拠

証拠取得元説明
レシート(Seal + Journal)証明バンドルzkVM ホストが生成した STARK 証明
期待 Image IDimageId-mapping.jsonデプロイ済みゲストプログラムの暗号的識別子

開発モードの検出

RISC0_DEV_MODE=1 で生成されたレシートは InnerReceipt::Fake 型であり、暗号学的な保証を持ちません。検証サービスはこれを dev_mode ステータスとして報告します。チェック評価では、設定に応じて dev_modesuccess または not_run に正規化されます(既定では not_run 側)。

失敗モード

症状原因深刻度
Image ID 不一致マッピングが古い、またはプローバーイメージが異なる重大
レシート検証失敗証明が暗号学的に無効重大
開発モード検出フェイクレシートが混入重大(本番環境)

段階ステータスの集約

各段階のステータスは、その段階に属するチェックの結果から導出されます。

集約ルール条件
failedいずれかのチェックが failed
runningfailed がなく、いずれかのチェックが running
pendingfailed/running がなく、いずれかのチェックが pending
success全チェックが success
not_run上記のいずれにも該当しない

最終的な「Verified」表示の判定は、4 段階の結果に加えて追加のゲーティング条件を適用します。詳細は ゲーティングロジック を参照してください。