本文へスキップ
ecdsa.com
← すべての記事

C2PA コンテンツ認証情報:マニフェスト、署名、限界

c2pa-python 0.38.1(c2pa-rs 0.91.3)で JPEG に署名し、未信頼の署名者と信頼済みの署名者の両方として検証し、1 バイトを変更し、一般的な画像処理パイプラインに通しました。引用している仕様の本文は、現行版の C2PA 2.4 です。

要点:C2PA マニフェストは、メディアファイルについての署名付きの記述です。アサーション(「カメラで作成」や「生成 AI で編集」など何が行われたかと、ファイルのバイト列のハッシュ)を列挙したクレームに COSE で署名したもので、署名に使う鍵の X.509 証明書は署名の中に格納されています。バリデーターはマニフェストを Well-Formed(整形式)、Valid(有効。署名とハッシュが正しい)、Trusted(信頼済み。署名者の証明書がトラストアンカー、デフォルトでは C2PA トラストリストにつながる)のいずれかとして報告します。RFC 3161 タイムスタンプがあれば、証明書の有効期限が切れた後も有効なままです。一方、写真が真実を写していることは証明できず、ファイルを再エンコードするツールを通ると残りません。有効な認証情報は「この署名者がこのバイト列を保証している」ことを示すものとして扱い、認証情報がないことは何の情報にもならないものとして扱ってください。

マニフェストの中身

C2PA 仕様(バージョン 2.4、2026年4月)では、マニフェストはいくつかの部分で構成されます。

  • アサーション:それぞれがラベル付きの CBOR または JSON のボックスです。c2pa.actions.v2 はアセットに対して行われたことを列挙し(2.1 以降、「c2pa.created または c2pa.opened のいずれかが必須になった」)、カメラによる撮影と生成 AI を区別する IPTC 語彙の digitalSourceType を含みます。c2pa.hash.data はハードバインディングで、マニフェスト自体が格納されている範囲を除いた、アセットのバイト列に対するハッシュです。その後にサムネイル、素材(このアセットの作成元となったアセット)、メタデータが続く場合もあります。
  • クレーム:「一連のアサーションを参照する、デジタル署名された改ざん検知可能なデータ構造」です。参照はハッシュ付き URI によるため、どのアサーションを変更してもクレームが成り立たなくなります。
  • クレーム署名:クレームに対する COSE_Sign1 で、保護ヘッダーに署名者の証明書チェーンを含みます。「ID の認証情報(identity credential)はちょうど 1 つ存在しなければならない」とされており、バージョン 2.0 以降、その認証情報は X.509 証明書です。
  • 任意で、RFC 3161 のタイムスタンプ局が署名に対して発行するタイムスタンプを含めることができます。

これらはすべて JUMBF ボックスとしてシリアライズされ、ファイル内(JPEG では APP11 セグメント、MP4 や HEIF ではボックス)に格納されるか、リモートに保存されて URL で参照されます。アセットが編集されると、編集ツールは新しいマニフェストを追加し、以前のマニフェストを素材として記録します。そのため、マニフェストストアはマニフェストのチェーンになります。

使用できる署名アルゴリズムは ES256、ES384、ES512、PS256、PS384、PS512、Ed25519 です(§13.2.1)。鍵はアルゴリズムに適合していなければならず、RSA 鍵は 2048 ビット以上でなければなりません。ライブ動画については、同一のセグメントをキャッシュ可能に保つため、仕様は決定的なアルゴリズムを推奨しています。

JPEG への署名

実験では、pyca/cryptography 49.0.0 でテスト用 CA を作成し、C2PA バリデーターが確認する EKU を持つ P-256 の署名者証明書を作成します。EKU は c2pa-kp-claimSigning(1.3.6.1.4.1.62558.2.1)と、古いバリデーター向けの id-kp-emailProtection です。続いて、生成した 640×360 の JPEG に DigiCert のタイムスタンプ付きで署名します。

lab.py(抜粋):マニフェストの定義と署名(c2pa-python 0.38.1)
manifest = {
    "claim_generator_info": [{"name": "ecdsa.com C2PA lab", "version": "1.0"}],
    "title": "sunrise.jpg",
    "assertions": [
        {"label": "c2pa.actions.v2", "data": {"actions": [
            {"action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"}
        ]}}
    ],
}
signer = c2pa.Signer.from_info(c2pa.C2paSignerInfo(alg=b"es256", sign_cert=chain_pem, private_key=key_pem, ta_url=b"http://timestamp.digicert.com"))
builder = c2pa.Builder.from_json(manifest)
signed = io.BytesIO()
builder.sign(signer, "image/jpeg", io.BytesIO(original), signed)

