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

はじめに

本記事は「C2PA 実装入門」シリーズの第5回です。第4回では c2patool を使って Manifest の中身を実データで確認しました。ここからはいよいよ SDK を使ってコンテンツに Manifest を付与する実装に入ります。c2pa-rs(Rust)と c2pa-node(Node.js)の両方でコード例を示し、テスト用証明書の生成から署名済み画像の確認までを一気通貫で体験します。

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

テスト用証明書を用意する

C2PA の署名には X.509 証明書と秘密鍵が必要です。開発・テスト段階では自己署名証明書で十分なので、OpenSSL で生成します。

# EC P-256 鍵ペアを生成
openssl ecparam -genkey -name prime256v1 -noout -out test_key.pem

# 自己署名証明書を生成(有効期限365日)
openssl req -new -x509 -key test_key.pem \
  -out test_cert.pem -days 365 \
  -subj "/CN=C2PA Test Signer/O=My Organization"

この証明書は C2PA Trust List に登録されていないため、検証時には signingCredential.untrusted が報告されますが、Manifest の構造確認やハッシュ検証には問題ありません。本番環境向けの証明書取得については第7回で解説します。

c2pa-rs(Rust)での署名

セットアップ

Cargo.tomlc2pa クレートを追加します。

[dependencies]
c2pa = { version = "0.79", features = ["file_io"] }
serde_json = "1"

file_io フィーチャーを有効にすると、ファイルパスを直接指定して読み書きできる API が使えます。

最小限の署名コード

Builder API を使い、画像ファイルに Manifest を付与する最小限のコードです。

use c2pa::{Builder, SigningAlg};
use c2pa::openssl::OpenSslSigner;
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Builder を JSON 定義から生成
    let mut builder = Builder::from_json(&serde_json::json!({
        "title": "my_photo.jpg",
        "claim_generator_info": [{
            "name": "MyApp",
            "version": "1.0.0"
        }]
    }).to_string())?;

    // テスト用証明書と秘密鍵を読み込んで Signer を生成
    let signer = OpenSslSigner::new(
        Path::new("test_cert.pem"),
        Path::new("test_key.pem"),
        SigningAlg::Es256,
        None,  // TSA URL(テスト時は None でも可)
    )?;

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

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

sign_file は入力ファイルの形式を拡張子から判定し、JUMBF コンテナとして Manifest Store を埋め込んだ出力ファイルを生成します。

署名結果を c2patool で確認

c2patool output.jpg

第4回で見た JSON と同じ形式で、自分が付与した Manifest の中身を確認できます。claim_generator_infoMyApp が入っていること、validation_resultssigningCredential.untrusted 以外のエラーがないことを確認してください。

c2pa-node(Node.js)での署名

セットアップ

従来の c2pa-node はアーカイブされており、現在は後継の @contentauth/c2pa-node(c2pa-node-v2)を使用します。

npm install @contentauth/c2pa-node

最小限の署名コード

import { Builder, LocalSigner } from "@contentauth/c2pa-node";
import { readFileSync } from "fs";

async function signImage() {
  // テスト用証明書と秘密鍵を読み込み
  const cert = readFileSync("test_cert.pem");
  const key = readFileSync("test_key.pem");

  // LocalSigner を生成
  const signer = new LocalSigner({
    certificate: cert,
    privateKey: key,
    algorithm: "es256",
    tsaUrl: undefined,  // テスト時は省略可
  });

  // Builder で Manifest 定義を作成
  const builder = new Builder({
    title: "my_photo.jpg",
    claim_generator_info: [{
      name: "MyNodeApp",
      version: "1.0.0",
    }],
  });

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

  console.log("署名完了: output.jpg");
}

signImage().catch(console.error);

Rust 版と同様に、signFile がファイル形式を自動判定し、Manifest を埋め込んだ出力ファイルを生成します。

Assertion を追加する

Builder API では add_assertion メソッドで任意の Assertion を追加できます。第4回で見た c2pa.actions.v2cawg.training-mining を自前で付与してみましょう。

アクション Assertion(c2pa.actions.v2)

builder.add_assertion(
    "c2pa.actions.v2",
    &serde_json::json!({
        "actions": [{
            "action": "c2pa.created",
            "digitalSourceType":
              "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
        }]
    }),
)?;

digitalSourceType に IPTC ボキャブラリを指定することで、検証側のポリシー判定で「AI 生成か実撮影か」を機械的に振り分けられるようになります。AI 生成コンテンツの場合は trainedAlgorithmicMedia を指定します。

AI 学習オプトアウト(cawg.training-mining)

builder.add_assertion(
    "cawg.training-mining",
    &serde_json::json!({
        "entries": {
            "cawg.ai_inference":           { "use": "notAllowed" },
            "cawg.ai_generative_training": { "use": "notAllowed" }
        }
    }),
)?;

この Assertion により、コンテンツを AI 学習や推論に使うことを許諾しないという宣言を Manifest 内に記録できます。

Ingredient(素材の取り込み)

編集ワークフローでは、元画像の Manifest を引き継ぐことが重要です。Builder API の add_ingredient を使うと、元素材を Ingredient として取り込み、来歴チェーンを連結できます。

builder.add_ingredient_from_stream(
    &serde_json::json!({
        "title": "original_photo.jpg",
        "relationship": "parentOf"
    }).to_string(),
    "image/jpeg",
    &mut std::fs::File::open("original_photo.jpg")?,
)?;

relationship には parentOf(派生元)や componentOf(合成素材の一部)を指定します。取り込まれた Ingredient の Manifest は新しい Manifest Store 内に保持され、検証時に来歴の連鎖として確認できます。

まとめ

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

次の第6回では、署名されたコンテンツを受け取る側の視点に立ち、検証パイプラインの実装に進みます。

参考リンク


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