お断り: 本記事は AWS LambdaAmazon API Gateway REST APIAWS WAFAWS KMSc2pa-python の公式ドキュメントとソースコードを筆者が読解して整理した、ハンズオン記事です。Terraform / AWS SDK / c2pa-python のバージョンによって挙動が変わる可能性があるので、実機で試す際は最新版のドキュメントとソースをご確認ください。記事に誤りや古くなった箇所を見つけられた場合は、記事末尾のフィードバック枠よりお知らせいただけると助かります。

はじめに

前回の記事では、c2patool の external signer 経由で AWS KMS に署名処理を委譲し、Root CA の秘密鍵まで KMS に閉じ込める構成をハンズオンしました。あれは「コマンドラインから 1 ファイルを署名する」という単発の話でした。

この記事では、その「KMS で署名する関数」を Lambda の中に持ち込み、ブラウザからファイルを投げ込めば C2PA 署名済みアセットが返ってきて、別タブから検証もできる、という End-to-End のミニ SaaS を AWS サーバーレスで組み立てます。署名鍵は KMS、コンピュートは Lambda、配信は CloudFront、メタデータは DynamoDB、IaC は Terraform。フロントエンドは Vite + React + shadcn/ui で組みます。

リポジトリは TechThanks/c2pa-aws-serverless-demo に公開してあります。本記事の手順とコードはすべてこのリポジトリと対応しています。

なお、AWS の基本的な概念(IAM、S3、Lambda、Terraform プロバイダ)と前回記事で扱った C2PA 署名の流れは前提として進めます。AWS の入門解説そのものや、署名アルゴリズムの細部は扱いません。

何を作るか

最終的な構成はこんな形です。

flowchart LR User([Browser allowlisted IP]) subgraph AWS["AWS Cloud ap-northeast-1"] direction LR subgraph WAFCF["WAF v2 CLOUDFRONT scope"] CF["CloudFront
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 種類のサービスが登場します。

サービス役割
CloudFrontSPA(フロントエンド)と署名済みアセットを配信。AWS WAF(CLOUDFRONT scope)で IP 制限
API Gateway REST API4 つの 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 キー / 署名時刻)
KMSRoot CA 鍵 + leaf 署名鍵(前回と同じ ECC_NIST_P256)

全体フロー

立ち上げの手順は大きく 3 段です。

flowchart TB A["Step 1
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 です。clone して mise を入れれば以下のコマンドだけで動かせます。各コマンドは .mise.toml[tasks.xxx] セクションを呼んでいる薄いラッパーです。

0. SSO プロファイルで AWS にログイン

前回記事と同じ手順です。aws configure ssoc2pa プロファイルを作って、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.tomlterraform 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.exampleterraform/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 / WAF を一気に作ります。WAF の CLOUDFRONT scope は us-east-1 専用ですが、AWS Provider v6 のリソース個別 region 指定で、プロバイダエイリアスを足さずに済ませています。

CloudFront の作成と、CloudFront WAF の関連付けで合計 3〜4 分くらいかかります。

5. 証明書を発行して certs バケットへ配布

mise run certs

前回記事の scripts/issue-certs.py をそのまま流用しています。Root CA(自己署名)と leaf 証明書を KMS で発行して、ca.pem / kms-signer.pem / chain.pem を certs バケットに aws s3 cp でアップロードします。

6. フロントエンドをビルドしてデプロイ

mise run front-build
mise run front-deploy

front-deployscripts/deploy-frontend.sh で、Terraform outputs から VITE_API_URLVITE_CDN_URL を引っ張ってきて環境変数に渡し、npm run buildaws s3 sync → CloudFront invalidate を回します。

7. 動作確認

tf-apply 直後の outputs に CloudFront ドメイン(例: https://dXXXXXX.cloudfront.net)が出ているので、ブラウザで開きます。Upload タブで JPEG を投げると、asset_id が即返ってきて、Assets タブに status = signed で出ます。Verify タブに同じファイルを投げると validation_state: Trusted になります。

別の C2PA 署名済みファイル(他のシステムで署名されたもの)を Verify に投げると validation_state: Valid で帰ってきます。「署名は構造的に正しいけれど、私たちの Root CA は知らない」状態です。

8. 後片付け

mise run tf-destroy

CloudFront の削除に 3 分前後かかります。KMS 鍵は最低 7 日の PendingDeletion 期間が入る点は前回と同じです。

実装のポイント解説

upload Lambda は KMS にコールバックを渡す

lambda/upload/handler.py の中身は次の流れです。

  1. API Gateway から渡された event.body を base64 デコードしてバイト列にする
  2. certs バケットから chain.pem を起動時に 1 度だけ取得(Lambda コンテナ単位でキャッシュ)
  3. Builder(manifest_json) で C2PA マニフェスト定義を作り、Signer.from_callback(...) に「KMS.Sign を呼ぶコールバック関数」を渡して署名する
  4. 署名済みバイト列を 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")

KMS の Sign API は ECDSA 署名を ASN.1 DER エンコード(SEQUENCE { r, s })で返してきます。一方 c2pa-python は raw r||s(P-256 なら 64 バイト)を期待するので、cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature で DER をパースして 32 バイトずつ big-endian に揃えています。前回記事の kms-signer.py と同じ変換です。

verify Lambda は trust anchor を起動時に S3 から読む

lambda/verify/handler.py は、リクエストごとに c2pa-python の Reader で検証を走らせるシンプルな関数です。1 点だけポイントがあって、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 の署名・検証を「コマンドラインの単発デモ」から「ブラウザで触れるサービス」まで持ち上げました。鍵は KMS に閉じ込めたまま、ブラウザから 1 ファイルを投げるだけで署名済みアセットが返ってきて、別タブで検証もできる、という最小単位の SaaS が手に入った形です。

最小構成として実装してみると、組み合わせとしてはむしろシンプルで、改めて解説するべきことはあまり残りませんでした。一方で、この構成を起点に「実際のサービスとして育てる」段になると考えるべき論点が一気に増えます。leaf 証明書を期限切れまで放置するわけにはいかないので TSA を入れて長期検証に耐えるようにする、過去アセットの正当性を保つために Manifest Repository を別リソースに切り出す、IP 制限から Cognito ベースのマルチテナント認証に切り替える、動画や Live Video のように 10MB を超えるペイロードを扱うために非同期パイプラインを敷き直す。どれも独立した記事に値する深さがあるので、本シリーズで 1 本ずつ取り上げる予定です。

C2PA は仕様が大きい割に、署名と検証の本質は「鍵をどう持って、誰がそれをどう呼ぶか」だけです。今回の構成で「KMS + Lambda + API Gateway」の組み合わせ感が掴めれば、別のクラウド(GCP の Cloud KMS + Cloud Run、Azure の Key Vault + Functions)に置き換えるのも、AWS 内で SQS / Step Functions に分解するのも、設計を一段ずつ動かすだけの話になります。本記事がその「最初の足場」として読み手の手元に残ってくれたら嬉しいです。続編にもぜひ期待してください。

参考資料