お断り: 本記事は 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 でしか参照しないこと、この 2 点です。ここが 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

5 つ揃いました。これがこの Manifest の 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 を「SDK のブラックボックス」ではなく「部品単位で説明できる仕組み」として扱う助けになれば嬉しいです。

続く第5回 SDK でコンテンツに署名してみるでは、ここまで「読む側」だった視点を「作る側」に切り替えます。c2pa-rs と c2pa-python を使って、自分のコンテンツに Manifest を付与するコードを実際に書いていきます。

参考リンク


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