お断り: 本記事は 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 例はすべて、このコマンドの出力の該当部分を抜粋したものなので、実行結果と照らし合わせながら読み進めてください。

トップレベルの形

出力は階層が深いので、まず 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"
]

前編で整理した登場人物が一通り顔を揃えています。assertions が Assertion Store の中身、signature_info が Claim Signature の情報、label が Manifest 自身の URN、claim_version が Claim 仕様のバージョン(このテスト画像では 2)です。thumbnail はサムネイル Assertion を c2patool が専用フィールドとして引き出したもの、instance_idtitle は 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 も付いていて、これは Claim v2 の Section 10.2.2 Fields で規定される created_assertions フィールド(Claim 生成元自身が作成した Assertion を hashed URI で列挙するフィールド)に含まれていたことを示すマーカーです。c2patool は各 Assertion オブジェクトの出力時に、この列挙に含まれているかどうかをフラグ化して created: true として見せてくれています(ManifestAssertion::created フィールド(sdk/src/manifest_assertion.rs#L48-L50))。前編で触れた created_assertionsgathered_assertions の2系統分離が、ここで実データのフラグとして現れているわけです。

余談: どの Assertion が created になるのか(c2pa-rs の判定ロジック)

Claim v2 で Assertion を created_assertions に入れるか gathered_assertions に入れるかは、c2pa-rs では Claim::claim_assertion_type(sdk/src/claim.rs#L1395-L1420) が決めています。以下のいずれかに当てはまると Created 扱いになります。

  1. ラベルが HASH_LABELSc2pa.hash.data / c2pa.hash.boxes / c2pa.hash.bmff / c2pa.hash.collection)に含まれる(ハードバインディング系は自動で created)
  2. 呼び出し側で Claim::add_created_assertion を使って明示的に created として追加した
  3. 設定 settings.builder.created_assertion_labels に当該ラベルが列挙されている
  4. 上記いずれにも該当しない場合は Gathered

Builder 側の入口としては AssertionDefinition.created フィールド(sdk/src/builder.rs#L297) があり、ここに true を設定しておくと最終的に add_created_assertion 経由で Claim に組み込まれます。

今回のテスト画像で c2pa.actions.v2 だけが created: true になっているのは、テストデータ生成時に actions assertion に対してこのフラグが立てられていたためです。actions は「今この Claim Generator が何を作ったか/何を編集したか」という記述なので、Claim の署名者自身に帰属する= created_assertions に入れるのが自然、という運用になっています。逆に cawg.identitycawg.training-mining は、Assertion 自体がコンテンツの属性を述べるもので Claim 生成元が作った行為の記述ではないので、デフォルトのまま gathered_assertions 側に入っています。

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"

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

5つ揃いました。これがこの Manifest の Assertion Store 全体で、前編のツリー図の「Assertion」ノードに実際のラベルを当てはめた姿です。

全体像の再確認

ここまで確認してきたことをまとめると、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

前編の登場人物図の各ノードに、実データの URN とラベルが入った形になっています。Claim と Claim Signature は Assertion Store の外側、Manifest の直下に並ぶサブボックスで、Assertion とは包含関係ではなく並列に配置されます。

参考までに、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 フィールドを個別に覗いてみます。まずは 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.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 が「作成者がどの事実に名前を貸しているか」のリストです。ここでは 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(Adobe の c2pa-rsc2pa-node など)が標準で対応しており、先ほど見た validation_results の内訳がそのままこのステップ1〜5の成否に対応しています。ステップ6はユースケースごとに自前で設計する領域で、たとえば「AI 生成由来のコンテンツは社内配信のみ許可する」「特定の認証局で署名された写真のみニュース素材として採用する」といったポリシーをここで実装します。CAWG の身元情報を併用するなら「作成者の身元が特定の組織に紐付いている素材だけを採用する」のような追加判定もこの層で行うことになります。

まとめ

c2patooljq を組み合わせれば、C2PA Manifest の内部構造を実データに対して段階的に確認できます。前編で整理した概念モデルが、本記事で見た通り JSON の中に素直に現れており、「Assertion が並ぶ Assertion Store、Claim が参照リストとしてそれを束ね、Claim Signature がその束に署名する」という構造が一つひとつのコマンド結果として追えることを体感していただけたと思います。

自前の検証パイプラインを組む際や、既存の C2PA 実装のトラブルシューティングで「今どこの層で何が起きているか」を確認するときにも、本記事で紹介した jq クエリのパターンがそのまま使えるはずです。前編で扱った概念と合わせて、C2PA Manifest を「SDK のブラックボックス」ではなく「部品単位で説明可能な仕組み」として扱う助けになれば幸いです。

参考リンク


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