お断り: 本記事は AWS Lambda、Amazon API Gateway REST API、AWS WAF、AWS KMS、c2pa-python の公式ドキュメントとソースコードを筆者が読解して整理した、ハンズオン記事です。Terraform / AWS SDK / c2pa-python のバージョンによって挙動が変わる可能性があるので、実機で試す際は最新版のドキュメントとソースをご確認ください。記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。
はじめに
前回の記事では、c2patool の external signer 経由で AWS KMS に署名処理を委譲し、Root CA の秘密鍵まで KMS に閉じ込める構成をハンズオンしました。前回は「CLI 環境から単一ファイルを署名する」というスタンドアロンな検証手順を中心に取り上げました。
本記事では、その KMS 署名ロジックを AWS Lambda に組み込み、ブラウザから画像をアップロードするだけで即座に C2PA 署名済みアセットが生成され、さらに別タブから即座に真正性を検証できる End-to-End のフルサーバーレスアーキテクチャを構築します。署名鍵は KMS、コンピュートは Lambda、配信は CloudFront、メタデータは DynamoDB、IaC は Terraform。フロントエンドは Vite + React + shadcn/ui で実装します。
リポジトリは TechThanks/c2pa-aws-serverless-demo に公開してあります。本記事の手順とコードはすべてこのリポジトリと対応しています。
なお、本記事は AWS の基礎的なサービス構成(IAM、S3、Lambda、Terraform プロバイダー設定等)および前回記事で解説した C2PA 署名の基本原理を前提として進めます。インフラの入門的解説や暗号理論の基礎については割愛します。
何を作るか
構築するシステム全体のアーキテクチャは下図の通りです。
SPA + signed origin"] end subgraph WAFAPI["WAF v2 REGIONAL scope"] APIGW["API Gateway REST API"] end subgraph Compute["Lambda functions"] LU["Lambda upload
sync sign"] LV["Lambda verify"] LL["Lambda list-assets"] LD["Lambda delete-asset"] end subgraph Data["Storage"] S3FE[("S3 frontend")] S3S[("S3 signed")] S3C[("S3 certs")] DDB[("DynamoDB
assets metadata")] end subgraph KMS["KMS asymmetric keys P-256"] KSign["KMS leaf signer"] KCA["KMS Root CA"] end end User -->|"1. GET / over HTTPS"| CF CF -->|"page bundle"| S3FE User -->|"2. GET /signed/*"| CF CF -->|"/signed/* via OAC"| S3S User -->|"3. POST /uploads file body <=10MB"| APIGW APIGW --> LU LU -.->|"GetObject chain.pem"| S3C LU -->|"KMS.Sign ECDSA_SHA_256"| KSign LU -->|"PutObject signed"| S3S LU -->|"PutItem status=signed"| DDB User -->|"4. POST /verify file body"| APIGW APIGW --> LV LV -.->|"GetObject ca.pem"| S3C User -->|"5. GET /assets"| APIGW APIGW --> LL LL --> DDB User -->|"6. DELETE /assets/:id"| APIGW APIGW --> LD LD -->|"DeleteItem"| DDB LD -.->|"DeleteObject"| S3S KCA -. issues .-> KSign
本構成では、主に以下の 6 つのマネージドサービスを組み合わせています。
| サービス | 役割 |
|---|---|
| CloudFront | SPA(フロントエンド)と署名済みアセットを配信。AWS WAF(CLOUDFRONT scope)で IP 制限 |
| API Gateway REST API | 4 つの API ルート(upload / verify / list / delete)を持つ。AWS WAF(REGIONAL scope)で IP 制限 |
| Lambda(4 個) | upload / verify / list-assets / delete-asset |
| S3(3 個) | frontend / signed / certs(検証用 ca.pem と署名用 chain.pem を配布) |
| DynamoDB | アセットのメタデータ(asset_id / 状態 / S3 キー / 署名時刻) |
| KMS | Root CA 鍵 + leaf 署名鍵(前回と同じ ECC_NIST_P256) |
全体フロー
環境構築の手順は、大きく以下の 3 フェーズに分かれます。
Terraform でインフラを立てる"] A1["KMS / S3 / DynamoDB
Lambda / API Gateway
CloudFront / WAF"] B["Step 2
証明書を発行して S3 に配布"] B1["ca.pem 自己署名 Root
kms-signer.pem EE
chain.pem"] C["Step 3
フロントエンドをビルドして S3 に sync"] C1["dist/ を S3 frontend に sync
CloudFront invalidate"] A --> B --> C A -. 作るもの .-> A1 B -. 作るもの .-> B1 C -. 作るもの .-> C1
各ステップは .mise.toml にタスクとして定義されており、mise run tf-apply、mise run certs、mise run front-deploy を順次実行するだけで一連のデプロイが完了します。
実行手順
コード一式はリポジトリ TechThanks/c2pa-aws-serverless-demo にて公開しています。クローン後に mise を導入することで、以下のコマンド群で即座に動作確認が可能です。各コマンドは .mise.toml 内の定義を実行するタスクランナーとして機能します。
0. SSO プロファイルで AWS にログイン
前回記事と同じ手順です。aws configure sso で c2pa プロファイルを作って、export AWS_PROFILE=c2pa してから aws sts get-caller-identity で想定アカウントが返ってくることを確認します。
1. ツール導入と依存関係
mise trust .
mise install # Terraform / Python / uv / Node を一気にインストール
mise run install # uv sync + npm install
.mise.toml で terraform 1.10 / python 3.12 / uv latest / node 22 を固定し、Python 側は pyproject.toml + uv.lock、フロントエンド側は frontend/package-lock.json から uv / npm がそれぞれ依存を入れます。
2. allowed_ips を設定する
terraform/terraform.tfvars.example を terraform/terraform.tfvars にコピーして、自分の IP を CIDR で書き込みます。
cp terraform/terraform.tfvars.example terraform/terraform.tfvars
# エディタで開いて allowed_ips に自分の IP を /32 で追記する
3. Lambda Layer をビルド
mise run layer-build
c2pa-python / cryptography / asn1crypto を manylinux wheel から uv pip install --python-platform x86_64-manylinux_2_28 で取り、zip にまとめて build/layer.zip を作成します。
4. Terraform でリソースを作成
mise run tf-init
mise run tf-apply
KMS、S3、DynamoDB、Lambda、API Gateway、CloudFront、AWS WAF の各リソースを一括プロビジョニングします。なお、CloudFront 用の AWS WAF(CLOUDFRONT scope)は仕様上 us-east-1 リージョンに作成する必要がありますが、AWS Provider v6 のリソース個別 region 属性を活用し、プロバイダーエイリアスを増やさずに単一設定ファイル内で簡潔に定義しています。
CloudFront ディストリビューションの作成および WAF の関連付け処理には、およそ 3〜4 分を要します。
5. 証明書を発行して certs バケットへ配布
mise run certs
前回記事の scripts/issue-certs.py を流用しています。KMS 上の鍵ペアを用いて Root CA(自己署名)およびリーフ証明書を発行し、生成された ca.pem、kms-signer.pem、chain.pem を S3 の certs バケットへ安全に配置します。
6. フロントエンドをビルドしてデプロイ
mise run front-build
mise run front-deploy
front-deploy タスクは scripts/deploy-frontend.sh を呼び出し、Terraform outputs から取得した VITE_API_URL と VITE_CDN_URL をビルド時環境変数に注入した上で、npm run build、S3 への静的ファイル同期(aws s3 sync)、および CloudFront キャッシュの無効化(Invalidation)を自動実行します。
7. 動作確認
デプロイ完了後、Terraform の outputs に出力される CloudFront のドメイン(例: https://dXXXXXX.cloudfront.net)へブラウザからアクセスします。画面上部には Upload タブが表示され、ドラッグ&ドロップまたはファイル選択によって JPEG / PNG 画像をアップロードできます。

画像をアップロードすると即座に一意な asset_id が採番され、Assets タブへ遷移すると status = signed の状態で一覧に表示されます。画面を手動更新しなくても、クライアント側で定期的なポーリングが実行され最新のステータスが自動反映されます。

署名済み画像を Verify タブに投入すると、バックエンドの verify Lambda が S3 から信頼アンカー(ca.pem)を取得して c2pa-python の Reader API に渡します。これにより validation_state: Trusted の総合判定とともに、Manifest の完全な検証レポート JSON が表示されます。

外部システム等で発行された他の C2PA 署名済み画像を投入した場合は、validation_state: Valid と判定されます。これは暗号署名やコンテンツハッシュの整合性は証明されたものの、今回作成した独自のプライベート Root CA とは信頼関係が結ばれていない正当な動作結果を示しています。
8. 後片付け
mise run tf-destroy
CloudFront の削除に 3 分前後かかります。KMS 鍵は最低 7 日の PendingDeletion 期間が入る点は前回と同様です。
実装のポイント解説
upload Lambda は KMS にコールバックを渡す
署名を担当する lambda/upload/handler.py の内部処理シーケンスは以下の通りです。
- API Gateway から渡された
event.bodyを base64 デコードしてバイト列にする - certs バケットから
chain.pemを起動時に 1 度だけ取得(Lambda コンテナ単位でキャッシュ) Builder(manifest_json)で C2PA マニフェスト定義を作り、Signer.from_callback(...)に「KMS.Sign を呼ぶコールバック関数」を渡して署名する- 署名済みバイト列を signed バケットに
PutObject、メタデータを DynamoDB にPutItem
KMS 連携の心臓部はここです。
def _kms_sign_callback(data: bytes) -> bytes:
response = kms.sign(
KeyId=KMS_KEY_ID,
Message=data,
MessageType="RAW",
SigningAlgorithm="ECDSA_SHA_256",
)
r, s = decode_dss_signature(response["Signature"])
return r.to_bytes(32, "big") + s.to_bytes(32, "big")
AWS KMS の Sign API は、ECDSA 署名値を ASN.1 DER 形式(SEQUENCE { r, s })で返却します。一方、C2PA 仕様(COSE / JOSE 準拠)および c2pa-python では、raw 形式である r || s(P-256 の場合は各 32 バイト、計 64 バイト)のバイナリ表現が要求されます。そのため、cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature を用いて DER から r と s をデコードし、それぞれ 32 バイトのビッグエンディアン配列へ整形して連結しています。
verify Lambda は trust anchor を起動時に S3 から読む
lambda/verify/handler.py は、リクエストごとに c2pa-python の Reader で検証を走らせるシンプルな関数です。重要な実装ポイントとして、Root CA(ca.pem)を c2pa-python の信頼アンカー(trust anchor)として登録する処理は、リクエスト毎ではなく Lambda コンテナの初期化時(コールドスタート時)に 1 度だけ load_settings(...) を呼び出して行っています。これにより、ウォームリクエスト時の実行オーバーヘッドを最小化しています。
ca_pem = s3.get_object(Bucket=CERTS_BUCKET, Key=CA_KEY)["Body"].read().decode("utf-8")
load_settings(json.dumps({
"trust": {"trust_anchors": ca_pem, "verify_after_sign": False},
"verify": {"verify_trust": True},
}))
どこまでがこのデモのスコープか
意図的に省いている要素を明記しておきます。後続の記事で 1 つずつ掘り下げる予定です。
- 認証(Cognito / SSO)。今回は IP 制限のみ
- Manifest Repository
- leaf 証明書のローテーション運用 / TSA 連携(過去アセットを長期に Trusted で保つ仕組み)
- 動画 / Live Video(10MB 制限を超える領域)
- 動作パフォーマンス(スケール戦略・コールドスタート最適化など)
逆に「今回のデモで触れていること」は次のとおりです。
- 自前 KMS で Root CA + leaf 鍵を持ち、署名チェーンを
ca.pem/chain.pemとして配布 - API Gateway REST API + Lambda の最小構成で署名と検証を行う
- DynamoDB に asset_id ベースの最低限のメタデータを残す
- CloudFront distribution で SPA と署名済みアセットを配信
- AWS WAF v2 を CloudFront と REST API の両方に貼る IP 制限
おわりに
本記事では、C2PA の署名・検証基盤を単発の CLI スクリプトから、Web ブラウザ経由で実際に操作可能なフルサーバーレスの Web サービスへと昇華させました。秘密鍵を AWS KMS のセキュアな境界内に完全に封じ込めたまま、画像のアップロードから C2PA 署名付与、CloudFront 経由のセキュア配信、そして真正性検証に至る一連のサイクルを体験できる実践的なミニマム SaaS 構成となっています。
この最小構成をベースとすることで、今後の本格的な商用サービス化に向けた論点も明確になります。過去に生成したアセットの検証寿命を担保するための RFC 3161 タイムスタンプ局(TSA)との連携、メタデータを分離配信するための Manifest Repository の導入、Cognito によるマルチテナント認証、さらには動画などの大容量アセットを非同期分散処理するための SQS / Step Functions パイプラインへの拡張など、さらなる高度化への道筋が開けます。
C2PA の本質は「暗号鍵をどこに安全に保管し、誰の認可のもとで署名を適用するか」というセキュリティ設計にあります。本記事で示した「KMS + Lambda + API Gateway」のアーキテクチャパターンを足がかりに、各システムの要件に適合した強固な Content Credentials 基盤を設計・構築してみてください。