お断り: 本記事は C2PA Technical Specification v2.3(2026年4月時点)および c2pa-rsc2pa-python の公式ドキュメントとソースコードを筆者が読解して整理したものです。SDK のバージョンアップにより API が変更される可能性があります。実装の根拠として用いる際は、必ず各ライブラリの最新ドキュメントおよび C2PA 公式仕様をご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。

はじめに

本記事は「C2PA 実装入門」シリーズの第5回です。第4回では c2patool を使って Manifest の内部構造を実データから確認しました。本記事ではいよいよ SDK を用いて、コンテンツに Manifest を付与する実装を進めていきます。c2pa-rs(Rust)と c2pa-python(Python)の双方でコード例を示し、テスト用証明書の生成から署名済み画像の確認まで、一気通貫で実践できる構成としています。

検証パイプラインの実装は第6回で扱います。

c2pa-rs(Rust)での署名

セットアップ

Rust が未インストールの場合は、公式サイトの手順に沿って rustup 経由でセットアップしてください。

インストール後、プロジェクトを作成して依存クレートを追加し、サンプル画像を用意します。

cd /tmp
mkdir -p c2pa-test-rust && cd c2pa-test-rust
cargo init .
cargo add c2pa --features file_io
cargo add serde_json

# サンプル画像をダウンロード(NASA アポロ17号撮影、パブリックドメイン)
curl -sL -o input.jpg https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/earth_apollo17.jpg

ここまでのコマンドを実行すると、プロジェクトは以下の構造になります。

c2pa-test-rust/
├── Cargo.lock
├── Cargo.toml
├── input.jpg
└── src/
    └── main.rs

まず c2patool で、サンプル画像に Manifest がまだ入っていないことを確かめておきます。

c2patool input.jpg
# => Error: No claim found

まだ何も署名されていない通常の JPEG です。ここに Manifest を付与していくことが本記事の目的です。

なお、file_io フィーチャーを有効にしておくことで、ファイルパスを直接指定して読み書きする便利な API が利用可能になります。

最小限の署名コード

src/main.rs を以下の内容に書き換えます。テスト用の証明書は EphemeralSigner が内部で自動生成するため、OpenSSL による事前の鍵・証明書作成は不要です。

// src/main.rs
use c2pa::{Builder, EphemeralSigner};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Builder を JSON 定義から生成
    // c2pa.actions.v2 に c2pa.created を含めるのは仕様上の必須要件
    let mut builder = Builder::default()
        .with_definition(r#"{
            "title": "my_photo.jpg",
            "claim_generator_info": [{
                "name": "MyApp",
                "version": "1.0.0"
            }],
            "assertions": [{
                "label": "c2pa.actions.v2",
                "data": {
                    "actions": [{
                        "action": "c2pa.created",
                        "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
                    }]
                },
                "created": true
            }]
        }"#)?;

    // テスト用の一時署名者を生成(自己署名 CA + EE 証明書を自動生成)
    let signer = EphemeralSigner::new("C2PA Test Signer")?;

    // 入力画像に Manifest を付与して出力
    builder.sign_file(
        &signer,
        "input.jpg",
        "output.jpg",
    )?;

    println!("署名完了: output.jpg");
    Ok(())
}

EphemeralSigner はテスト・開発専用の署名用クラスで、インスタンス生成時に自己署名 CA とエンドエンティティ(EE)証明書をバックグラウンドで自動生成し、その鍵ペアで署名を行います。C2PA Trust List に登録された正規の認証局ではないため、検証時には signingCredential.untrusted が出力されますが、Manifest の構造確認やハッシュ検証を行う上では何ら支障ありません。本番環境向けの証明書取得は第7回で扱います。

ビルドして実行します。sign_file は出力先に同名ファイルが既に存在しているとエラーになるため、再実行する際はあらかじめ rm -f output.jpg で削除してください。

cargo run
# => 署名完了: output.jpg

sign_file は拡張子から入力ファイルのフォーマットを自動判定し、JUMBF コンテナとして Manifest Store を埋め込んだ output.jpg を生成します。

署名結果を c2patool で確認

c2patool output.jpg

第4回で確認した JSON と同様の形式で、付与した Manifest の内容が確認できます。claim_generator_infoMyApp が反映されていること、および validation_resultssigningCredential.untrusted(未信頼の証明書)以外のエラーが含まれていないことを確認します。

c2pa-python(Python)での署名

セットアップ

uv が未インストールの場合は、公式サイトの手順に沿ってインストールしてください。

cd /tmp
mkdir -p c2pa-test-python && cd c2pa-test-python
uv init .
uv add c2pa-python

# c2pa-rs のテスト用証明書と秘密鍵をダウンロード(CA 署名済みチェーン)
curl -sL -o test_cert.pem https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/certs/es256.pub
curl -sL -o test_key.pem https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/certs/es256.pem

