お断り: 本記事は C2PA Technical Specification v2.3(2026年4月時点)および c2pa-rs の公式ドキュメントとソースコードを筆者が読解して整理したものです。SDK のバージョンアップにより API が変更される可能性があります。実装の根拠として用いる際は、必ず各ライブラリの最新ドキュメントおよび C2PA 公式仕様をご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。

はじめに

本記事は「C2PA 実装入門」シリーズの第6回です。第5回では SDK でコンテンツに Manifest を付与する側を扱いました。今回は視点を「署名されたコンテンツを受け取って検証する側」に切り替え、c2pa-rs の Reader API を用いた検証パイプラインの実装方法を解説します。

第2回で整理した検証フェーズの理論を、実際のコードを通じて段階的に確認していきます。なお、本番運用に向けた証明書調達やインフラ設計については第7回で詳しく扱います。

検証の全体像

C2PA Manifest の検証は、仕様書 Chapter 15 “Validation” において厳密に定義されています。この検証プロセスは、SDK が自動的に実行する暗号学的検証フェーズと、ユースケースに応じてアプリケーション側で実装するポリシー判定フェーズに大別されます。

担当内容
SDKJUMBF パース、hashed URI 照合、Hard Binding 検証、署名検証、タイムスタンプ検証、証明書失効確認
自前実装Assertion の内容に基づくビジネスロジック(ポリシー判定)

SDK を呼び出すだけで低レイヤーの暗号学的検証が一括して実行されるため、開発者は業務要件に応じたポリシー判定の実装に集中できます。ただし、SDK が返す検証ステータスコードを正しく解釈し、改ざんや失効発生時のエラーハンドリングを適切に設計しておくことが不可欠です。

セットアップ

Rust が未インストールの場合は、公式サイトの手順に従ってセットアップしてください。

cd /tmp
mkdir -p c2pa-test-verify && cd c2pa-test-verify
cargo init .
cargo add c2pa --features file_io
cargo add serde_json

# 検証対象として第4回で使ったテスト画像をダウンロード(Manifest 付き)
curl -sL -o signed.jpg https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/C_with_CAWG_data.jpg

プロジェクト構成は以下の通りです。

c2pa-test-verify/
├── Cargo.lock
├── Cargo.toml
├── signed.jpg
└── src/
    └── main.rs

c2patool で、Manifest が入っていることを確かめておきます。

c2patool signed.jpg | jq '.active_manifest'
# => "urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d"

Reader API で Manifest を読み出す

src/main.rs を以下の内容に書き換えます。Reader API を用いて画像ファイルから Manifest Store を読み出し、検証結果、Assertion 一覧、署名者情報を出力する基本的な実装例です。

// src/main.rs
use c2pa::Reader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let reader = Reader::default().with_file("signed.jpg")?;

    // 検証状態
    println!("Validation state: {:?}", reader.validation_state());

    // アクティブな Manifest
    if let Some(manifest) = reader.active_manifest() {
        println!("Active Manifest: {}", manifest.label().unwrap_or("unknown"));
        println!("Title: {:?}", manifest.title());
        println!("Claim version: {:?}", manifest.claim_version());

        // Assertion の一覧
        println!("\nAssertions:");
        for assertion in manifest.assertions() {
            println!("  - {} (created: {})", assertion.label(), assertion.created());
        }

        // 署名情報
        if let Some(sig) = manifest.signature_info() {
            println!("\nSignature info:");
            println!("  alg: {:?}", sig.alg);
            println!("  issuer: {:?}", sig.issuer);
            println!("  time: {:?}", sig.time);
        }
    }

    // 検証結果の詳細(JSON から取得)
    let json: serde_json::Value = serde_json::from_str(&reader.json())?;
    if let Some(results) = json.get("validation_results") {
        if let Some(active) = results.get("activeManifest") {
            if let Some(failures) = active.get("failure").and_then(|f| f.as_array()) {
                println!("\nFailures:");
                for f in failures {
                    println!("  [NG] {} - {}",
                        f["code"].as_str().unwrap_or(""),
                        f["explanation"].as_str().unwrap_or(""));
                }
            }
            if let Some(successes) = active.get("success").and_then(|s| s.as_array()) {
                println!("\nSuccesses ({} items):", successes.len());
                for s in successes.iter().take(3) {
                    println!("  [OK] {} - {}",
                        s["code"].as_str().unwrap_or(""),
                        s["explanation"].as_str().unwrap_or(""));
                }
                if successes.len() > 3 {
                    println!("  ... and {} more", successes.len() - 3);
                }
            }
        }
    }

    Ok(())
}

