お断り: 本記事は AWS KMS Cryptographic Details、AWS KMS Sign API、c2pa-rs(c2patool) の公式ドキュメントとソースコードを筆者が読解して整理した、ハンズオンの構成記事です。コマンド・パラメータは AWS SDK や c2patool のバージョンによって挙動が変わる可能性があります。実機で試す際は、必ずAWS KMS 公式ドキュメントと c2patool の最新ヘルプをご確認ください。本記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。
はじめに
前回の記事では、OpenSSL で自前の Root CA と EE 証明書を発行し、ローカルの秘密鍵ファイルで c2patool に署名させて、トラストアンカーに登録することで signingCredential.trusted まで持っていきました。あの構成は学習用としては素直ですが、本番運用に乗せるなら署名鍵がローカルに置きっぱなしというのは避けたいところです。
そこで本記事では、署名鍵を AWS KMS(Key Management Service)に持ち、c2patool 側からは KMS の API を通じて署名処理を呼び出す構成を組み立てます。さらに、Root CA の秘密鍵まで KMS に置くことで、ローカルファイル上に秘密鍵を一切残さない構成に踏み込みます。鍵生成から証明書発行、c2patool --signer-path で KMS に繋ぐところまで、一気通貫でハンズオンします。
なお、本記事は AWS の基本的な概念や操作(アカウント作成、IAM ユーザー / ロールの設定、AWS CLI のセットアップ、リージョンの選択など)は習得済みである前提で進めます。AWS そのものの入門解説は扱わないので、必要に応じて AWS 公式ドキュメント を参照してください。
なぜ AWS KMS で署名するのか
AWS KMS は AWS のマネージドな鍵管理サービスで、Asymmetric Key(非対称鍵)として ECDSA や RSA の鍵を作ると、その鍵での署名・検証 API が kms:Sign / kms:Verify で使えます。C2PA の ES256(ECDSA P-256 + SHA-256)はそのまま KMS の ECC_NIST_P256 + ECDSA_SHA_256 でカバーできます。
ローカル鍵運用で困ること
前回記事の構成だと、運用に乗せた瞬間に次のような困りごとが顔を出します。
- 秘密鍵がファイルとしてローカルに置かれており、
catで中身を見ることができ、かつコピーも簡単にできてしまう - 誰がいつ署名したかの記録が残らない(プロセスの ps log とアプリログ程度)
- 署名権限の制御がファイル読み取り権限に依存し、粒度が粗い
- バックアップは自前運用で組み込む必要がある
KMS にすると何ができるか
AWS KMS に鍵を持たせると、それぞれが KMS のマネージド機能でほぼ素直に解決します。
- 鍵生成と保管が KMS 側で完結し、ローカルに鍵ファイルが存在しない
kms:SignAPI はデータを送って署名結果だけを返す。秘密鍵を取り出す API は存在しない(GetPublicKey は公開鍵のみ)- 誰がいつ署名したかの監査ログが CloudTrail に自動で残る
- IAM ポリシーで「この IAM ロールは署名のみ可、鍵削除不可」のような最小権限を組める
- バックアップやレプリケーションは KMS のマルチリージョン構成で運用に乗る
つまりどういうことか
ローカル鍵運用で抱えていた「鍵漏洩リスク」「監査の不在」「権限制御の粗さ」「バックアップ運用の手間」の 4 つが、KMS に鍵を移すだけでまとめて解消されます。アプリケーション側のコードは「秘密鍵で sign する関数」が「KMS API を呼ぶ関数」に置き換わるだけで、c2patool との接点(--signer-path 経由)は変わりません。
なお KMS の内部実装では FIPS 140 認定の HSM が使われています(AWS KMS Cryptographic Details も参照)が、利用者から見るとあくまで API サービスです。「ハードウェアで署名している」と言うより、「鍵が KMS の外に出ないマネージドサービスを使う」と捉える方が利用感に近いです。
必要なもの
| 項目 | 用途 |
|---|---|
| AWS アカウント(KMS 利用権限) | 鍵保管 |
| AWS CLI v2 | AWS 認証 |
| mise | Terraform / Python / uv の導入とタスクランナー |
| c2patool 0.26.50 以降 | 外部 signer 対応の c2patool(cargo install c2patool) |
具体的な Terraform / Python のバージョンは .mise.toml で固定し、Python パッケージは uv でプロジェクト直下の .venv/ に入れます。システム側の Python / Terraform は触らない構成です。
全体フロー
ざっとこんな流れで進めます。KMS で 2 つの鍵(CA 用 / 署名用)を作り、両方の証明書を KMS の CA 鍵で署名して発行、c2patool が外部 signer 経由で KMS に署名を依頼する、という 4 段構成です。
CA 鍵
(alias/c2pa-rootca)")] KMSLF[("AWS KMS
署名鍵
(alias/c2pa-signer)")] end subgraph S2["Step 2: 証明書を発行(全部 KMS で署名)"] CACERT["ca.pem
(自己署名 Root)"] LFCERT["kms-signer.pem
(EE)"] KMSCA -->|自己署名| CACERT KMSCA -->|EE を発行| LFCERT KMSLF -->|公開鍵を埋め込み| LFCERT end subgraph S3["Step 3: c2patool で署名"] IMG["input.jpg"] SIGNER["kms-signer.py
(external signer)"] OUT["signed.jpg
(Manifest 入り)"] IMG --> SIGNER SIGNER -->|kms:Sign API| KMSLF SIGNER --> OUT end subgraph S4["Step 4: 検証"] VER{"c2patool で
チェーン照合"} NG["untrusted"] OK["trusted"] VER -->|アンカー無し| NG VER -->|ca.pem をアンカーに登録| OK end LFCERT -->|chain.pem として渡す| SIGNER OUT --> VER CACERT -->|trust --trust_anchors| VER
ローカル鍵で署名していた前回構成からの差分は、Step 1 で鍵を全部 KMS に置くことと、Step 2/3 の署名処理が KMS API 呼び出しに変わることです。Step 4 の検証側は前回と完全に同じ手順がそのまま使えます。
実行手順
本記事の Terraform / Python / .mise.toml 一式は TechThanks/c2pa-aws-kms-signing にまとめてあります。clone して mise を入れれば以下のコマンドだけで動かせます。各ステップの解説で、対応する repo 内ファイルへのリンクも示します。
なお以下に出てくる mise run xxx はすべて .mise.toml の [tasks.xxx] セクションで定義された薄いラッパーです。各タスクが裏で実行している実際のシェルコマンドが知りたい場合は、.mise.toml を直接開けば 1 行〜数行で書かれています。
0. SSO プロファイルを用意して AWS にログイン
初回だけ標準形式の SSO プロファイルを 1 つ作っておきます。
aws configure sso
対話プロンプトで以下を入力します。
| 質問 | 入力 |
|---|---|
| SSO session name | c2pa |
| SSO start URL | 利用する AWS Identity Center の https://d-XXXX.awsapps.com/start |
| SSO region | ap-northeast-1 等 |
| SSO registration scopes | デフォルト(Enter) |
| (ブラウザ認可後) アカウント / ロール選択 | 検証に使うアカウントと AWSAdministratorAccess 等 |
| Default client Region | ap-northeast-1 |
| Default output format | json |
| CLI profile name | c2pa |
完了したら次のように使います。
aws sso login --profile c2pa # 期限切れになったら都度
export AWS_PROFILE=c2pa # mise タスクのサブシェルにも引き継がれる
aws sts get-caller-identity # 想定アカウントが返ることを確認
以降の mise run tf-* などはこのシェルで実行してください。新しいシェルを開くたびに export AWS_PROFILE=... が必要です(.envrc / direnv 等を使うと自動化できます)。
1. ツール導入と Python 依存
mise trust . # .mise.toml を信頼(初回のみ)
mise install # Terraform 1.10 / Python 3.12 / uv をインストール、.venv も自動作成
mise run install # uv sync で pyproject.toml + uv.lock から .venv にインストール
.mise.toml で Terraform / Python / uv のバージョンを固定し、Python パッケージは pyproject.toml + uv.lock から uv が venv に入れます。設定全体は .mise.toml と pyproject.toml を参照してください。
2. KMS 鍵を作成
mise run tf-init # 初回のみ
mise run tf-apply # AWS 側に CA 鍵 + 署名鍵を作成
CA 用と署名用、それぞれ ECC_NIST_P256(NIST P-256 楕円曲線、COSE/JOSE では ES256 と呼ばれる)の鍵 + alias を 1 つずつ立てます。Terraform 定義は terraform/ ディレクトリ配下を参照してください。
3. 証明書を発行
mise run certs
out/ca.pem(自己署名 Root CA)、out/kms-signer.pem(EE)、out/chain.pem(チェーン)が出力されます。
通常の OpenSSL ベースの CA 運用では、openssl x509 -req がローカルの秘密鍵で証明書を一気に発行してくれます。今回は CA の秘密鍵が AWS KMS の中にあり、ローカルでは触れません。そこで自前で次の手順を Python で踏んでいます。
- 証明書の中身(subject / issuer / 有効期限 / 公開鍵 / 拡張)を ASN.1 構造として組み立てて、署名対象部分(TBSCertificate)の DER バイト列を作る
- そのバイト列を AWS KMS の Sign API に送る
- KMS が返した署名値を受け取る
- 中身(1)+ アルゴリズム指定 + 署名値(3)を連結した X.509 証明書として再合成する
ここで出てくる用語を簡単に補足しておきます。
- ASN.1(Abstract Syntax Notation One): 言語に依存しない形でデータ構造を記述するための国際標準の表記法。X.509 証明書、PKCS、LDAP、SNMP など、暗号・通信プロトコル系の標準仕様で広く使われている
- DER(Distinguished Encoding Rules): ASN.1 で記述したデータ構造を一意なバイト列に変換するためのバイナリエンコーディング規格。X.509 証明書は最終的に DER バイト列として保存・伝送される(PEM はその DER を Base64 で包んで
-----BEGIN CERTIFICATE-----等のヘッダを付けただけのテキスト形式) - TBSCertificate(To-Be-Signed Certificate): X.509 証明書のうち「署名対象になるブロック」のこと。subject / issuer / 有効期限 / 公開鍵 / 拡張など、証明書の中身がここに入っており、CA はこの部分のハッシュに署名する
スクリプト本体は scripts/issue-certs.py を参照してください。
4. 署名対象の画像を用意して署名
c2pa-rs リポジトリのテスト用 fixture(NASA アポロ 17 号の地球画像)をそのまま入力に使います。
curl -L -o input.jpg \
https://raw.githubusercontent.com/contentauth/c2pa-rs/main/cli/tests/fixtures/earth_apollo17.jpg
mise run sign --input input.jpg
out/signed.jpg が生成されます。c2patool が scripts/kms-signer.py を子プロセスとして起動し、署名対象を stdin で渡し、KMS 経由の署名値を stdout で受け取る、というやり取りで動いています。署名アルゴリズムや証明書チェーンの指定は CLI ではなく manifest/manifest.json の alg / sign_cert フィールドで行います。
この時点の検証ログは validation_state: Valid + signingCredential.untrusted。署名の構造的整合性は取れているが、CA を信頼していないので「信頼済み」とは言えない状態です。
5. 検証
mise run verify
verify タスクは c2patool out/signed.jpg trust --trust_anchors out/ca.pem で自前 Root CA をトラストアンカーに登録します。c2patool 0.26.50 ではトラストアンカーの指定が trust サブコマンド配下で、フラグ名は --trust_anchors(アンダースコア)です。
これで validation_state: Trusted + signing certificate trusted, found in System trust anchors まで持ち上がります。Step 4 の Valid から Step 5 の Trusted への遷移が、信頼アンカーを追加することの意味です。
6. 後片付け
mise run tf-destroy
なお仕様上、KMS 鍵は即時削除できないのでご注意ください(最低 7 日の PendingDeletion 期間が入ります)。
ローカル鍵運用との違い
前回記事のローカル鍵運用と、本記事の AWS KMS 運用を整理すると、
| 観点 | ローカル鍵 | AWS KMS |
|---|---|---|
| CA 秘密鍵の所在 | ローカルファイル(ca.key) | KMS 内部 |
| 署名鍵の所在 | ローカルファイル | KMS 内部 |
| 秘密鍵をエクスポートする API | cat 一発 | 提供されない |
| 署名権限の制御 | ファイル読み取り権限次第 | IAM ポリシー |
| 監査ログ | なし | CloudTrail に署名ごとに残る |
| 紛失・故障 | バックアップ次第 | 概念上発生しない |
実装上は「ローカルの秘密鍵で sign する関数」が「KMS API を呼ぶ関数」に置き換わっただけで、c2patool との接点(--signer-path 経由)は同じです。さらに、CA 秘密鍵までローカルから消えるので、開発機の盗難・バックアップ流出・ログ事故の経路が一段減ります。
おわりに
c2patool の external signer 機構を通じて AWS KMS に署名処理を委譲し、Root CA の秘密鍵まで KMS に置く、というところまで踏み込みました。鍵生成・証明書発行・署名・検証のサイクルが、ローカル鍵の場合と同じ感覚で回せることが分かれば、後は Google Cloud の Cloud KMS や Azure Key Vault に乗り換えるのも、適切な PKCS#11 経由でハードウェアトークンに繋ぎ替えるのも、同じ external signer のフックの中で実装を差し替えるだけです。
c2pa-rs / c2patool 側に signer の plugin 的な層が用意されているおかげで、署名鍵を「どこに置くか」と「どう使うか」を分離できます。ローカル鍵から KMS、KMS から HSM、HSM から組み込み Secure Element、というふうに、運用要件に応じて一段ずつ動かせる設計になっているのが C2PA の良いところです。