お断り: 本記事は 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 以外のエラーがないこと、このあたりを見ておけばOKです。

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 署名済み証明書チェーンを借りてきます(Rust 版の EphemeralSigner 相当の仕組みが、c2pa-python にはまだないためです)。プロジェクトはこんな構成になります。

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 を追加する

最小コードでは c2pa.actions.v2 を定義 JSON に直接書きました。ここからは 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 版でも同じ)。

Ingredient(素材の取り込み)

編集ワークフローでは、元画像の Manifest を引き継ぐことが大事になってきます。第2回で触れた「Manifest のチェーン」を実際に作ってみましょう。先ほど署名した output.jpg(Manifest 付き)を 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 の来歴チェーンです。

まとめ

本記事では、テスト用証明書の生成から、c2pa-rs と c2pa-python を使った Manifest の付与、Assertion の追加、Ingredient の取り込みまでを実際のコードで一通り体験しました。Builder API は「定義を JSON で作り、Assertion を積んで、署名する」というシンプルな 3 ステップで構成されているので、既存のアプリケーションに組み込むときの敷居は思ったより低いと思います。

続く第6回では、視点を「署名されたコンテンツを受け取る側」に切り替えて、検証パイプラインの実装に進んでいきます。

参考リンク


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