お断り: 本記事は C2PA Technical Specification v2.3(2026年4月時点)と c2pa-python・Adobe Trustmark のソースコードおよび公開ドキュメントを筆者が読解して整理したものです。Soft Binding は仕様で算法(algorithm)レジストリが拡張され続けている領域であり、SDK の対応状況も変化します。実装や設計判断の根拠として用いる際は、必ずC2PA 公式仕様・c2pa-org/softbinding-algorithm-list ・各ライブラリの最新ドキュメントをご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。
はじめに
本記事は、シリーズ「C2PA 実装入門」 の応用編・ソフトバインディング編です。前編にあたるハードバインディング実装 では、c2pa-rs が c2pa.hash.data を自動付与してバイト単位の完全一致を保証する仕組みと、1 バイトの改変で assertion.dataHash.mismatch が出ることまでを確認しました。
ハードバインディングは強力な反面、JPEG 再エンコードや SNS 投稿時のメタデータ除去で簡単に壊れます。それを補うのがソフトバインディングです。本記事では、C2PA 公認のソフトバインディングアルゴリズムレジストリに登録された Adobe Trustmark を使い、画像に不可視透かしを焼き付けたうえで c2pa.soft_binding assertion として Manifest にも同じ識別子を記録する流れを、/tmp から完全再現できる Python ハンズオンとして実装します。
ソフトバインディング assertion の構造
ソフトバインディングは、対応するアルゴリズム(alg)と、コンテンツから抽出した識別子(指紋や透かし)を c2pa.soft_binding assertion として Manifest に追加します。assertion の構造は仕様書 Section 18.2 で定義されており、概ね以下のフィールドで構成されます。
| フィールド | 役割 |
|---|---|
alg | ソフトバインディングアルゴリズムの識別子(ICANN ドメイン形式の名前を仕様が推奨) |
blocks | アルゴリズムが算出した識別子のリスト。各エントリは scope と value を持つ |
alg_params | アルゴリズム固有のパラメータ(任意) |
url | アルゴリズムの公開仕様への URL(任意) |
description | 補足説明(任意) |
仕様書(Section 9.3.2 “Referenced List of Soft Binding Algorithms”)では、alg の選び方について次のように定めています。
Soft bindings are generated by an algorithm named in the
algfield of the soft binding assertion. The algorithm name should be among those algorithms listed in the soft binding algorithm list as supported by this specification.
登録済みアルゴリズムの一覧は c2pa-org/softbinding-algorithm-list に JSON で管理されています。本記事ではその中から Adobe が公開している透かし方式 Adobe Trustmark(alg = com.adobe.trustmark.Q)を使います。OSS 実装が pip で導入でき、画像への不可視透かし埋め込みから抽出まで Python で完結するため、ハンズオンにちょうど向いています。
Trustmark で透かしを埋め込む
第5回で使った uv で Python プロジェクトを立ち上げます。Trustmark は PyTorch に依存するのでインストールには少し時間がかかります。
cd /tmp
mkdir -p c2pa-soft-binding && cd c2pa-soft-binding
uv init .
# Trustmark は Python 3.10〜3.12 に対応。uv の requires-python を合わせる
sed -i '' 's/requires-python = ">=3.12"/requires-python = ">=3.10,<3.13"/' pyproject.toml
uv python pin 3.11
uv add trustmark c2pa-python
# サンプル画像と c2pa-rs のテスト用証明書をダウンロード
curl -sL -o input.jpg https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/earth_apollo17.jpg
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
# 以降のステップで作成する Python スクリプトを先に用意しておく
touch embed_watermark.py sign_soft_binding.py
この時点でプロジェクトは以下の構成になります(.venv と __pycache__ は省略)。
c2pa-soft-binding/
├── embed_watermark.py
├── input.jpg
├── main.py
├── pyproject.toml
├── README.md
├── sign_soft_binding.py
├── test_cert.pem
├── test_key.pem
└── uv.lock
main.py と README.md は uv init が自動生成したもので本記事では使いません。作成済みの embed_watermark.py と sign_soft_binding.py に順番に中身を書いていきます。
透かしと payload の関係
透かしを埋め込むとは、ここでは「画像の全ピクセルに人間の目では知覚できない微小な変調を加えて、短いビット列(payload)を画像データそのものに焼き付ける」ことを指します。Trustmark は学習済みのニューラルネットワークがエンコーダ・デコーダとして働き、元画像と payload を受け取ると見た目が変わらない範囲でピクセル値を書き換え、逆に受け取った画像からそのビット列を取り出します。Base で示された見本とほぼ同じ絵のまま、ファイル内部には識別子が入っている、というイメージです。
payload は画像に結びつけたい任意の短い識別子です。内容そのものに来歴情報を詰め込むのではなく、「この画像は TT0001 として登録されているもの」というラベルだけを物理的に焼き付ける役割で、C2PA soft binding の文脈では後述のとおり Manifest を引くためのキーとして使います。Trustmark Q バリアントは ECC(エラー訂正符号)込みで 61bit ほどしか入らないため、英数字なら 7 文字程度が実用上の上限で、本記事では TT0001 という最小限のダミー ID を使っています。実運用では UUID の短縮形や DB の連番を入れることが多いです。
透かしはハードバインディングのバイト単位ハッシュと違い、JPEG 再エンコードやリサイズなど「見た目がほぼ同じままの変換」であれば生き残るよう設計されています。そのおかげで、Manifest ごとメタデータが剥がれた配信物からでも、画像から payload を抽出 → 外部の Manifest Repository に問い合わせ、という経路で来歴に辿り着けます。
実装
Trustmark の Q バリアントで短い識別子 TT0001 を埋め込みます。
# embed_watermark.py
from PIL import Image
from trustmark import TrustMark
tm = TrustMark(verbose=False, model_type="Q")
payload = "TT0001" # Q variant は ECC 込み 7文字程度が上限
cover = Image.open("input.jpg").convert("RGB")
encoded = tm.encode(cover, payload, MODE="text")
encoded.save("watermarked.png")
print(f"埋め込み完了: payload='{payload}' -> watermarked.png")
decoded, present, confidence = tm.decode(Image.open("watermarked.png"), MODE="text")
print(f"抽出結果: present={present} confidence={confidence:.3f} payload='{decoded}'")
初回実行時は Trustmark のモデルファイル(約 77MB)が自動ダウンロードされます。
uv run python embed_watermark.py
# => 埋め込み完了: payload='TT0001' -> watermarked.png
# => 抽出結果: present=True confidence=1.000 payload='TT0001'
埋め込んだ payload が confidence 1.000 で抽出できており、透かし自体は成立しています。次にこの画像に C2PA Manifest を付与し、c2pa.soft_binding assertion として同じ payload を記録します。
c2pa-python で soft_binding assertion を記録する
# sign_soft_binding.py
import base64
import json
import c2pa
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)
payload = "TT0001"
payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
manifest_json = json.dumps({
"title": "soft_binding_demo.png",
"claim_generator_info": [{
"name": "TechThanksSoftBindingDemo",
"version": "0.1.0",
}],
"assertions": [
{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.created",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
}],
},
"created": True,
},
{
"label": "c2pa.soft_binding",
"data": {
"alg": "com.adobe.trustmark.Q",
"url": "https://github.com/adobe/trustmark/",
"description": "Adobe Trustmark variant Q invisible watermark",
"blocks": [
{
"scope": {"start": 0, "length": 0},
"value": payload_b64,
}
],
},
},
],
})
builder = c2pa.Builder(manifest_json)
builder.sign_file("watermarked.png", "output.png", signer)
print("署名完了: output.png")
rm -f output.png
uv run python sign_soft_binding.py
# => 署名完了: output.png
c2patool output.png で Manifest を確認すると、Trustmark で埋め込んだ payload が c2pa.soft_binding として記録されています。
c2patool output.png | jq '.manifests[].assertions[] | select(.label == "c2pa.soft_binding")'
{
"label": "c2pa.soft_binding",
"data": {
"alg": "com.adobe.trustmark.Q",
"blocks": [
{
"scope": {"length": 0, "start": 0},
"value": "VFQwMDAx"
}
],
"description": "Adobe Trustmark variant Q invisible watermark",
"url": "https://github.com/adobe/trustmark/"
}
}
value の VFQwMDAx は Base64 で TT0001 を表します。画像(透かし)と Manifest(soft_binding value)で同じ識別子が保持できていることが分かります。
Manifest Recovery をシミュレートする
ソフトバインディングの真価は「Manifest が剥がれても画像から識別子を引ける」ところです。仮に output.png から Manifest を取り除いた(あるいは SNS 経由の再エンコードでメタデータが落ちた)状況でも、画像側に残った Trustmark 透かしから payload を復元し、それを鍵に外部の Manifest Repository を引けば来歴に辿り着けます。ここでは検証側の役回りを簡易的に再現し、「画像から抽出した payload」と「Manifest 内に記録された value」が一致することを確かめます。
# verify_soft_binding.py
import base64
import json
import subprocess
from PIL import Image
from trustmark import TrustMark
# 1) Manifest から soft_binding を読み出す
result = subprocess.run(
["c2patool", "output.png"], capture_output=True, text=True, check=True
)
manifest_data = json.loads(result.stdout)
active_id = manifest_data["active_manifest"]
assertions = manifest_data["manifests"][active_id]["assertions"]
sb = next(a for a in assertions if a["label"] == "c2pa.soft_binding")
alg = sb["data"]["alg"]
manifest_payload = base64.b64decode(sb["data"]["blocks"][0]["value"]).decode("utf-8")
print(f"Manifest 記録値: alg={alg} payload='{manifest_payload}'")
# 2) 画像から Trustmark で payload を抽出
tm = TrustMark(verbose=False, model_type="Q")
extracted, present, confidence = tm.decode(Image.open("output.png"), MODE="text")
print(f"画像から抽出: present={present} confidence={confidence:.3f} payload='{extracted}'")
# 3) 照合
if extracted == manifest_payload:
print("OK: Manifest Recovery 成立")
else:
print("NG: 一致しない")
uv run python verify_soft_binding.py
# => Manifest 記録値: alg=com.adobe.trustmark.Q payload='TT0001'
# => 画像から抽出: present=True confidence=1.000 payload='TT0001'
# => OK: Manifest Recovery 成立
実運用では payload そのものを Manifest の URL 解決キーに使うのではなく、外部の Manifest Repository(TrustMark であれば Trustmark Cloud などが候補)への問い合わせキーとして扱うのが一般的です。ここでは同じマシン内で完結させていますが、狙っているのは「画像が単独で出回っても、透かしから来歴に辿り着ける経路が一本残る」状態です。
画像を JPEG に再エンコードしたり、軽くリサイズしてから verify_soft_binding.py を再実行すると、ハッシュベースの hard binding(c2pa.hash.data)は壊れる一方で、透かしベースの soft binding だけが生き残ることを手元で確かめられます。これが soft binding を導入する実務的な理由です。
検証側はどう振る舞うか
Manifest を受け取った検証側は、まずハードバインディングを評価します。c2pa.hash.data などの assertion が示すバイト範囲のハッシュを再計算し、一致すれば「このバイト列は Manifest が署名したものと完全に同一」と判定します。一致しなければ検証は失敗で、ここで処理を打ち切るのが基本です。
ソフトバインディングはハードバインディングが取れなかった場合や、配信物から Manifest 自体が剥がれていたときの「リカバリ経路」として活用されます。仕様書の Manifest Recovery では、検証側が次のような流れで Manifest を再発見するシナリオが想定されています。
- 受け取ったコンテンツに Manifest が埋め込まれていない、あるいは破損している
- ソフトバインディングアルゴリズムを使って、コンテンツから指紋や透かし値を抽出
- その値で外部の Manifest Repository を検索し、対応する Manifest 候補を取得
- 候補の Manifest と現在のコンテンツとの整合を確認し、来歴を提示
この経路を実用にするには、検証側もソフトバインディングのアルゴリズムを実装している必要があります。alg フィールドが ICANN ドメイン形式で命名されるのは、誰がどのアルゴリズムを定義しているかを名前空間で明示するためで、検証実装と Manifest 側の alg 表記が一致しないとリカバリは成立しません。
アルゴリズム選定の現実的な観点
ソフトバインディングを実用するうえで悩ましいのが、アルゴリズム選定です。仕様書はアルゴリズム名の付け方とレジストリのあり方を定めますが、具体的な算法は別の標準化やオープンソース実装に委ねられています。
代表的な選択肢の方向性を整理します。
- 知覚ハッシュ系: pHash や DCT 系の指紋。OSS 実装が多く、画像のリサイズや軽い色補正には強い反面、トリミングや回転には弱い
- ディープラーニング系の埋め込みベクトル: より頑健な一致判定が可能だが、学習済みモデルの互換性管理が継続的な負担になる
- 電子透かし: コンテンツに目に見えない透かしを埋め込む方式。再エンコードに強い反面、生成側で透かしを入れる工程が必要
検証側の実装負担、頑健性、誤一致率のバランスを見て選定し、選んだアルゴリズム名(alg)の意味を仕様化したうえで url から辿れる状態にしておくのが、相互運用を成立させるうえで重要です。
まとめ
C2PA の Hard Binding と Soft Binding は、対立概念ではなく補完関係にあります。Hard Binding は「Manifest と完全一致するコンテンツである」ことの証明、Soft Binding は「変換後でも Manifest を辿れる」ことの保険です。c2pa-rs と c2patool は前者をほぼ自動で扱ってくれますが、後者は運用要件に合わせて自分でアルゴリズムを選び、assertion として組み込む設計判断が必要になります。
本記事では Adobe Trustmark を使って画像に payload を焼き付け、c2pa-python から c2pa.soft_binding assertion として Manifest にも同じ識別子を記録するところまでを、検証側の Manifest Recovery シミュレーションも含めて手元で完全再現しました。前編のハードバインディング実装 と合わせて読むと、両バインディングの守備範囲の違いがよりはっきり掴めるはずです。
TechThanks では、生成 AI 由来コンテンツやメディア配信における C2PA 導入の支援を行っています。Hard / Soft Binding の組み合わせ設計、Manifest Repository の構築、検証パイプラインの実装まで含めた相談がありましたら、お気軽にお問い合わせください。