お断り: 本記事は 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 が裏で勝手に処理してくれるフェーズと、ユースケースごとに自前で組むポリシー判定に分かれます。
| 担当 | 内容 |
|---|---|
| SDK | JUMBF パース、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() は読み込みと同時に暗号学的検証を走らせてくれます。検証に失敗してもエラーにはならず、結果は内部に持ったままです。つまり Reader の生成が成功したからといって Manifest が信頼できるわけではないので、必ず validation_state() と検証結果の詳細を見る必要があります。
validation_state の解釈
validation_state() は検証結果を大まかに示してくれます。
Valid: 署名とハッシュは正しいが、証明書が Trust List に載っていない(テスト証明書はここ)Trusted: 署名・ハッシュ・証明書チェーンすべてが検証を通過Invalid: 改ざん検知やパースエラーなど、構造的な問題あり
検証結果の code の代表例も並べておきます。
| code | 意味 |
|---|---|
claimSignature.validated | Claim Signature の署名値が正しい |
assertion.hashedURI.match | Claim から Assertion への参照が改変されていない |
assertion.dataHash.match | アセット本体のハッシュが一致 |
timeStamp.validated | RFC 3161 タイムスタンプの検証成功 |
signingCredential.untrusted | 署名証明書が Trust List に載っていない |
signingCredential.revoked | 署名証明書が失効している |
assertion.dataHash.mismatch | アセット本体が改ざんされている |
コードの全一覧は仕様書の Section 15.2.2 “Standard Status Codes” にまとまっています。
ポリシー判定を追加する
検証パイプラインの締めくくりは、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 の情報を一切信頼せず、改ざんが検知された旨をログに残したうえで、後段の処理にコンテンツを渡さないようにします。
証明書期限切れ・失効時
signingCredential.revoked や証明書の有効期限切れが検出されたときの対処は、ユースケースによって割れます。ニュース素材なら「署名時点のタイムスタンプが有効期限内だったかどうか」を確認するだけで通せる場面もあれば、法的証拠としての利用なら厳密に弾くべき場面もあります。
Manifest が存在しない場合
C2PA Manifest が埋め込まれていないコンテンツをどう扱うかも、設計上の悩みどころです。Reader::default().with_file() は Manifest がないファイルに対してエラーを返してくるので、ここのフォールバック処理は事前に設計しておく必要があります。
まとめ
C2PA の検証パイプラインは、Reader API を呼ぶだけで暗号学的検証が一通り片付き、開発者は validation_state() の確認とポリシー判定ロジックの設計に集中できる構造になっています。
ポリシー判定の典型パターンとしては、c2pa.actions.v2 の digitalSourceType による AI 生成判定、cawg.training-mining による学習オプトアウト確認、署名者の CA による振り分けあたりがあります。どれも Manifest 内の Assertion を読み取って、受け入れ可否を組み立てていくロジックです。
続く第7回では、テスト環境から本番環境に移すときに付いて回る証明書取得、ワークフロー設計、パフォーマンス考慮、規制動向について整理していきます。
参考リンク
- C2PA Technical Specification v2.3
- C2PA Validation - Chapter 15
- C2PA Standard Status Codes
- c2pa-rs Reader API ドキュメント(docs.rs)
- c2pa-rs Rust SDK
- C2PA 実装入門 第5回 SDK でコンテンツに署名してみる
TechThanks は Content Credentials の実装支援に取り組んでいます。C2PA SDK の導入や検証パイプラインの設計についてお気軽にご相談ください。