Claude Access Transparencyの検証手順 — axt-verifyで改ざんを見抜く
Access Transparencyのイベントが後から消されたり書き換えられたりしていないかを、transparency logとaxt-verifyで自組織のマシン上から確かめる手順と、終了コードの読み方を扱います。
Access Transparencyのイベントが後から消されていないかは、transparency logとaxt-verifyで自分で確かめられます。axt-verifyはAnthropicが公開しているオープンソースの検証ツールです。組織のUUIDとCompliance Access Keyを渡してrunを定期実行するだけで、ログが追記だけで伸びていること、Activity Feedのイベントがログの記録とバイト単位で一致することを確認します。transparency logはベータ版で、Access Transparency自体が要請ベースの提供です。エンドポイントやレスポンスの形は変わる可能性があります。
Access Transparencyの仕組みそのものはClaude Access Transparencyとは何かで扱っています。ここでは、そのフィードを「信じるための裏付け」を取る側の手順に絞ります。
transparency logは何を証明し、何を証明しないのか
transparency logは、追記しかできない記録を改ざん検知できる形にする技術です。ログが伸びるたびにAnthropicが「チェックポイント」に署名します。チェックポイントは、ログの識別名(origin)、現在のエントリ数、全エントリを束ねるMerkleツリーのルートハッシュを書いた短い文書です。過去のチェックポイントを手元に持っていれば、あとから「今のログは、あのチェックポイントの内容を同じ順序のまま含んでいるか」を証明させられます。
組織ごとにログが1本あり、originはaxt.anthropic.com/<組織UUID>で固定です。Access Transparencyのイベント(anthropic_accessとcmek_preserve)は、フィードに出る前にまずログへリーフとして追記されます。フィード上のイベントには、ログ内での位置(0始まり)を示すtransparency_log_leaf_indexが付きます。
検証で分かることと分からないことは、はっきり分かれています。
| 観点 | 証明される | 証明されない |
|---|---|---|
| イベントの中身 | 証明されるリーフに含まれる11フィールドが、Anthropicがログに書いた内容とバイト単位で一致する | 証明されないworkspace_uuidなどリーフ外のフィールドは対象外 |
| ログの履歴 | 証明される手元のチェックポイント以降、履歴が追記だけで伸びている | 証明されない— |
| 記録の網羅性 | 証明される— | 証明されない全アクセスが記録されたか、記録が実態を正しく表すかは証明しない |
| フィードの網羅性 | 証明される— | 証明されないフィードがログの全リーフを載せたかは、包含証明単体では言えない |
最後の行は見落としやすい点です。包含証明は「あなたに見せたイベントが、ログに確かに入っている」ことを示すだけです。フィードから消されたイベントは、そもそも検証対象に上がりません。ログのエントリバンドルには全リーフが入っているので、完全な集合を直接読んで突き合わせる余地は残されています。
transparency_log_leaf_indexが付いていること自体も、証明ではなく目印です。検証を通す前に「ログにコミット済み」と扱わないのが原則です。
検証を始める前に用意する3つのもの
準備するものは次の3つです。
read:compliance_activitiesスコープを持つCompliance Access Key。Activity Feedを読むキーと同じもので、transparency log専用の権限はありません。取得手順はCompliance APIのセットアップとアクセスキー作成にあります。- 組織のUUID。Claude ConsoleのSettings > Organizationで確認します。
- 最後に検証したチェックポイントを保管する場所。これがあって初めて「今日は整合している」が「監視を始めてからずっと整合している」に変わります。
2つ目で注意が必要です。UUIDはCompliance APIのレスポンスのorganization_uuidと同じ値ですが、Consoleから取るのが正しいやり方です。この値が「そのチェックポイントは自分のもの」という根拠になるので、検証対象のAPIから取ってはいけません。originの文字列も自分で組み立てます。APIの応答から読み取る値ではありません。
親組織のキーを使う場合は、子組織ごとにorganization_idを指定して読みます。子組織のUUIDも、親のConsole側の組織一覧から取ります。
axt-verifyを入れて定期実行する
axt-verifyはGoの単一バイナリで、Go 1.26以降が要ります。Compliance Access Keyは環境変数ANTHROPIC_COMPLIANCE_ACCESS_KEYだけから読み、フラグやファイルでは受け取りません。
go install github.com/anthropics/axt-verify/cmd/axt-verify@latest
export ANTHROPIC_COMPLIANCE_ACCESS_KEY="<Compliance Access Key>"
axt-verify --org 25f6429a-3293-49bf-afed-cb312911554b \
--state /var/lib/axt-verify/25f6429a-3293-49bf-afed-cb312911554b.state \
run上のUUIDはドキュメントの例です。自分のUUIDに置き換えます。--stateを省くとカレントディレクトリのaxt-verify.stateに書かれるので、cronから回すときは必ず絶対パスを渡します。
runは次の順で動きます。
- 最新のチェックポイントを取り、署名とorigin行を検証する
- 前回保存したチェックポイントから追記だけで伸びていることを、一貫性証明(consistency proof)で確かめる
- Activity FeedのAccess Transparencyイベントを読み、各イベントのリーフを組み立て直して包含証明(inclusion proof)を検証する
- 新しいチェックポイントと進捗を
--stateファイルに保存する
ログに載る署名鍵はaxt-verifyのリリースに組み込まれています。APIに「どの鍵を信じるか」を尋ねないので、配信経路が書き換えられても信頼の起点は動きません。実行頻度は最低でも1日1回、1時間に1回でも妥当とされています。
cronの例です。終了コードが0でなければ通知するようにします。
17 * * * * ANTHROPIC_COMPLIANCE_ACCESS_KEY=$(cat /etc/axt-verify/key) axt-verify --org <uuid> --state /var/lib/axt-verify/state --json run >>/var/log/axt-verify.jsonl || alert "axt-verify exit $?"--jsonを付けると、結果が1行1オブジェクトで出ます。ok、checkpoint.size、events.verified、events.failed[]などのフィールドを持つので、SIEMやログ基盤にそのまま流せます。SIEM側の設計はCompliance APIのSIEM連携の考え方が使えます。
正常時の出力は次のような形です(ドキュメントの例)。
origin: axt.anthropic.com/25f6429a-3293-49bf-afed-cb312911554b
checkpoint: tree size 1207, root hash nB9mUdyOWMp0zXI0k1S…=
append-only: verified from tree size 1188
events: 19 verifiedイベントの4つの結果と終了コードの読み方
runが返す1イベントごとの結果は4種類です。
| 結果 | 意味 | 対応 |
|---|---|---|
| Verified | 意味組み立て直したリーフが、イベントの指すindexに署名済みログ上で確かに存在する | 対応なし |
| Pending | 意味indexが最新チェックポイントの範囲外にある。イベントが出た直後は普通に起きる | 対応次回以降のrunが再検証する。24時間を超えると失敗扱い |
| Not logged | 意味indexなしでフィードに出たイベント | 対応一覧を目で確認する。実行自体は失敗にならない |
| Failed | 意味検証に失敗した | 対応終了コード1の扱い |
Not loggedが出る場面は限られます。組織がAccess Transparencyに未加入の間、または組織のログが作られる前に記録されたイベントです。ログ作成後、かつ加入期間中の日付のイベントにindexがないのは想定外です。終了コード0のまま流れてしまうので、サマリーか--jsonのevents.not_loggedを必ず読みます。
終了コードで見るなら次のとおりです。
| 終了コード | 意味 | 取るべき動き |
|---|---|---|
| 0 | 意味失敗なし(Not loggedとrun内のPendingは失敗ではない) | 取るべき動き— |
| 1 | 意味検証失敗 | 取るべき動きセキュリティ上の発見として扱う。stateファイルと出力を保存し、Anthropicのアカウント担当かサポートに連絡する |
| 2 | 意味使い方か設定の誤り | 取るべき動きフラグ、環境変数、stateファイルを直す |
| 3 | 意味完了できなかった | 取るべき動き再実行する。続くなら調べる |
終了コード1の原因は多岐にわたります。origin違いの署名、署名の不一致、ログが縮んだか保存済みチェックポイントを延長できない状態、同じツリーサイズで異なるルートハッシュを持つ2つの署名済みチェックポイントなどです。ウィンドウ内のイベントが前回と違う中身やindexで出てきた場合も同じ扱いです。失敗時には両方のチェックポイントと証明が出力に残るので、Anthropic側のサービスが落ちていても証拠として単独で成立します。
終了コード3は、ネットワークエラーやレート制限のほか、ログがまだ作られていない場合にも出ます。最初のAccess Transparencyイベントが記録されるまで、transparency logのエンドポイントはすべて404を返します。加入直後のrunが3で終わるのは想定内です。Activity Feedに最初のイベントが出てから数日たっても404が続く場合は、エスカレーションの対象になります。
署名鍵のローテーションと1で落ちたとき
署名鍵はローテーションされます。計画的な切り替えは、新しい鍵で最初のチェックポイントに署名する30日以上前に、ドキュメントの「Published key fingerprints」表で予告されます。現在の鍵は、鍵ハッシュ1dff5fe4、SHA-256フィンガープリント1dff5fe420d49743fe444a04fc17f818eea856699dec2ebbc24df15602c74a58で、署名開始日は2026-08-17です。axt-verifyのREADMEにも同じフィンガープリントが載っています。
axt-verifyは1リリースに鍵を1つだけ持ちます。切り替え日にAnthropicが新しい鍵での署名を始め、同じ日に対応するリリースが出ます。旧リリースを切り替え日以降に動かすと終了コード1で落ち、新リリースを切り替え日より前に動かしても同じです。マッチするリリースに揃えれば解消します。
終了コード1が出たら、失敗した署名行の鍵ハッシュを確かめます。axt-verifyは、応答の署名が主張する鍵ハッシュと、自分が信頼している鍵のハッシュを8桁の16進で出力します。
- 表に載っている鍵で、切り替え日を過ぎている: 対応リリースへ上げる(または
--log-keyで新しい鍵を渡す) - 表にない鍵:
GET /keysが何を返していようと、正当ではありません。セキュリティ上の発見です
READMEは、署名が検証できない以上その鍵ハッシュは「主張」にすぎない点も強調しています。表にある鍵のハッシュを付けたまま偽のチェックポイントを渡すこともできるので、リリースを上げても失敗が続くなら、原因が回転ではないものとして扱います。
手元のチェックポイントとイベントを保管する
runが検証できるのは「前回のstateファイル」からの延長です。もっと強い証拠は、ある日のログの姿を自分で保管しておくことです。
axt-verify --org <uuid> --state /var/lib/axt-verify/archive.state \
checkpoint --from yesterday.ckpt --save today.ckpt
mv today.ckpt yesterday.ckptcheckpointはフィードを読まずにチェックポイントと追記の整合だけを見るので、runより軽く、間隔を詰められます。stateファイルはrunと別にします。--saveは検証に成功した後だけ、チェックポイントの本文をそのまま書き出します。数か月後でも、保管したチェックポイントのツリーサイズからの一貫性証明が現在のログにつながらなければ検証は失敗します。
鍵が入れ替わったあとの古いアーカイブは--fromでは署名検証が通らないので、--from-trustedで渡します。こちらは署名を確かめず、ツリーサイズとルートハッシュをあなた自身の記録として受け取ります。originだけは引き続き照合されます。
イベント側の保管も同じ発想です。フィードからエクスポートしたイベントは、axt-verify events FILEの入力にできます。
axt-verify --org <uuid> events siem-export.jsonevents FILEは、JSONオブジェクト、配列、/v1/compliance/activitiesのページ、1行1オブジェクトのいずれも読めます。Access Transparency以外のタイプの行は無視して数えるだけで、失敗にはしません。stateファイルは使わず、フィードにも触りません。監査人にサンプルを渡して検証してもらう用途にも使えます。ただし取得済みのチェックポイントを持たないので、「ログが昨日と同じものか」までは言えません。それを見るのは定期実行のrunです。
つまずきやすい5つの場面
runの再確認範囲は7日です。直近のイベントだけを読み直すため、古いイベントがフィードから消えたり書き換えられたりしても、runでは気づけません。エクスポートを保管し、あとで同じ範囲を再エクスポートして突き合わせます。再エクスポート側をevents FILEで検証することもできます。- stateファイルを消すと基準がリセットされます。次の
runはログが返す最新のチェックポイントから始まり(終了コード0)、その回はロールバックを検知できません。--saveで保管したチェックポイントを--fromで渡すのが、消えない基準点になります。 - チェックポイントを止められると気づけません。独立したタイムスタンプ機関がないため、古いチェックポイントの再送とフィードの凍結を組み合わせた攻撃は、クライアント側からは検知できません(READMEの制限事項)。保管したチェックポイントを実行間や複数マシンで比較し、想定外に長く増えないログはAnthropicに問い合わせます。
- 親組織のイベントを一括で流すと失敗します。複数の子組織を含むエクスポートを
events FILEに渡すと、--orgと異なるorganization_uuidのイベントがすべて失敗します。組織ごとに分割し、それぞれの--orgで検証します。 --log-keyは信頼の起点を差し替えます。環境変数AXT_VERIFY_LOG_KEYでも同じことができるため、cronの実行環境を保護し、その行を監視します。
自前で実装する場合のリーフの作り方
axt-verifyを使わず自前で検証するなら、リーフ(ログ上の1エントリ)の作り方を厳密に再現する必要があります。リーフは、バージョンバイト0x01の後ろに、Activity Feedで返ったイベントから11フィールドだけを取り出したRFC 8785(JSON Canonicalization Scheme)のJSONを続けたものです。
- 対象は
id、type、created_at、accessed_at、organization_id、organization_uuid、workspace_id、accessor_department、reason_code、actor(typeとemail_address)、resource_details(type、id、parent) - フィードにあるが対象外の
workspace_uuidやtransparency_log_leaf_indexは落とす - 省かれた対象フィールドは
nullとして入れる(空文字列とは別扱い) - バージョン
0x01ではactor.email_addressとresource_details.parentは常にnull - 文字列はサーバーが返した表現のまま使う(タイムスタンプの小数桁も含む)
リーフハッシュはSHA-256(0x00 || エントリ)、内部ノードはSHA-256(0x01 || 左 || 右)です。ドキュメントのテストベクターをPythonで再現すると、次のようになります。
import json, hashlib, base64
event = {
"accessed_at": "2025-07-08T18:39:58Z",
"accessor_department": "Trust & Safety",
"actor": {"email_address": None, "type": "anthropic_actor"},
"created_at": "2025-07-08T18:40:00Z",
"id": "activity_01GPXmAhizavrUuoXNn3tzeA",
"organization_id": "org_015gtSHLz269eTwgrH8NX5yk",
"organization_uuid": "25f6429a-3293-49bf-afed-cb312911554b",
"reason_code": "safety_review",
"resource_details": {"id": "msg_01HXAMPLE12345678", "parent": None, "type": "message"},
"type": "anthropic_access",
"workspace_id": "wrkspc_01PaGUP2rbg1XDh7Z9W1CEpd",
}
entry = b"\x01" + json.dumps(event, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode()
print(base64.b64encode(hashlib.sha256(b"\x00" + entry).digest()).decode())
# 6ro7vTcFq+sYDZiiGvZaFetUIoMSLig3DGDrCKK1HFU=このスクリプトの出力は、ドキュメントが示す期待値6ro7vTcFq+sYDZiiGvZaFetUIoMSLig3DGDrCKK1HFU=と一致します。数値がリーフに現れないので、この簡易なシリアライズで足りますが、一般のイベントで使う前には自前の実装をこのテストベクターで確かめるのが前提です。
チェックポイントと証明の取得は、素のcurlでもできます。
curl --fail-with-body -sS -G \
"https://api.anthropic.com/v1/compliance/transparency_log/inclusion" \
--data-urlencode "leaf_index=41" \
--header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" \
--header "anthropic-version: 2023-06-01"応答のhashesは、リーフからルートへ向かう兄弟ハッシュ(base64)の並びです。同じ応答のcheckpointが、この証明の検証先になります。証明のエンドポイントは便宜のためのもので、ハッシュタイル(/tile/{level}/{index})から自分で証明を計算すれば、証明APIの出力を信用しなくて済みます。そのときは、ECDSAのノート鍵に対応したtlog-tilesライブラリを使います。
まとめ
transparency logの検証は、フィードを信じる前提を「自分で持つ2つの基準」に置き換える作業です。1つはConsoleから取った組織UUIDが決めるorigin、もう1つは前回保存したチェックポイントです。axt-verify runを1時間ごとに回し、終了コードが1なら証拠を残して連絡、3なら再実行という運用が最小構成になります。あわせて--saveでチェックポイントを、フィードのエクスポートでイベントを手元に残しておくと、数か月後にも「その時点の記録が今も含まれているか」を証明できます。
ログが守るのはAnthropicがコミットした内容の不変性で、アクセスの網羅性ではありません。その線引きを忘れなければ、フィード側の使い方はCompliance API Activity Feedの使い方と組み合わせて設計できます。