お断り: 本記事は C2PA Technical Specification v2.3(2026年4月時点)、c2pa-rs および c2patool の公式ドキュメントとソースコードを筆者が読解・整理したものです。SDK やツールのバージョンにより CLI フラグ名や挙動は変わる可能性があります。実装の根拠として用いる際は、必ず各ライブラリの最新ドキュメントおよびC2PA 公式仕様をご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。
はじめに
C2PA における「信頼(Trust)」のモデルは、仕様書を読むだけでは直感的に把握しにくい側面があります。そこで本記事では実践的なハンズオンを通じて、OpenSSL で独自のプライベート Root CA および EE(エンドエンティティ)証明書を発行し、c2pa-rs による署名から c2patool による真正性検証までの一連の流れを体験します。
本記事のゴールは以下の 2 点です。
- 標準の Trust List のみを参照する環境では、自前証明書による署名が
signingCredential.untrusted(未信頼の署名証明書)と判定される挙動を確認する - 検証側に自前の Root CA をトラストアンカーとして追加登録することで、全く同一の画像・Manifest が
signingCredential.trusted(信頼済み)へと遷移する状態変化を実証する
C2PA の Manifest や Trust List の基本概念については第4回および第7回で整理していますので、併せてご参照ください。
この記事で組み立てる全体像
全体の処理フローは以下の 3 フェーズで進行します。証明書の生成、c2pa-rs による署名、c2patool による多段階検証の構成です。
(素の画像)"] end subgraph S2["Step 2: 証明書を作る"] CA["Root CA
ca.pem / ca.key"] EE["EE 証明書
ee.pem / ee.key"] CA -->|発行| EE end subgraph S3["Step 3: 署名"] SIGN["c2pa-rs で署名"] OUT["output.jpg
(Manifest 入り)"] SIGN --> OUT end subgraph S45["Step 4-5: 検証"] VER{"c2patool で
チェーン照合"} NG["untrusted"] OK["trusted"] VER -->|アンカー無し| NG VER -->|ca.pem をアンカーに登録| OK end IMG --> SIGN EE -->|chain.pem として渡す| SIGN OUT --> VER CA -->|--trust_anchors| VER
登場する用語を先に整理
本記事では OpenSSL、c2pa-rs、c2patool を横断して操作するため、公開鍵暗号基盤(PKI)や X.509 証明書に関連する専門用語が頻出します。主要な用語の概要と登場箇所をあらかじめ整理します。
| 用語 | 概要 | 登場箇所 |
|---|---|---|
| openssl | 暗号鍵と X.509 証明書を生成・管理する CLI ツール。ecparam / req / x509 サブコマンドを使用 | Step 2 全般 |
| Root CA(ルート認証局) | 信頼の起点となる自己署名証明書。下位の証明書に対して正当性を保証する署名権限を持つ | ca.pem。Step 2 で生成、Step 5 で登録 |
| EE 証明書(End-Entity) | 証明書チェーンの末端に位置し、実際の署名者(主体)に紐づく証明書。下位証明書への署名権限は持たない | ee.pem。C2PA のアセット署名者に対応 |
| 証明書チェーン | EE 証明書から Root CA に至る階層的な証明書の束。検証側はチェーンを上位へ遡航して検証する | chain.pem(= ee.pem + ca.pem) |
| CSR(Certificate Signing Request) | EE 側が公開鍵と識別情報を CA に提出し、証明書発行を求める署名要求データ | ee.csr(OpenSSL が生成する中間ファイル) |
| PEM | 証明書や暗号鍵を Base64 テキストで表現したフォーマット(-----BEGIN...-----) | 各種 .pem ファイル |
| ECDSA P-256(prime256v1) | NIST 定義の楕円曲線を用いたデジタル署名アルゴリズム。C2PA における標準的な署名方式 | 鍵生成時に指定 |
| keyUsage | 証明書の基本的な暗号用途を制約する X.509v3 拡張。アセット署名には digitalSignature が必須 | v3_ee.cnf |
| extendedKeyUsage(EKU) | keyUsage をさらに細分化し、特定のアプリケーション用途を指定する OID 拡張 | v3_ee.cnf |
| Claim Signing 用 OID | C2PA の Claim 署名専用であることを示す識別子 1.3.6.1.4.1.311.76.59.1.9 | EKU に指定 |
| Claim Signing 用 EKU | 上記 OID を EKU に格納した状態。これが無いと c2pa-rs が署名時点で証明書を拒否する | EE 証明書の前提条件 |
| トラストアンカー | 検証側が「この認証局から連なるチェーンは無条件に信頼する」と事前に定めた起点証明書 | --trust_anchors=ca.pem |
| C2PA Trust List | C2PA アライアンスが公式に管理する適合ルート認証局のリスト(PEM 形式) | Step 5 での併用例 |
この表の中で、直接設定を記述するのは ca.pem(Root CA)と ee.pem(EE 証明書)の 2 枚のみで、残りはそこから派生して生成されるファイルです。
事前準備
使うツールは以下のとおりです。執筆時点の手元環境のバージョンを記載しておきます。
- Rust 1.94.0
- OpenSSL 3.6.2(自前 CA の発行用)
- c2patool 0.26.15(
trustサブコマンドと--trust_anchorsオプションを使う)
c2patool は cargo で入れるのが一番ラクです。
cargo install c2patool
c2patool --version
# => c2patool 0.26.15
作業ディレクトリを切っておきます。以降のコマンドはとくに断りがなければ、この /tmp/c2pa-trust-anchor/ をカレントにして実行する前提です。パスが .. や相対になっている箇所も、ここが起点だと思って読んでください。
cd /tmp
mkdir -p c2pa-trust-anchor && cd c2pa-trust-anchor
pwd
# => /tmp/c2pa-trust-anchor
Step 1: サンプル画像を用意
NASA アポロ17号のパブリックドメイン画像を拝借してきます。第5回と同じく、c2pa-rs リポジトリのテストフィクスチャから取ってくるのが手軽です。
curl -sL -o input.jpg \
https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/earth_apollo17.jpg
まだ素の JPEG なので、念のため確認しておきます。
c2patool input.jpg
# => Error: No claim found
Manifest が入っていないことをここで見ておくと、後で署名後との差分がクリアになります。
Step 2: 自前の Root CA と EE 証明書を OpenSSL で発行
C2PA の署名処理に用いる EE 証明書には、仕様上満たすべき厳格な技術要件があります。第7回で整理した条件のうち、自前環境の構築時に必須となる要件は以下の通りです。
- EE 証明書の
keyUsageにdigitalSignatureを含めること - EE 証明書の
extendedKeyUsageに Claim Signing 用 OID(1.3.6.1.4.1.311.76.59.1.9)を含めること - Root CA の
basicConstraintsをcritical, CA:TRUEとすること - 署名アルゴリズムとして ECDSA P-256(
prime256v1)を採用すること
Claim Signing 用 EKU が欠落している場合、c2pa-rs は署名実行時に CertificateProfileError(InvalidCertificate) を返して処理を中断します。規格適合上、極めて重要なポイントです。
設定ファイルを作成する
まず Root CA 用と EE 用の OpenSSL 設定ファイルを作ります。
cat > v3_ca.cnf <<'EOF'
[req]
distinguished_name = dn
x509_extensions = v3_ca
prompt = no
[dn]
CN = TechThanks Test Root CA
O = TechThanks Test
[v3_ca]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
EOF
cat > v3_ee.cnf <<'EOF'
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = 1.3.6.1.4.1.311.76.59.1.9
EOF
余談: この 2 つの cnf ファイルは何を宣言しているのか
設定ファイルの各行は「これから作る証明書にどんな情報・拡張を載せるか」を OpenSSL に伝える設計書です。-config / -extfile で読み込まれ、証明書発行時に X509v3 extensions として焼き込まれます。
v3_ca.cnf — Root CA 用
[req] セクションは openssl req コマンドの動作設定です。
distinguished_name = dn: Subject(発行先の名前)を[dn]セクションから読むx509_extensions = v3_ca:-x509で自己署名証明書を作るときの拡張を[v3_ca]から読むprompt = no: 対話プロンプトを出さずファイルの値をそのまま使う
[dn] セクションに書いた CN と O がそのまま Subject DN になります。自己署名なので、これが Issuer にもコピーされます。
[v3_ca] セクションが Root CA たる所以の本体です。
basicConstraints = critical, CA:TRUE: 「この証明書は CA である」と宣言。他の証明書に署名できるkeyUsage = critical, keyCertSign, cRLSign: 鍵の用途を制限。keyCertSignは証明書への署名、cRLSignは失効リストへの署名
v3_ee.cnf — EE(署名者)証明書用
こちらはセクションヘッダ無しでベタ書き。openssl x509 -req -extfile v3_ee.cnf で読むときは、ファイル全体が拡張セットとして解釈されます。
basicConstraints = CA:FALSE: 「この証明書は CA じゃない」= チェーンの末端。これより下には署名できないkeyUsage = critical, digitalSignature: データへの署名用途専用。keyCertSignが無いので他の証明書には署名不可extendedKeyUsage = 1.3.6.1.4.1.311.76.59.1.9: C2PA Claim Signing 専用の OID。これが無いと c2pa-rs が署名時点でCertificateProfileErrorを返して通してくれない
EE は「署名するだけの実働メンバー」、CA は「部下に権限を渡す管理職」、と役割が明確に分離されています。
critical フラグの意味
critical を付けた拡張は、検証ソフトが「この拡張の意味を知らない」場合に証明書全体を拒否します。付けないと無視されて通過してしまうので、信頼の根幹になる basicConstraints と keyUsage には必ず付けておきます。
なぜ Root 側と EE 側で設定ファイルを分けるのか
OpenSSL のコマンド体系では、Root CA 作成は openssl req -x509 -config v3_ca.cnf、EE 証明書発行は openssl x509 -req -extfile v3_ee.cnf で、読み込む設定形式が違います。req は [req] → [dn] → [v3_ca] という参照構造、x509 -extfile は「拡張だけ書いたファイル」をそのまま読むため、ファイルを分けて書いています。
実際に焼き込まれた結果を確認
cnf に書いた内容がそのまま証明書の拡張領域に入っていることを、作成後に確認できます。
openssl x509 -in ca.pem -noout -text | grep -A2 "X509v3"
# X509v3 Basic Constraints: critical
# CA:TRUE
# X509v3 Key Usage: critical
# Certificate Sign, CRL Sign
openssl x509 -in ee.pem -noout -text | grep -A1 "X509v3"
# X509v3 Basic Constraints:
# CA:FALSE
# X509v3 Key Usage: critical
# Digital Signature
# X509v3 Extended Key Usage:
# 1.3.6.1.4.1.311.76.59.1.9
Root CA の鍵と自己署名証明書を作る
# Root CA 秘密鍵(ECDSA P-256)
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
# Root CA 自己署名証明書(10年有効)
openssl req -new -x509 -key ca.key -out ca.pem -days 3650 -config v3_ca.cnf
これで「TechThanks 自前のルート CA」ができました。
EE 証明書を CA に発行してもらう
EE 側の鍵を作って CSR を出し、先ほどの Root CA で署名してもらいます。
# EE 秘密鍵
openssl ecparam -name prime256v1 -genkey -noout -out ee.key
# CSR
openssl req -new -key ee.key -out ee.csr \
-subj "/CN=TechThanks Test Signer/O=TechThanks Test"
# CA 署名で EE 証明書を発行
openssl x509 -req -in ee.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-out ee.pem -days 365 -extfile v3_ee.cnf
c2pa-rs の create_signer::from_files には、EE 証明書と Root CA 証明書を連結した「証明書チェーン PEM」を渡します。連結順序は EE 証明書を先頭、上位の Root CA を末尾とします。
cat ee.pem ca.pem > chain.pem
証明書の拡張属性を確認します。
openssl x509 -in ee.pem -noout -ext extendedKeyUsage
# => X509v3 Extended Key Usage:
# 1.3.6.1.4.1.311.76.59.1.9
Claim Signing EKU が正しく組み込まれていることが確認できます。
ここまでで生成した鍵ペア・証明書・署名要求の関係を下図に示します。EE 側は自身の鍵ペアから公開鍵を抽出して ee.csr(署名要求)を作成し、Root CA に提出します。Root CA は受け取った CSR の公開鍵に指定の拡張属性を付与した上で、自身の秘密鍵で電子署名を行い ee.pem を発行します。つまり ee.pem は EE が単独で生成するものではなく、認証局(CA)による信頼の付与を経て発行される成果物です。最後に EE 側が発行された ee.pem と公開されている ca.pem を連結して chain.pem を構成します。
鍵ペア(秘密鍵+公開鍵)")] CSR["ee.csr
署名要求
(公開鍵+Subject+EE自己署名)"] EEKEY -->|公開鍵を載せて
EE秘密鍵で所有証明| CSR end subgraph CAside["Root CA(発行する側)"] CAKEY[("ca.key
鍵ペア(秘密鍵+公開鍵)")] CAPEM["ca.pem
自己署名証明書"] CAKEY -->|公開鍵を載せて
秘密鍵で自己署名| CAPEM end CSR -->|①CAに提出| EEPEM["ee.pem
EE 証明書
(CSRの公開鍵+CAの署名)"] CAKEY -->|②CA秘密鍵で署名して発行| EEPEM EEPEM --> CHAIN["chain.pem
= ee.pem + ca.pem"] CAPEM --> CHAIN
この時点で /tmp/c2pa-trust-anchor/ はこういう状態です。
tree
# .
# ├── ca.key # Root CA の秘密鍵
# ├── ca.pem # Root CA 証明書(← これを後で --trust_anchors に渡す)
# ├── ca.srl # openssl x509 -CAcreateserial が作るシリアル管理ファイル
# ├── chain.pem # ee.pem + ca.pem を連結したチェーン(署名時に渡す)
# ├── ee.csr # EE の証明書署名要求
# ├── ee.key # EE の秘密鍵(← これも署名時に渡す)
# ├── ee.pem # Root CA に署名された EE 証明書
# ├── input.jpg # 署名対象のサンプル画像
# ├── v3_ca.cnf
# └── v3_ee.cnf
これ以降のステップで主役になるのは ca.pem(検証側のトラストアンカー)、chain.pem と ee.key(署名側が使う)、そして input.jpg の 4 つです。
Step 3: c2pa-rs で自前の証明書を使って署名
Rust のプロジェクトを /tmp/c2pa-trust-anchor/signer/ に切ります。証明書や画像は親ディレクトリに置いたままにして、コード側から ../ で参照する構成にすると、ファイルが二重管理にならずに済みます。
# /tmp/c2pa-trust-anchor/ にいる前提
cargo new signer
cd signer
cargo add c2pa --features file_io
cargo add serde_json
src/main.rs を以下の内容に差し替えます。chain.pem / ee.key / input.jpg はひとつ上の階層にあるので、パスを ../ 付きで指定しています。
// src/main.rs
use c2pa::{create_signer, Builder, SigningAlg};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut builder = Builder::default()
.with_definition(r#"{
"title": "my_photo.jpg",
"claim_generator_info": [{
"name": "TechThanksTrustAnchorDemo",
"version": "1.0.0"
}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.created",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
}]
},
"created": true
}]
}"#)?;
// 自前のチェーン PEM と EE 秘密鍵で署名者を作る
let signer = create_signer::from_files(
"../chain.pem",
"../ee.key",
SigningAlg::Es256,
Some("http://timestamp.digicert.com".to_string()),
)?;
builder.sign_file(&*signer, "../input.jpg", "../output.jpg")?;
println!("署名完了: ../output.jpg");
Ok(())
}
第5回で使った EphemeralSigner との違いは、create_signer::from_files で自前の PEM と鍵を渡しているところだけです。署名者を差し替えれば、Builder 側のコードは同じまま動きます。
ビルドして署名します。
cargo run
# => 署名完了: ../output.jpg
署名が終わったところで、作業ディレクトリ全体を見直しておきます(ビルド成果物の signer/target/ は大きいので除外)。
cd .. # /tmp/c2pa-trust-anchor/ に戻る
tree -I target
# .
# ├── ca.key
# ├── ca.pem
# ├── ca.srl
# ├── chain.pem
# ├── ee.csr
# ├── ee.key
# ├── ee.pem
# ├── input.jpg # 署名前の元画像
# ├── output.jpg # 自前 EE 証明書で署名した画像
# ├── signer
# │ ├── Cargo.lock
# │ ├── Cargo.toml
# │ └── src
# │ └── main.rs
# ├── v3_ca.cnf
# └── v3_ee.cnf
以降の c2patool の検証コマンドも、この /tmp/c2pa-trust-anchor/ をカレントにしたまま実行します。
Step 4: c2patool で検証(トラストアンカーなし)
まずはトラストアンカーを何も指定せず、素の状態で c2patool に食わせます。--detailed を付けると validation_results が JSON で返ってくるので、そこを抜き出します。
c2patool output.jpg --detailed | jq '.validation_results'
validation_results の中身はだいたいこんな形になります(抜粋)。
{
"activeManifest": {
"success": [
{ "code": "claimSignature.insideValidity" },
{ "code": "claimSignature.validated" },
{ "code": "assertion.hashedURI.match" },
{ "code": "assertion.dataHash.match" }
],
"informational": [
{ "code": "timeStamp.untrusted" }
],
"failure": [
{
"code": "signingCredential.untrusted",
"explanation": "signing certificate untrusted"
}
]
}
}
claimSignature.insideValidity や assertion.hashedURI.match は成功しており、Manifest 自体の暗号学的整合性は保たれています。一方で signingCredential.untrusted が failure 配列に含まれています。これは、アセットのハッシュや署名ダイジェスト自体は数学的に完全であるものの、署名に用いられた証明書が検証側の信頼するルート認証局に紐づいていないことを示しています。
この状態が「暗号としては正当だが、認証局として未信頼」という評価です。
Step 5: 自前 Root CA をトラストアンカーに登録
C2PA 仕様の Section 14.4.1 C2PA Signers では、検証側は C2PA 公式の Trust List を含めた上で、独自のトラストアンカーを追加定義することが認められています。ここではその規定に沿い、自前で生成した ca.pem を検証側のトラストアンカーとして追加します。
検証側が実行する信頼性判定のメカニズムを下図に示します。output.jpg 内に埋め込まれた EE 証明書から Root CA までのチェーンを遡航し、そのルート証明書が指定されたトラストアンカー群の中に存在するかどうかで最終判定が分岐します。
(ee.pem)"] CAIN["Root CA 証明書
(ca.pem)"] EEIN -->|チェーンをたどる| CAIN end CAIN --> MATCH{"Root がアンカーに
登録されているか?"} ANCHOR[("--trust_anchors
で渡した PEM")] ANCHOR --> MATCH MATCH -->|一致| OK["signingCredential.trusted"] MATCH -->|未登録| NG["signingCredential.untrusted"]
c2patool の trust サブコマンドでは、--trust_anchors オプションでトラストアンカーの PEM ファイルを指定できます(環境変数 C2PATOOL_TRUST_ANCHORS でも指定可能です)。複数の Root CA を信頼対象とする場合は、PEM を連結した単一ファイルを渡します。
--detailed フラグを付与して、同じディレクトリにある ca.pem をトラストアンカーに指定して実行します。
c2patool output.jpg --detailed trust --trust_anchors=ca.pem \
| jq '.validation_results'
{
"activeManifest": {
"success": [
{ "code": "signingCredential.trusted",
"explanation": "signing certificate trusted, found in System trust anchors" },
{ "code": "claimSignature.insideValidity" },
{ "code": "claimSignature.validated" },
{ "code": "assertion.hashedURI.match" },
{ "code": "assertion.dataHash.match" }
],
"informational": [
{ "code": "timeStamp.untrusted" }
],
"failure": []
}
}
failure 配列が空になり、signingCredential.trusted が success に記録されました。注目すべきは、同一の output.jpg に対して画像ファイル自体には一切手を加えず、検証側のトラストアンカー設定を変更しただけで判定結果が遷移した点です。これが C2PA における「検証者主体の信頼評価モデル」の本質です。
環境変数による設定
CI/CD パイプラインや常駐検証サーバーにおいては、CLI オプションの代わりに環境変数で指定する構成が有効です。
# /tmp/c2pa-trust-anchor/ をカレントにした状態で
export C2PATOOL_TRUST_ANCHORS="$(pwd)/ca.pem"
c2patool output.jpg --detailed trust \
| jq '.validation_results.activeManifest.success[] | select(.code=="signingCredential.trusted")'
# => { "code": "signingCredential.trusted", ... }
C2PA 公式の Trust List と自前 CA を併用したい場合は、公式 PEM と自前 PEM を結合したファイルを生成し、それを --trust_anchors に指定します。
# /tmp/c2pa-trust-anchor/ にいることを確認
pwd
# => /tmp/c2pa-trust-anchor
curl -sL -o c2pa-trust-list.pem \
https://raw.githubusercontent.com/c2pa-org/conformance-public/refs/heads/main/trust-list/C2PA-TRUST-LIST.pem
cat c2pa-trust-list.pem ca.pem > anchors.pem
# 連結できたか軽く確認(Trust List の枚数 + 自前 Root CA 1 枚になっていれば OK)
grep -c "BEGIN CERTIFICATE" anchors.pem
c2patool output.jpg --detailed trust --trust_anchors=anchors.pem \
| jq '.validation_results'
実務運用においては、自社プライベート CA のライフサイクルを内部管理しつつ、公式 Trust List の更新を定期的に同期取得してマージする運用設計が一般的です。
想定されるユースケース
自前のプライベート CA をトラストアンカーとして組み込む構成は、以下のような実践シナリオで威力を発揮します。
- 社内専用のワークフローやメディア管理基盤において先行導入を進めたい場合。公式 Trust List への登録審査を待たずに、検証側システム(社内 CMS やアセットレビュー環境)をプライベート CA 基準で直ちに本稼働させることが可能
- パブリック CA から C2PA 証明書を購入・調達する前段として、PoC やステージング環境で
Trusted判定を含む E2E のパイプライン挙動を完全に検証したい場合 - 公式 Trust List とは独立して、特定の提携パートナー企業との間でのみ相互信頼関係を結び、アセット流通の閉域検証を行いたい場合
一方で、一般の Web 空間へ広く公開し不特定多数のユーザー環境で検証させるアセットについては、各プラットフォームが保持する公式 Trust List に登録された正規 CA の証明書で署名する必要があります。
まとめ
本記事では、自前で発行した Root CA を c2patool のトラストアンカーに追加することで、同一アセットの検証判定が signingCredential.untrusted から signingCredential.trusted へと変化する実態をコードとコマンドで検証しました。要点を整理します。
- 署名側(c2pa-rs)では、EE 証明書の EKU に C2PA Claim Signing OID(
1.3.6.1.4.1.311.76.59.1.9)を正しく組み込む必要がある - 検証側(c2patool)では、
--trust_anchorsオプションまたは環境変数を用いて、信頼すべきルート認証局の PEM を動的に注入できる - 「信頼(Trusted)」の可否はアセット内に固定された情報ではなく、コンテンツを検証・消費する側の信頼ポリシーによって決定される
C2PA の信頼モデルは、「どの認証局を正当と認めるか」を検証システム側が主体的に決定・変更できる柔軟なアーキテクチャの上に成り立っています。この構造を理解しておくことで、社内向けプライベート CA の運用からパブリック CA による広域運用まで、要件に応じた最適なシステム設計が可能となります。
参考リンク
- C2PA Technical Specification v2.3
- C2PA Trust List(GitHub)
- c2pa-rs
- c2patool 使い方ドキュメント
- C2PA Certificate Policy
TechThanks は Content Credentials の実装支援に取り組んでいます。C2PA の署名基盤構築や検証パイプラインの設計、自前 CA 運用の設計までお気軽にご相談ください。