お断り: 本記事は C2PA Technical Specification v2.3(2026年4月時点)と c2pa-rs(コミット 0eeabc7のソースコードおよびテストフィクスチャを筆者が読解し、c2patool による実データダンプで確認しながら整理したものです。仕様は継続的に更新されており、細部の記述や用語は変更される可能性があります。実装や設計判断の根拠として用いる際は、必ずC2PA 公式仕様および関連する一次資料をご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。

はじめに

本記事は「C2PA 実装入門」シリーズの第4回です。第3回で整理した Assertion・Claim・Claim Signature・JUMBF コンテナという 4 つのレイヤーが、実際のファイル内でどのように表現されているかを、c2pa-rs 付属のテスト画像と c2patooljq を組み合わせて段階的に確認していきます。

構造の概念や各レイヤーの役割は第3回で整理しているため、まだ読まれていない方は先に目を通すか、本記事を読み進めながら必要に応じて参照してください。記事の最後では、実データの構造把握が自前の検証パイプライン設計にどう直結するかを整理し、第5回の実装ハンズオンへ繋げます。

前提: 登場人物のおさらい

実データを確認する前に、前編で扱った構成要素の階層構造を再確認しておきます。

Manifest Store
└── Manifest
    ├── Assertion Store  (Assertion の実体の入れ物)
    │   ├── Assertion
    │   ├── Assertion
    │   └── ...
    ├── Claim            (Assertion への参照リスト+メタ情報)
    └── Claim Signature  (Claim に対する署名)

押さえておきたいのは、Claim と Assertion が包含関係ではなく並列に配置される点、そして Claim は Assertion の実データを持たず hashed URI による参照のみを保持する点です。この仕様が c2patool の JSON 出力の構造に直接影響するため、念頭に置いておくと理解がスムーズになります。詳細については前編の登場人物と全体像をご覧ください。

セットアップ

本記事の JSON とツリー図はすべて、contentauth/c2pa-rs リポジトリに含まれるテスト画像 sdk/tests/fixtures/C_with_CAWG_data.jpgc2patool で読み出した結果から抜粋しています。このファイルは Claim v2 で署名されており、CAWG(Creator Assertions Working Group)の身元情報や AI 学習オプトアウトの Assertion まで含まれているため、現行仕様の実運用に近い構造を把握するのに適した題材です。

c2patool のインストール手順は c2pa-rs README の Installation セクションを参照してください。環境が整っていれば、以下のコマンドで手元でも同様の JSON を再現できます。

cd /tmp
git clone --depth 1 https://github.com/contentauth/c2pa-rs.git
c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg

c2patool に画像パスを渡すと、そのアセットに埋め込まれている Manifest Store の内容を JSON 形式で標準出力に出力します。以降で取り上げる JSON 例はいずれもこのコマンドの出力抜粋であるため、手元の実行結果と照合しながら読み進めてみてください。

c2patool の JSON 出力は、ファイル内に埋め込まれたバイナリの生データをそのまま表示しているわけではありません。実際のファイル内部では Manifest は JUMBF ボックスとして格納され、Claim は CBOR バイナリ、Claim Signature は COSE_Sign1 バイナリ形式です。c2patool(内部的には c2pa-rs の Manifest::from_store)がこれらをパースし、Claim フィールドの展開や署名情報の抽出、一部 Assertion の専用フィールド化を行って、可読性の高い JSON 形式に整形してくれています。前編で確認した「Assertion / Claim / Claim Signature は並列に配置される別個のボックス」という構造が 1 つの manifest オブジェクトに統合されて出力されるため、生データとは見え方が異なる箇所が存在します。本記事ではこの点に留意しながら読み解いていきます。

トップレベルの形

出力は階層が深いので、まず jq 'keys' でトップレベルのキーだけ見てみます。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg | jq 'keys'
[
  "active_manifest",
  "manifests",
  "validation_results",
  "validation_state",
  "validation_status"
]

active_manifest は「いま有効な Manifest の URN」、manifests がその Manifest 本体の入れ物、validation_results 以下は c2patool が読み込み時に走らせた検証の結果です。前編の登場人物図でいうと、manifests が「Manifest Store → Manifest」のところに相当します。

Manifest を特定する

manifests の下はさらに Manifest URN をキーとしたオブジェクトになっていて、キーを抜き出すと active_manifest と同じ URN が出てきます。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg | jq '.manifests | keys'
[
  "urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d"
]

この urn:c2pa:... は C2PA 仕様書 Section 8.1 Uniquely Identifying C2PA Manifests and Assets で定義されている、個々の Manifest を一意に識別する URN で、Claim v2 の書式に従った urn:c2pa:<UUID> の形をしています。1 つのアセットに複数の Manifest が時系列で連なる場合は、ここに複数の URN が並び、active_manifest はそのうち「末尾の有効な Manifest」を指すポインタになります。

余談: Claim v1 と v2 で URN の書式が違う話

URN の書式は Claim のバージョンによって2系統に分かれていて、同じ仕様書の中でも Claim v2 と Claim v1 で違う名前空間が使われます。

Claim バージョン書式名前空間定義箇所
Claim v2(現行仕様の本流)urn:c2pa:<UUID>[:<claim-generator-id>[:<version_reason>]]urn:c2pa:v2.3 Section 8.1
Claim v1(旧形式)[<vendor>:]urn:uuid:<UUID>urn:uuid:v1.4 仕様書

ABNFc2pa-namespace = "urn:c2pa:" と定義されているのは前者の Claim v2 用の URN です。v1 仕様書には Manifest 識別子の正式な ABNF はなく、JUMBF URI リファレンスの例として urn:uuid:<UUID> 形式が示されているのみで、vendor プレフィックス付きの形は規定されていません。それでも c2pa-rs は Claim v1 を生成する際に vendor 指定オプションを受け付け、<vendor>:urn:uuid:<UUID> という形のラベルを出力するようになっており(Claim::new(sdk/src/claim.rs#L379))、他のテストフィクスチャ(例: C.jpg)には contentauth:urn:uuid:<UUID> のような SDK 由来の拡張形式が残っています。識別子まわりの規定が v1 では緩く、v2 で厳密化された経緯がここから読み取れます。

Manifest の第一階層を覗く

続いてこの Manifest オブジェクトの第一階層を覗いてみます。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg | jq '.manifests[] | keys'
[
  "assertions",
  "claim_generator_info",
  "claim_version",
  "instance_id",
  "label",
  "signature_info",
  "thumbnail",
  "title"
]

前編で整理した登場人物が一通り揃っています。「Claim 本体はどこ?」と気になるところですが、実はこの manifest オブジェクト自体が Claim の中身を展開したものです。claim_version, claim_generator_info, label, instance_id, assertions, title — これらはすべて Claim の CBOR ドキュメントに含まれるフィールドであり、c2patool(内部的には c2pa-rs の Manifest::from_store(sdk/src/manifest.rs#L380))が Claim から各フィールドを展開し、manifest オブジェクトの直下にフラットに並べてくれています。

同様に signature_info は Claim Signature(COSE_Sign1 バイナリ構造)の中身を、同ファイル L655alg, issuer, common_name, time などに分解して、人間に読める形に整形したものです。

JUMBF の生のボックス構造では c2pa.claim(Claim 本体の CBOR)と c2pa.signature(COSE_Sign1 バイナリ)が別々のボックスとして Manifest の直下に並んでいますが、c2patool はそれらを 1 つの manifest オブジェクトに統合して出力しています。対応関係を表でまとめておきます。

c2patool の JSON フィールドJUMBF 上の実体
assertionsc2pa.assertions ボックス(Assertion Store)
claim_version, claim_generator_info, label, instance_id, titlec2pa.claim ボックス(Claim 本体の CBOR から展開)
signature_infoc2pa.signature ボックス(COSE_Sign1 から要約)
thumbnailAssertion Store 内の c2pa.thumbnail.claim を専用フィールドとして引き出したもの

Assertion 配列の形

次に assertions 配列の中身を見ます。まずは各要素が持っているキーを確認します。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg | jq '.manifests[].assertions[] | keys'
[
  "created",
  "data",
  "label"
]
[
  "data",
  "label"
]
[
  "data",
  "label"
]

配列には 3 件の Assertion オブジェクトが入っていて、それぞれ最低でも label(識別子)と data(中身)を持っています。先頭の 1 件には created: true も付いていて、これは前編で触れた created_assertions / gathered_assertions の 2 系統分離が、実データのフラグとして現れたものです。Claim の created_assertions フィールドに含まれていた Assertion に対して c2patoolcreated: true を付けて出力しています(ManifestAssertion::created(sdk/src/manifest_assertion.rs#L48-L50))。

Assertion のラベル一覧

続いて label だけ一覧化して、何のラベルが並んでいるかを確認します。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg | jq '.manifests[].assertions[].label'
"c2pa.actions.v2"
"cawg.training-mining"
"cawg.identity"

c2pa.actions.v2 はコンテンツに対して行われた操作の記録(一覧)、cawg.training-mining は AI 学習・推論での利用可否の宣言、cawg.identity は作成者の身元情報です。cawg.*CAWG(Creator Assertions Working Group) が整備している C2PA の拡張仕様で、各 Assertion の中身はこの後で詳しく見ていきます。

JUMBF の生の Assertion Store

ここで 1 つ注意点があります。JUMBF の実際のボックス構造にはもう 2 つ、サムネイル c2pa.thumbnail.claim とアセット本体のハードバインディング c2pa.hash.data が含まれているはずですが、この assertions 配列には出力されていません。c2patool が JSON を整形する際に、サムネイルは専用フィールド(thumbnail)として引き出し、ハードバインディングは検証用に内部参照するだけで、人間向けの assertions 配列からは除外しているためです。

両者が実際に Assertion Store に収まっていることは、validation_resultsassertion.hashedURI.match に並ぶ URL から確認できます。hashed URI の末尾を抽出すると、生の Assertion Store の中身がそのまま確認できます。

c2patool c2pa-rs/sdk/tests/fixtures/C_with_CAWG_data.jpg \
  | jq -r '.validation_results.activeManifest.success[]
           | select(.code == "assertion.hashedURI.match")
           | .url | split("/") | .[-1]'
c2pa.thumbnail.claim
c2pa.actions.v2
c2pa.hash.data
cawg.training-mining
cawg.identity

これで Assertion Store の全容が確認できました。

全体像の再確認

ここまで確認してきた内容をまとめると、Manifest Store の全体像はこのツリー図になります。

c2pa (Manifest Store)
└── urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d  (Manifest)
    ├── c2pa.assertions  (Assertion Store)
    │   ├── c2pa.thumbnail.claim
    │   ├── c2pa.actions.v2
    │   ├── c2pa.hash.data
    │   ├── cawg.training-mining
    │   └── cawg.identity
    ├── c2pa.claim
    └── c2pa.signature

参考までに、c2patool の出力から最上位部分だけを抜き出したものを並べておきます。

{
  "active_manifest": "urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d",
  "manifests": {
    "urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d": {
      "claim_generator_info": [
        { "name": "c2pa cawg test", "version": "0.58.0", "org.contentauth.c2pa_rs": "0.58.0" }
      ],
      "title": "C_with_CAWG_data.jpg",
      "assertions": [
        { "label": "c2pa.actions.v2", "data": { "...": "..." } },
        { "label": "cawg.training-mining", "data": { "...": "..." } },
        { "label": "cawg.identity", "data": { "...": "..." } }
      ],
      "signature_info": { "...": "..." },
      "label": "urn:c2pa:822f2ec0-ef27-4d95-88b4-74586c12873d",
      "claim_version": 2
    }
  }
}

各 Assertion の中身

次に各 Assertion の data フィールドを 1 つずつ確認していきます。まずは c2pa.actions.v2

{
  "label": "c2pa.actions.v2",
  "data": {
    "actions": [
      {
        "action": "c2pa.created",
        "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
      }
    ],
    "allActionsIncluded": true
  },
  "created": true
}

c2pa.created は「このコンテンツを初回作成した」ことを示すアクションで、digitalSourceType には IPTC の digitalsourcetype ボキャブラリから digitalCapture(カメラなどで実際に撮影されたもの)が入っています。これがあると、下流のポリシー判定で「生成 AI 由来かどうか」を機械的に振り分けられます。

次に cawg.training-mining

{
  "label": "cawg.training-mining",
  "data": {
    "entries": {
      "cawg.ai_inference":            { "use": "notAllowed" },
      "cawg.ai_generative_training":  { "use": "notAllowed" }
    }
  }
}

entries の下に AI 推論(cawg.ai_inference)と生成 AI 学習(cawg.ai_generative_training)の 2 軸があって、どちらも notAllowed、つまり「このコンテンツを AI 学習や推論に使うのは認めない」という宣言です(CAWG Training and Data Mining Assertion 仕様)。

そして cawg.identity

{
  "label": "cawg.identity",
  "data": {
    "signer_payload": {
      "referenced_assertions": [
        { "url": "self#jumbf=c2pa.assertions/cawg.training-mining", "hash": "..." },
        { "url": "self#jumbf=c2pa.assertions/c2pa.hash.data",       "hash": "..." }
      ],
      "sig_type": "cawg.x509.cose"
    },
    "signature_info": { "alg": "Ed25519", "issuer": "C2PA Test Signing Cert" }
  }
}

referenced_assertions は「作成者がどの Assertion の内容を自分の署名で裏付けるか」を指定するリストです。ここでは cawg.training-mining(AI 学習オプトアウトの宣言)と c2pa.hash.data(アセット本体ハッシュ)の 2 つを作成者が追認している、という形になっています。signature_info.algEd25519 になっていて、あとで見る Claim Signature 側の Es256 とは別アルゴリズムなのがポイントです。Claim Signature がカバーするスコープとは独立した、作成者自身による部分的な追認として動いています。

署名情報

Claim Signature の情報は signature_info に現れます。

{
  "signature_info": {
    "alg": "Es256",
    "issuer": "C2PA Test Signing Cert",
    "common_name": "C2PA Signer",
    "cert_serial_number": "640229841392226413189608867977836244731148734950",
    "time": "2025-07-29T23:13:49+00:00"
  }
}

alg は C2PA が許容する署名アルゴリズムの識別子、time は RFC 3161 タイムスタンプ局から得た署名時刻です。このテスト画像では Es256(ECDSA with SHA-256)が使われていて、先ほどの cawg.identity 側の Ed25519 と別鍵・別アルゴリズムの組み合わせになっています。同じ Manifest の中で Claim Signature と CAWG identity signature がそれぞれ独立した信頼の枝を構成している、というのがここからはっきり見て取れます。

検証結果

最後に validation_results を見ておきます。c2patool は Manifest を読み込む際に裏で検証も走らせていて、ステップごとの成否がそのまま並びます。

{
  "validation_results": {
    "activeManifest": {
      "success": [
        { "code": "timeStamp.validated",      "explanation": "timestamp message digest matched: DigiCert SHA256 RSA4096 Timestamp Responder 2025 1" },
        { "code": "claimSignature.validated", "explanation": "claim signature valid" },
        { "code": "assertion.hashedURI.match","explanation": "hashed uri matched: self#jumbf=c2pa.assertions/c2pa.actions.v2" },
        { "code": "assertion.hashedURI.match","explanation": "hashed uri matched: self#jumbf=c2pa.assertions/cawg.identity" },
        { "code": "assertion.dataHash.match", "explanation": "data hash valid" }
      ],
      "failure": [
        { "code": "signingCredential.untrusted", "url": ".../c2pa.signature",    "explanation": "signing certificate untrusted" },
        { "code": "signingCredential.untrusted", "url": ".../cawg.identity",     "explanation": "signing certificate untrusted" }
      ]
    }
  }
}

assertion.hashedURI.match は Claim から Assertion への参照が改変されていないこと、assertion.dataHash.match はアセット本体とハードバインディングのハッシュが一致していること、timeStamp.validated は Claim Signature に付随する RFC 3161 タイムスタンプの検証結果を表しています。上の例では、テスト用証明書が Trust List に入っていないので signingCredential.untrustedfailure として 2 件(Claim Signature 本体と cawg.identity 内の作成者署名)挙がっていますが、これは想定どおりの結果です。failure が 2 件並んでいるのは、まさに Claim Signature と CAWG identity signature が独立した署名であることの現れでもあります。

検証パイプラインを設計するときの観点

ここまで実データで見てきた構造を、自前の検証パイプラインに落とし込むとどうなるか、整理しておきます。C2PA Manifest の構造を掴んでおくと、どこで何を検査すべきかがクリアになります。

ステップ検査対象
1. JUMBF パースManifest Store の取り出しと構造妥当性
2. Assertion ハッシュ照合Claim に列挙された hashed URI と Assertion Store の整合性
3. Hard Binding 検証アセット本体ハッシュと c2pa.hash.data の値の一致
4. Claim Signature 検証COSE_Sign1 の署名検証
5. 証明書チェーン検証C2PA Trust List への適合と失効状態
6. アクション解釈c2pa.actions などのビジネスロジック上の解釈

ステップ 1〜5 は C2PA SDK(c2pa-rsc2pa-python など)が標準で処理を担っており、先ほど確認した validation_results の内訳がそのままこのステップ 1〜5 の成否に対応しています。ステップ 6 はユースケースごとに自前で設計する領域で、たとえば「AI 生成由来のコンテンツは社内配信のみ許可する」「特定の認証局で署名された写真のみニュース素材として採用する」といったポリシーをここに実装します。CAWG の身元情報を併用するなら「作成者の身元が特定の組織に紐付いている素材だけを採用する」といった追加判定もこのレイヤーで実装します。

まとめ

c2patooljq を組み合わせることで、C2PA Manifest の内部構造を実データに沿って段階的に把握できます。前編で整理した概念モデルが、本記事で確認した通り JSON の中に忠実に反映されており、「Assertion が並ぶ Assertion Store」「それを参照リストとして束ねる Claim」「束全体を保証する Claim Signature」という論理構造をコマンドの出力結果を通じて実感していただけたかと思います。

自前の検証パイプラインを設計する際や、既存の C2PA ワークフローのトラブルシューティングにおいて「現在どのレイヤーで何が起きているか」を特定する際にも、本記事で紹介した jq クエリのパターンが役立つはずです。C2PA Manifest を単なるブラックボックスとして扱うのではなく、部品単位で挙動を把握・制御できる技術基盤として活用していきましょう。

続く第5回 SDK でコンテンツに署名してみるでは、これまでの「読み取り側」の視点から「生成・署名側」へと切り替えます。c2pa-rs および c2pa-python を用いて、コンテンツへ実際に Manifest を付与する実装コードを解説していきます。

参考リンク


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