# サンプル画像をダウンロード(NASA アポロ17号撮影、パブリックドメイン)
curl -sL -o input.jpg https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/earth_apollo17.jpg

c2pa-python は自己署名証明書に対応していないため、ここでは c2pa-rs リポジトリで提供されているテスト用の CA 署名済み証明書チェーンを利用します(執筆時点の c2pa-python には Rust 版の EphemeralSigner に相当する手軽なヘルパーが提供されていないためです)。プロジェクト構成は以下の通りです。

c2pa-test-python/
├── input.jpg
├── main.py
├── pyproject.toml
├── README.md
├── test_cert.pem
├── test_key.pem
└── uv.lock

最小限の署名コード

main.py を以下の内容で作成します。

# main.py
import c2pa
import json

# テスト用証明書と秘密鍵を読み込み
with open("test_cert.pem", "rb") as f:
    cert = f.read()
with open("test_key.pem", "rb") as f:
    key = f.read()

# Signer を生成(TSA URL には DigiCert の無料タイムスタンプサービスを指定)
signer_info = c2pa.C2paSignerInfo("es256", cert, key, b"http://timestamp.digicert.com")
signer = c2pa.Signer.from_info(signer_info)

# Manifest 定義を JSON で作成
# c2pa.actions.v2 に c2pa.created を含めるのは仕様上の必須要件
manifest_json = json.dumps({
    "title": "my_photo.jpg",
    "claim_generator_info": [{
        "name": "MyPythonApp",
        "version": "1.0.0",
    }],
    "assertions": [{
        "label": "c2pa.actions.v2",
        "data": {
            "actions": [{
                "action": "c2pa.created",
                "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
            }],
        },
        "created": True,
    }],
})

# Builder で入力画像に Manifest を付与して出力
builder = c2pa.Builder(manifest_json)
builder.sign_file("input.jpg", "output.jpg", signer)

print("署名完了: output.jpg")

画像と証明書をプロジェクトルートに配置して実行します。

uv run main.py

Rust 版と同様に、sign_file がファイル形式を自動判別し、Manifest を埋め込んだ output.jpg を生成します。c2pa-python は内部で c2pa-rs のネイティブバインディングを呼び出しているため、提供される署名機能は Rust 版と概ね同等です。

Assertion を追加する

最小構成のコードでは、Manifest 定義 JSON に直接 c2pa.actions.v2 を記述しました。続いて、Builder API の add_assertion メソッドを活用して、プログラムから動的に Assertion を追加する実装方法を確認します。第4回で取り上げた cawg.training-mining(AI 学習・マイニングのオプトアウト宣言)を追加した完全なコード例を示します。

Rust 版

// src/main.rs
use c2pa::{Builder, EphemeralSigner};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut builder = Builder::default()
        .with_definition(r#"{
            "title": "my_photo.jpg",
            "claim_generator_info": [{
                "name": "MyApp",
                "version": "1.0.0"
            }],
            "assertions": [{
                "label": "c2pa.actions.v2",
                "data": {
                    "actions": [{
                        "action": "c2pa.created",
                        "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
                    }]
                },
                "created": true
            }]
        }"#)?;

    // AI 学習オプトアウトの Assertion を追加
    builder.add_assertion(
        "cawg.training-mining",
        &serde_json::json!({
            "entries": {
                "cawg.ai_inference":           { "use": "notAllowed" },
                "cawg.ai_generative_training": { "use": "notAllowed" }
            }
        }),
    )?;

    let signer = EphemeralSigner::new("C2PA Test Signer")?;
    builder.sign_file(&signer, "input.jpg", "output.jpg")?;

    println!("署名完了: output.jpg");
    Ok(())
}

Python 版

# main.py
import c2pa
import json

with open("test_cert.pem", "rb") as f:
    cert = f.read()
with open("test_key.pem", "rb") as f:
    key = f.read()

signer_info = c2pa.C2paSignerInfo("es256", cert, key, b"http://timestamp.digicert.com")
signer = c2pa.Signer.from_info(signer_info)

manifest_json = json.dumps({
    "title": "my_photo.jpg",
    "claim_generator_info": [{
        "name": "MyPythonApp",
        "version": "1.0.0",
    }],
    "assertions": [
        {
            "label": "c2pa.actions.v2",
            "data": {
                "actions": [{
                    "action": "c2pa.created",
                    "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
                }],
            },
            "created": True,
        },
        {
            "label": "cawg.training-mining",
            "data": {
                "entries": {
                    "cawg.ai_inference":           {"use": "notAllowed"},
                    "cawg.ai_generative_training": {"use": "notAllowed"},
                },
            },
        },
    ],
})

builder = c2pa.Builder(manifest_json)
builder.sign_file("input.jpg", "output.jpg", signer)

print("署名完了: output.jpg")