証明書のプロファイルは重要です。署名者証明書は、鍵用途の digitalSignature、CA:FALSE、クレーム署名用の EKU を持ち、ルートは keyCertSign を持つ CA です。適切な EKU のない証明書や、自己署名のリーフ証明書は、署名が数学的に正しくても検証に失敗します。これらの拡張については X.509 証明書のフィールドで解説しています。

検証:Valid、Trusted、Invalid

同じスクリプトで、ファイルを 4 回読み込みます。信頼の設定をしない場合、テスト用ルートをトラストアンカーに設定した場合、画像データを 1 バイト変更した場合、そしてアップロード処理のパイプラインと同じように Pillow 12.0.0 で画像をデコードして再エンコードした場合です。

lab.py(抜粋):トラストアンカーを指定した読み込み、改ざん、再エンコード
trusted = c2pa.Context.from_dict({"trust": {"trust_anchors": pem(root).decode()}, "verify": {"verify_trust": True}})
read("signed, test root as trust anchor", signed, trusted)

# Change one byte of the compressed image data (after the JPEG Start of Scan marker).
sos = signed.rfind(b"\xff\xda")
tampered = bytearray(signed)
tampered[sos + 200] ^= 0x01
read("one byte of image data changed", bytes(tampered), trusted)

# What many upload pipelines do: decode and re-encode, dropping metadata.
Image.open(io.BytesIO(signed)).save(out := io.BytesIO(), "JPEG", quality=85)
read("re-encoded by an image pipeline", out.getvalue(), trusted)
出力(python:3.14.8-slim-trixie)
c2pa-python 0.38.1, 0.91.3
JPEG: 13376 bytes unsigned, 52438 bytes with the manifest (+39062)
--- signed, no trust anchors configured: validation_state = Valid
    status codes: timeStamp.validated, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted, signingCredential.untrusted
    signer: Example Newsroom / Example Newsroom Photo Desk, alg Es256, time 2026-10-22T06:43:11+00:00
    assertions: ['c2pa.actions.v2']
    assertion store: ['c2pa.actions.v2', 'c2pa.hash.data', 'c2pa.thumbnail.claim']
    certificate chain in the signature: 981 bytes DER
--- signed, test root as trust anchor: validation_state = Trusted
    status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted
--- one byte of image data changed: validation_state = Invalid
    status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, signingCredential.ocsp.skipped, timeStamp.untrusted, assertion.dataHash.mismatch
--- re-encoded by an image pipeline: no C2PA manifest found

出力の読み方は次のとおりです。

  • Valid だが未信頼:暗号学的なチェックはすべて合格しています(claimSignature.validated、3 つのアサーションに対する 3 つの assertion.hashedURI.match、画像のバイト列に対する assertion.dataHash.match)。しかし signingCredential.untrusted です。誰でも CA を作成して署名できるからです。仕様はこの状態を厳密に定義しています。マニフェストが Valid となるのは、整形式で、改変されておらず、署名が「有効期間内(inside validity)」で検証され、認証情報が失効していない場合です。Trusted となるのは、それに加えて署名用の認証情報が「signingCredential.trusted の成功コードを受け取る」場合だけです(§14.3)。
  • Trusted:バリデーターがルートを認識すると、この状態になります。本番環境では、そのルートは C2PA トラストリストから取得します。これは「ハードウェアとソフトウェアの署名者に証明書を発行する、X.509 証明書のトラストアンカーの C2PA 管理リスト」で、バリデーターは c2pa-kp-claimSigning EKU についてこれを含めなければなりません。ユーザーは独自のアンカーを追加できます。
  • Invalid:1 バイト変更した後の状態です。署名は依然として正しいものの、ハードバインディングが一致しません(assertion.dataHash.mismatch)。切り抜き、フィルター、再圧縮でも同じ結果になるため、編集ツールは古いマニフェストをそのまま残すのではなく、新しいマニフェストを書き込まなければなりません。
  • 消失:再エンコード後の状態です。マニフェストはメタデータに格納されているため、ピクセルをデコードして新しいファイルを書き出すパイプラインでは、EXIF と同様に削除されます。出力には、認証情報がかつて存在したことを示すものは何も残りません。

マニフェストによって、13 KB の画像に 39 KB が加わりました。クレームとこの実験の 981 バイトの証明書チェーンに加えて、c2pa-rs が生成したサムネイルと、DigiCert の証明書を含むタイムスタンプトークンが格納されているためです。timeStamp.untrusted が表示されるのは、実験で C2PA TSA トラストリストを設定しなかったためです。タイムスタンプは暗号学的には有効で、単にそのタイムスタンプ局がバリデーターのリストに含まれていなかっただけです。

