監査人は、AI エージェントが前四半期に何をしたかの証拠を求めます。あなたは監査ログの CSV エクスポートを彼らに渡します。彼らは 1 つの質問をします。「これが事後編集されていないことを証明できますか?」ほとんどのチームはそれができません。ログは変更可能なデータベースに保存され、ログを書き込んだのと同じシステムによってエクスポートされます。監査人は信仰に基づいてすべての証跡をとらなければなりませんが、まさにそれが証拠ではない特性です。
この投稿では、プラットフォームが信頼せずに保持する監査証拠をどのように構築するかについて説明します。 前の投稿 では、Claude Code および MCP の展開において、エージェントごとの ID と改ざん防止台帳が重要である理由について説明しました。これは、証拠を独立して検証可能にする 3 つのプロパティ、つまりハッシュ チェーン、イベントごとの暗号署名、監査人のツールがすでに理解している機械読み取り可能なコンプライアンス形式でのエクスポートをさらに深く掘り下げたものです。
ハッシュ チェーン: すべてのイベントは以前のすべてのイベントにコミットします
台帳は追加専用で、テナントごとにハッシュ チェーンされます。各イベントの prev_hash フィールドには、前のイベントの SHA-256 ハッシュが含まれます。イベント N のチェーン ハッシュは、イベント N-1 の prev_hash と連結されたイベント N 自身のフィールド (テナント、シーケンス番号、タイムスタンプ、アクター、アクション、ターゲット、メタデータ ダイジェスト、ペイロード ハッシュ) を含む正規のバイナリ プリイメージに対して計算されます。シーケンス 1 では、prev_hash はすべてゼロ、つまりジェネシス アンカーです。
重要なプロパティ: チェーンの途中でイベントを変更、挿入、または削除すると、そのハッシュが変更され、次のイベントの prev_hash リンクが切断され、次のイベントも切断され、というように先端まで続きます。チェーンをたどって各ハッシュを再計算することで、単一の編集を検出できます。
プリイメージは、JSON ではなく、バージョン管理されたドメイン区切り文字を備えた固定長のプレフィックス付きバイナリ エンコードです。これは、JSON ベースのハッシュが未解決のままにする 3 つの攻撃対象領域に対する意図的な選択です。
- キーの順序: JSON オブジェクトのキーの順序は、ほとんどのシリアライザーでは保証されません。キーの順序が異なると、意味的に同一のデータであっても異なるハッシュが生成され、これにより誤ったチェーン ブレークが発生します。さらに悪いことに、攻撃者がキーの順序を変更して一致するハッシュを偽造できるようになります。
- 空白と数値の書式設定:
{"seq": 1}、{"seq":1}、および{"seq": 1.0}は意味的には同等の JSON ですが、生成される SHA-256 ダイジェストは異なります。 - 連結偽造: 長さのプレフィックスがない場合、2 つの隣接する短いフィールドをマージして、同じハッシュを持つ 3 番目の長いフィールドを偽造することができます。各フィールドにそのバイト数 (4 バイトのビッグエンディアン) を長さの接頭辞として付けると、これが閉じられます。
core/internal/store/canon/canon.go の実装が唯一の真実の情報源です。 Append (書き込み) と Verify (読み取り) は両方とも、同じ EventHash 関数を呼び出します。ドリフトする 2 番目の実装はありません。
func EventHash(e Event) []byte {
var buf []byte
buf = lps(buf, domainEvent) // "olivares.audit.v1"
buf = lps(buf, e.TenantID)
var seq [8]byte
binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
buf = append(buf, seq[:]...)
buf = lps(buf, e.OccurredAt)
buf = lps(buf, e.Actor)
buf = lps(buf, e.ActorKind)
buf = lps(buf, e.Action)
buf = lps(buf, e.TargetKind)
buf = lps(buf, e.TargetID)
buf = append(buf, fixed(e.MetaDigest)...)
buf = append(buf, fixed(e.PayloadHash)...)
buf = append(buf, fixed(e.PrevHash)...)
sum := sha256.Sum256(buf)
return sum[:]
}
ドメイン区切り文字 "olivares.audit.v1" は、ハッシュをその目的とバージョンにバインドします。異なるドメインのハッシュ (チェックポイント、ペイロード、メタデータ ダイジェスト) は、生のバイトが偶然一致したとしても、イベント ハッシュと衝突することはありません。
Ed25519 署名: すべてのイベントは独自のアンカーです
ハッシュ チェーンは内部の一貫性を証明しますが、信頼性を証明するものではありません。生のデータベース書き込みアクセス権を持つ攻撃者は、変更されたイベントを使用してチェーン全体を最初から再計算し、元のチェーンとは異なりますが、内部的には一貫した有効なチェーンを生成する可能性があります。ハッシュ チェーンは 改ざん を検出します。それらは起源を証明するものではありません。
イベントごとの Ed25519 署名により、この gap が閉じられます。レジャーに追加されるすべてのイベントは、書き込み時に署名されます。この署名には、ドメインで区切られたテナントのプリイメージ、シーケンス番号、およびイベントのチェーン ハッシュが含まれます。
domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash
署名はイベントに保存されますが、設計によりチェーン ハッシュのプリイメージからは除外されます。これは偶然ではありません。署名がハッシュに含まれている場合、イベントに署名すると、それが証明するはずのハッシュが変更されてしまいます。署名はハッシュを変更せずに証明します。
公開キーのみを保持する外部検証者は、各イベントを個別に確認できます。イベントのフィールドからチェーン ハッシュを再計算し、プリイメージを再構築し、Ed25519 署名を検証します。署名後にイベントが変更された場合、その特定のイベントに対する署名チェックは失敗します。検証者は証拠を作成したシステムを信頼する必要はありません。
このプラットフォームはキーのローテーションもサポートしています。署名鍵が途中で変更されたチェーンは、現在の鍵と前世代の公開鍵を固定することによってエンドツーエンドで検証します。 verify 関数は候補キーのセットを受け入れ、いずれかの候補が検証した場合にイベントが有効であるとみなします。
イベントごとの署名に加えて、定期的なチェックポイントにより、別の署名ドメイン (olivares.audit.checkpoint.v1) でチェーン チップが公証されます。データベース レベルだけでなく、ホスト レベルのセキュリティ侵害に対して防御する必要がある組織の場合、チェックポイントはオフボックスの KMS/HSM キー (AWS KMS、GCP Cloud KMS、Azure Key Vault) によって署名できます。この場合、秘密キーはホスト上に存在しません。イベントごとのシグネチャは DB のみの攻撃者を処理します。オフボックス チェックポイントは、ホスト侵害攻撃者を処理します。 2 つの脅威モデルは異なります。いずれの署名も単独では両方をカバーしません。
台帳契約: 同じ取引で封印される
監査システムにおける一般的な障害は、状態の変化と監査レコードの間の結果的な整合性です。状態が変化すると、監査書き込みがキューに入れられるかバッチ処理されます。監査書き込みが失敗した場合、状態変更はすでにコミットされています。その結果、システムには存在するが証拠には存在しない、監査されていない突然変異が発生します。
このプラットフォームはより強力な契約を強制します。状態の突然変異と台帳シールの両方が同じデータベース トランザクションで発生します。シールが失敗すると、遷移全体がロールバックされ、状態の変更は決してコミットされません。これはベストエフォート型ではありません。それは拒否クローズです。
セッション実行時台帳はこれを示しています。 Claude Code セッションの状態が遷移すると (作成、起動、停止、停止、失敗)、appendRunEvent は遷移を 2 つの場所にアトミックに記録します。
sc.Audit().Append経由のグローバル ハッシュ チェーン監査台帳 —PayloadHashによって固定された改ざん防止チェーン。- セッションごとにクエリ可能なレジャー —
audit_seqによってグローバル チェーンにリンクされた追加専用のプロジェクション。
どちらの書き込みも呼び出し元の Mutate トランザクション内で行われます。 runtime_ledger.go のコード コメントは、設計意図を直接述べています。「台帳は記録システムであるため、シールが失敗すると移行全体がロールバックされます。これはベストエフォートではありません。」
PayloadHash 自体は、正規の非機密遷移事実 (実行参照、シーケンス、イベント タイプ、状態遷移、およびタイムスタンプ) のみをコミットします。トランスクリプトのコンテンツ、プロンプト、環境値、またはシークレットは決して含まれません。台帳は何が起こったかを証明します。 発言された内容は保存されません。
同じパターンが、workspace_ledger.go のワークスペース ファイルの変更にも適用されます。ファイルの書き込み、mkdir、移動、または削除は、ファイル システム操作が実行される「前」にシールされます。証拠を追加できない場合、突然変異は実行されません。シールには、操作タイプ、パス、および書き込まれたコンテンツの SHA-256 が含まれますが、コンテンツ バイト自体は含まれません。
OSCAL エクスポート: 監査人のツールが取り込んだ機械読み取り可能な証拠
改ざん防止台帳は必要ですが、監査人にとっては十分ではありません。証拠が独自の形式である場合でも、監査人はそれを解釈するツールに依存します。 OSCAL — NIST によって管理されている Open Security Controls Assessment Language — は、この gap を閉じる形式です。
このプラットフォームは、封印された証拠パッケージを、次の 3 つのモデルを含む OSCAL バンドルとしてエクスポートします。
- コンポーネント定義: コンプライアンス フレームワーク (NIST SP 800-53、ISO 27001、EU AI 法など) に対する実装要件として表現されたコントロール プレーンの機能。実装された各要件には、コントロール ID、それを証明する機能キー、およびカスタム プロパティとしての実際のステータスが含まれます。
- 評価結果: OSCAL 準拠ステータスのコントロールごとの所見。各所見のターゲットには、理由フィールドに正確な製品ステータスが保存された
satisfiedまたはnot-satisfiedが含まれます。 - コントロール マッピング: OSCAL 1.2.0 コントロール マッピング モデルを使用した、フレームワーク コントロールからプラットフォームの機能参照モデルへの横断歩道。関係は常に
intersects-withです。機能はコントロールの一部に対応します。決して適合性を主張するものではありません。その主張は、実際の運用証拠に基づいた評価結果にのみ存在します。
OSCAL エクスポートの正直さの制約は、明示的に記述する価値があります。 OSCAL の検索ステータス列挙型には、satisfied と not-satisfied という 2 つの値があります。 「partial」や「仕様」はありません。設計によってアドレス指定された partially 実装されたコントロール、gapped、または unmapped は、OSCAL not-satisfied にマップされ、実際の製品ステータスは status.reason とプラットフォーム独自の名前空間 (https://olivares.ai/ns/oscal) の下のカスタム プロパティに乗ります。エクスポートでは、partially-met コントロールが satisfied にロンダリングされることはありません。シール時の実際の運用証拠によって裏付けられたコントロールのみが、OSCAL satisfied を受け取ります。
すべての OSCAL ドキュメントには、レジャー アンカー プロパティ (マニフェスト ハッシュ、シール時のレジャー シーケンス番号、レジャー ハッシュ、整合性検証結果) が含まれています。これらは、監査人が GRC ツールで読み取る OSCAL 文書と、独立して検証できる基礎となる改ざん明示チェーンとの間の橋渡しとなります。
オフライン認証とは具体的に何を意味するのか
「オフライン認証」はマーケティング用語ではありません。具体的な技術手順が説明されています。検証者はエクスポートされた証拠を取得し、Air-gapped マシン上で検証ツールを実行し、証拠を作成したシステムにネットワーク アクセスせずに証拠の整合性を確認します。
プラットフォームのアーカイブ エクスポートは、JSONL セグメント (イベントごとに 1 行、正規 JSON) のディレクトリ ツリーとセグメントごとのマニフェストを書き込みます。マニフェストには、セグメントのシーケンス範囲、イベント数、最初と最後のチェーン ハッシュ、イベント ファイルの SHA-256、およびセグメント間の連続性のための前のセグメントの最後のハッシュが記録されます。
次に、オフライン ベリファイア (VerifyArchiveDir) は、ネットワーク呼び出しを行わずに完全に定メモリ内で次の処理を実行します。
- マニフェストをロードし、イベント ファイルとペアにします。 野良イベント ファイル (マニフェストなし) またはイベント ファイルが欠落しているマニフェストは失敗します。証拠の単位はペアです。
- 各イベント ファイルを 1 行ずつストリーミングします。 イベントごとに、ライブ システムが使用するものと同じ
EventHash関数を使用して、アーカイブされたフィールドからチェーン ハッシュを再取得します。保存されているハッシュと比較します。prev_hashリンケージを確認し、gap-freedom をシーケンスします。 - 正規性を確認します。 解析された各行を再マーシャリングし、ディスク上のバイトに対してバイト同一の出力が生成されることを確認します。これにより、ハッシュ チェックには合格しても隠されたデータが含まれる未知のフィールドの密輸や重複キー攻撃を防止できます。
- イベントごとの Ed25519 署名を検証します。 チェックポイント以外のイベントごとに、署名のプリイメージを再構築し、固定された公開キーと照合して検証します。
- チェックポイント署名を確認します。 チェックポイント イベントごとに、チェックポイント ドメインの署名を固定されたチェックポイント キーと照合して確認します。イベント キーのみが固定されている (チェックポイント キーがない) 場合、チェックポイント イベントは「検証不能」、つまり拒否クローズされ、スキップ不可能とマークされます。
- セグメント間の連続性を確認します。 各セグメントの最初のシーケンスが前のセグメントの最後のシーケンスに 1 を加えたものに続いていること、および前のセグメントの最後のハッシュが現在のセグメントの
prev_segment_last_hashと一致することを確認します。 - イベント ファイル ダイジェストを確認します。 ストリーミング中に計算されたイベント ファイルの SHA-256 は、マニフェストの
events_sha256と一致する必要があります。
検証者は、最初に検出した不一致を、特定のシーケンス番号と機械可読な理由 (hash-mismatch、prev-mismatch、seq-gap、event-sig-invalid、event-sig-missing、checkpoint-sig-invalid、count-mismatch、events-sha256-mismatch、または) とともに報告します。 segment-link-mismatch。
正直な制限: オフライン検証ツールは、チェックした範囲を正確に証明し、その範囲外は何も証明しません。削除されたプレフィックスまたはテールはオフラインでは検出できません。ディレクトリには、チェーンがどこで始まったのか、どこで終わったのかがわかりません。検証レポートには、StartsMidChain フラグを含むテナントごとの Ranges フィールドが含まれるため、監査人は何が証明されたかを正確に知ることができます。ライブ システムのチェーン内の署名済みチェックポイントはテールをカバーします。オフライン エクスポートはアーカイブされた範囲をカバーします。これらは一緒になって完全な証明書を形成します。
| レイヤー | それが証明するもの | 証明されないこと |
|---|---|---|
| ハッシュチェーン | 内部の一貫性。いかなる編集も連鎖を断ち切ります | 起源 (イベントを書いた人) |
| イベントごと Ed25519 | 起源。各イベントにはキーホルダーのサインが入っていました | ホストレベルの侵害に対する防御 |
| オフボックスチェックポイント | ホスト侵害に対する耐性 (KMS/HSM キーがホスト上に存在しない) | イベントごとの粒度 (チェックポイントのみを対象とする) |
| OSCAL エクスポート | 機械可読でフレームワークにマッピングされたコンプライアンスの証拠 | すべての管理が完全に満たされていること (生きた証拠のみが重要) |
| アーカイブの検証 | 上記すべてのオフライン再導出 | エクスポートされた範囲の前後のイベント |
コードパス: 状態の突然変異から封印された証拠まで
Claude Code セッション状態の変更から検証可能な証拠までのシーケンスは 3 つの層に影響します。 runtime_ledger.go では、appendRunEvent 関数は、正規の長さプレフィックス付き遷移フィールドを SHA-256 ハッシュすることによって、PayloadHash を構築します。
func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
h := sha256.New()
for _, part := range []string{
runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
} {
_, _ = h.Write([]byte(strconv.Itoa(len(part))))
_, _ = h.Write([]byte{':'})
_, _ = h.Write([]byte(part))
}
var sum [32]byte
copy(sum[:], h.Sum(nil))
return sum
}
次に、そのハッシュは sc.Audit().Append に渡され、次のテナントごとのシーケンス番号が割り当てられ、前のイベントのハッシュにリンクされ、EventHash 経由でこのイベントのチェーン ハッシュが計算され、Ed25519 で署名され、シールされたイベントが挿入されます。これらのすべてが呼び出し元のトランザクション内で行われます。
結果: トランザクションがコミットされるまでに、セッションの状態変化とその改ざん防止監査レコードは両方とも永続化されるか、両方ともロールバックされます。状態が変化したが変化しなかったという証拠はありません。
リンク
- 製品: Audit — 監査証跡と証拠のエクスポート
- 製品: コンプライアンス — フレームワーク評価と OSCAL
- セキュリティ モデル — 台帳を支える信頼モデル