cawg.training-mining を付与することで、「このコンテンツを AI モデルの学習や推論に利用することを許可しない」という作成者の意思を Manifest 内に明記できます。上記 Rust 版では add_assertion メソッドで個別に追加していますが、Python 版の例のように定義 JSON 内の assertions 配列にまとめて記述しても同様に動作します(Rust 版でも定義 JSON への一括記述が可能です)。

Ingredient(素材の取り込み)

画像の編集・加工ワークフローにおいては、加工前の元画像が持つ Manifest を来歴として継承することが極めて重要です。第2回で解説した「Manifest のチェーン(来歴の連鎖)」を実際に構築してみましょう。先ほど署名した original_signed.jpg を Ingredient(素材・原材料)として取り込み、編集後の画像に対して新たな Manifest を付与する手順を実装します。

// src/main.rs
use c2pa::{Builder, EphemeralSigner};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --- Step 1: 元画像に署名 ---
    let mut builder1 = Builder::default()
        .with_definition(r#"{
            "title": "original.jpg",
            "claim_generator_info": [{
                "name": "CameraApp",
                "version": "1.0.0"
            }],
            "assertions": [{
                "label": "c2pa.actions.v2",
                "data": {
                    "actions": [{
                        "action": "c2pa.created",
                        "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
                    }]
                },
                "created": true
            }]
        }"#)?;

    let signer = EphemeralSigner::new("C2PA Test Signer")?;
    builder1.sign_file(&signer, "input.jpg", "original_signed.jpg")?;
    println!("Step 1: original_signed.jpg に署名完了");

    // --- Step 2: 署名済み画像を Ingredient として取り込み ---
    let mut builder2 = Builder::default()
        .with_definition(r#"{
            "title": "edited.jpg",
            "claim_generator_info": [{
                "name": "EditorApp",
                "version": "1.0.0"
            }],
            "assertions": [{
                "label": "c2pa.actions.v2",
                "data": {
                    "actions": [
                        {
                            "action": "c2pa.opened",
                            "ingredients": [{"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"}]
                        },
                        {
                            "action": "c2pa.cropped"
                        }
                    ]
                },
                "created": true
            }]
        }"#)?;

    // 署名済み original_signed.jpg を parentOf として取り込み
    builder2.add_ingredient_from_stream(
        r#"{"title": "original_signed.jpg", "relationship": "parentOf"}"#,
        "image/jpeg",
        &mut std::fs::File::open("original_signed.jpg")?,
    )?;

    let signer2 = EphemeralSigner::new("C2PA Test Editor")?;
    builder2.sign_file(&signer2, "original_signed.jpg", "edited_signed.jpg")?;
    println!("Step 2: edited_signed.jpg に署名完了(Ingredient チェーン付き)");

    Ok(())
}

Step 1 で「カメラで撮影された」Manifest を付与し、Step 2 で「その画像を開いてクロップ(トリミング)した」という新たな Manifest を追加しています。relationship: "parentOf" は、取り込んだ素材が編集後の成果物の親(直接の原材料)であることを示す関係定義です。

sign_file は出力先に同名ファイルが存在するとエラーとなるため、再実行時はあらかじめ削除しておきます。

rm -f original_signed.jpg edited_signed.jpg
cargo run

生成された画像を c2patool で確認すると、Manifest が 2 つ連鎖している様子が確認できます。

c2patool edited_signed.jpg | jq '{
  manifests_count: (.manifests | length),
  titles: [.manifests[] | .title],
  ingredients: [.manifests[].ingredients[]? | .title]
}'
{
  "manifests_count": 2,
  "titles": [
    "edited.jpg",
    "original.jpg"
  ],
  "ingredients": [
    "original_signed.jpg"
  ]
}

Manifest Store の内部に 2 つの Manifest(original.jpgedited.jpg)が格納され、edited.jpg の Ingredient として original_signed.jpg が参照されている構造が分かります。active_manifest は最新の署名である edited.jpg 側の Manifest を指しており、ここから Ingredient の参照リンクを辿ることで、元素材 original.jpg の来歴まで完全に遡ることができます。これが C2PA における来歴チェーン(Provenance Chain)の基本構造です。

まとめ

本記事では、テスト用証明書の生成から、c2pa-rs および c2pa-python を用いた Manifest の埋め込み、Assertion の追加、そして Ingredient を用いた来歴チェーンの構築までをコード例とともに解説しました。C2PA の Builder API は、「定義 JSON の作成」「Assertion の追加」「署名実行」というシンプルなステップで設計されているため、既存の画像処理パイプラインや Web アプリケーションへも無理なく組み込むことが可能です。

続く第6回では、視点を「署名されたコンテンツを受け取って検証する側」に移し、検証パイプラインの実装と信頼リスト(Trust List)の運用について掘り下げていきます。

参考リンク


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