タイムスタンプと証明書の有効期間

仕様の記述は明快です。「タイムスタンプのないマニフェストは、署名用の認証情報の有効期限が切れるか失効した時点で有効ではなくなる」とされ、タイムスタンプが存在して信頼されている場合、バリデーターは証明書の確認に「現在時刻ではなく、証明された時刻を使用しなければならない」とされています(§10.3.2.5.1、§15.8.2)。これは Authenticode と同じ仕組みです。証明書は署名の時点で有効でありさえすればよく、それがいつだったかはタイムスタンプ局が証明します。オフラインで署名するデバイスには別の解決策が必要です。たとえば Google の Pixel カメラは「デバイス上の信頼できるタイムスタンプ」を使い、写真が「デバイスがオフラインのときに撮影されたものであっても、証明書の有効期限が切れた後も信頼できる」ようにしています(Google Security Blog)。

信頼の根拠

署名者の ID は証明書のサブジェクトです。そのため、認証情報の価値は、誰がその証明書を発行し、何を確認したかによって決まります。C2PA は適合性プログラムとトラストリストを運営しています。バリデーターはトラストリストを EKU ごとに設定し、notBefore と notAfter の日付を指定してアンカーを追加できます。ハードウェアも重要です。Google によると、Pixel 10 のカメラアプリは「C2PA 適合性プログラムが現在定める最高のセキュリティ評価である Assurance Level 2 を達成した」とのことです。クレームの署名鍵は「Titan M2 セキュリティチップ内の Android StrongBox を使って生成・保管」され、証明書は、Android Key Attestation(鍵のアテステーション)によって正規のデバイスとアプリであることが証明された後にのみ発行されます。セキュアなハードウェア内の鍵については、ハードウェアで保護された鍵で解説しています。

署名者の記述といえるのは、署名されたアサーションだけです。仕様には次のような例があります。「EXIF メタデータに含まれる GPS 位置情報の正確さについて、たとえそのメタデータがハードバインディングの対象であっても、署名者は責任を負わない」(§9.2.6)。署名者に帰属するのは、c2pa.metadata アサーションに格納されたメタデータだけです。

コンテンツ認証情報にできないこと

  • 画像が真実であることは証明しない:カメラの認証情報が示すのは、このデバイスがこのバイト列を生成したということです。演出された場面や画面を撮影した写真も、カメラによる撮影であることに変わりはありません。生成 AI の認証情報は、それを付けることを選んだツールによる記述にすぎません。
  • 簡単に削除できる:今回の再エンコードでは、エラーもなくマニフェストが削除されました。また、プライバシーやサイズの理由で、意図的にメタデータを削除することもよくあります。仕様が用意している答えはソフトバインディングです。これは、リポジトリでマニフェストを再び見つけられるようにするフィンガープリントや不可視の電子透かしです。永続的なコンテンツ認証情報(Durable Content Credential)とは、「マニフェストリポジトリでの発見を可能にするソフトバインディングが 1 つ以上存在する」認証情報のことです(§2.4.1)。個々のプラットフォームがその方法でマニフェストを保存・検索するかどうかは、プラットフォーム次第です。
  • ないことは何も意味しない:Web 上のほとんどの画像には認証情報がありません。マニフェストがないことは、改変の証拠にはなりません。
  • 意図した以上の情報が漏れることがある:素材のチェーン、デバイス名、位置情報はファイルと一緒に伝わります。作成者は、アセットが素材になるときにアサーションを削除(redaction)できます(§6.8)。ツールは、何を含めるかを利用者が選べるようにすべきです。

C2PA を使って開発する場合

  1. パイプラインで保持するか再署名する:サービスでアップロードされたファイルをリサイズまたは圧縮する場合は、元のファイルを認証情報ごと保持するか、元のファイルを素材とし、実際の処理を正しく表す c2pa.edited または c2pa.resized アクションを記録した新しいマニフェストを作成して、自分の証明書で署名してください。
  2. 検証状態を正確に表示する:Trusted と単なる Valid を区別し、証明書から取得した署名者名を表示し、マニフェストがない場合は特別な表示をしないでください。
  3. C2PA トラストリストと最新のソフトウェアで検証する:signingCredential.untrusted や timeStamp.untrusted などのステータスコードは、通常は偽造ではなく設定の問題を示しています。
  4. 証明書を正しく構成する:c2pa-kp-claimSigning EKU を設定し、鍵は HSM またはセキュアなハードウェアに保管し、すべての署名にタイムスタンプを付けてください。
  5. 認証情報がないことを判断材料にしない:認証情報がない、または削除されているのは通常の状態です。

出典