ビルドして実行します。

cargo run

実行すると、以下のような検証結果が出力されます。

Validation state: Valid
Active Manifest: urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d
Title: Some("C_with_CAWG_data.jpg")
Claim version: Some(2)

Assertions:
  - c2pa.actions.v2 (created: true)
  - cawg.training-mining (created: false)
  - cawg.identity (created: false)

Signature info:
  alg: Some(Es256)
  issuer: Some("C2PA Test Signing Cert")
  time: Some("2025-07-29T23:13:49+00:00")

Failures:
  [NG] signingCredential.untrusted - signing certificate untrusted

Successes (9 items):
  [OK] timeStamp.validated - timestamp message digest matched: ...
  [OK] claimSignature.insideValidity - claim signature valid
  [OK] claimSignature.validated - claim signature valid
  ... and 6 more

Reader::default().with_file() は、ファイルの読み込みと同時に暗号学的検証を自動的に実行します。ここで重要なのは、検証に失敗した場合でも関数自体はエラー(Err)を返さず、検証結果のステータスとして内部に保持される点です。つまり、Reader インスタンスの生成に成功したことと、Manifest が真正であることは同義ではありません。必ず validation_state() や詳細ステータスを確認して判定を行う必要があります。

validation_state の解釈

validation_state() は、総合的な検証状態を以下の 3 つのステータスで表現します。

  • Valid: 署名とハッシュは正しいが、証明書が Trust List に載っていない(テスト証明書はここ)
  • Trusted: 署名・ハッシュ・証明書チェーンすべてが検証を通過
  • Invalid: 改ざん検知やパースエラーなど、構造的な問題あり

検証結果の code の代表例も並べておきます。

code意味
claimSignature.validatedClaim Signature の署名値が正しい
assertion.hashedURI.matchClaim から Assertion への参照が改変されていない
assertion.dataHash.matchアセット本体のハッシュが一致
timeStamp.validatedRFC 3161 タイムスタンプの検証成功
signingCredential.untrusted署名証明書が Trust List に載っていない
signingCredential.revoked署名証明書が失効している
assertion.dataHash.mismatchアセット本体が改ざんされている

コードの全一覧は仕様書の Section 15.2.2 “Standard Status Codes” にまとまっています。

ポリシー判定を追加する

検証パイプラインの実装において最も重要なのは、Manifest 内の Assertion を読み解き、自社の受け入れ基準(ビジネスロジック)に照らして判定を下す部分です。先ほどのコードにポリシー判定処理を追加します。src/main.rs を以下のように更新してください。

// src/main.rs
use c2pa::Reader;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let reader = Reader::default().with_file("signed.jpg")?;

    // --- 検証状態の確認 ---
    let state = reader.validation_state();
    println!("Validation state: {:?}", state);

    let manifest = reader.active_manifest()
        .ok_or("No active manifest found")?;

    // --- ポリシー判定: AI 生成コンテンツの振り分け ---
    let mut is_ai_generated = false;
    for assertion in manifest.assertions() {
        if assertion.label() == "c2pa.actions.v2" {
            if let Ok(data) = assertion.value() {
                if let Some(actions) = data["actions"].as_array() {
                    for action in actions {
                        let source_type = action["digitalSourceType"]
                            .as_str().unwrap_or("");
                        if source_type.contains("trainedAlgorithmicMedia") {
                            is_ai_generated = true;
                        }
                    }
                }
            }
        }
    }
    println!("AI generated: {}", is_ai_generated);

    // --- ポリシー判定: AI 学習オプトアウトの確認 ---
    let mut training_allowed = true;
    for assertion in manifest.assertions() {
        if assertion.label() == "cawg.training-mining" {
            if let Ok(data) = assertion.value() {
                let training = &data["entries"]["cawg.ai_generative_training"]["use"];
                if training == "notAllowed" {
                    training_allowed = false;
                }
            }
        }
    }
    println!("Training allowed: {}", training_allowed);

    // --- 署名者の確認 ---
    if let Some(sig) = manifest.signature_info() {
        let issuer = sig.issuer.as_deref().unwrap_or("unknown");
        println!("Signed by: {}", issuer);
    }

    // --- 最終判定 ---
    let json: serde_json::Value = serde_json::from_str(&reader.json())?;
    let has_tampering = json["validation_results"]["activeManifest"]["failure"]
        .as_array()
        .map(|failures| failures.iter().any(|f|
            f["code"].as_str() == Some("assertion.dataHash.mismatch")
        ))
        .unwrap_or(false);

    if has_tampering {
        println!("REJECT: content has been tampered with");
    } else if is_ai_generated {
        println!("FLAG: AI-generated content, internal use only");
    } else if !training_allowed {
        println!("NOTE: training/mining not allowed for this content");
    } else {
        println!("ACCEPT: content is verified");
    }

    Ok(())
}
cargo run
Validation state: Valid
AI generated: false
Training allowed: false
Signed by: C2PA Test Signing Cert
NOTE: training/mining not allowed for this content

今回のテスト画像は digitalCapture(カメラ撮影)であるため AI 生成物としてはフラグ付けされず、一方で cawg.training-mining の宣言に基づき training_allowed: false と判定されていることが分かります。このように、組織のコンプライアンス要件やサービス規約に応じた柔軟なポリシーエンジンを構築できます。

エラーハンドリング

本番パイプラインを運用する上では、特に以下の 3 つのエラーパターンに対するハンドリング方針を定めておく必要があります。

改ざん検知時

assertion.dataHash.mismatch が検出された場合、アセット本体が署名後に改ざんされたことを意味します。Manifest に記述された主張は一切信頼せず、インシデントとしてログに記録した上で、後続の公開・配信処理を即座に遮断(Reject)します。

証明書期限切れ・失効時

signingCredential.revoked(証明書失効)や有効期限切れに対する扱いは、サービスの要件によって方針が分かれます。報道コンテンツ等であれば、署名時に取得された RFC 3161 タイムスタンプが証明書の有効期間内であることを確認して受け入れる運用も一般的ですが、金融・公的証明などの高セキュリティ要件では即座に棄却する厳格な運用が求められます。

Manifest が存在しない場合

そもそも C2PA Manifest が付与されていない一般コンテンツをどう扱うかも重要な設計事項です。Reader::default().with_file() は Manifest が存在しないファイルに対してエラーを返すため、通常の未署名コンテンツとしてパスさせるのか、あるいは未検証として警告を付与するのか、業務フローに応じたフォールバックを定義しておく必要があります。

まとめ

C2PA の検証パイプラインは、Reader API を活用することで複雑な暗号学的検証を抽象化し、開発者が validation_state() の解釈とビジネスロジック(ポリシー判定)に専念できるアーキテクチャとなっています。

ポリシー判定の代表例としては、c2pa.actions.v2digitalSourceType による AI 生成コンテンツの判別、cawg.training-mining による機械学習オプトアウトの遵守確認、署名元 CA の検証によるコンテンツの信頼性区分などが挙げられます。いずれも Manifest 内の各種 Assertion を抽出・評価することで実現されます。

続く第7回では、開発環境から本番運用へ移行する際に不可欠となる正規の証明書調達、署名インフラのワークフロー設計、スループットやレイテンシへの配慮、そして各国の規制動向について包括的に解説します。

参考リンク


TechThanks は Content Credentials の実装支援に取り組んでいます。C2PA SDK の導入や検証パイプラインの設計についてお気軽にご相談ください。