Documentation
¶
Overview ¶
Package audit provides hash-chain log, export, and verification for the AgentPaaS daemon. It implements a canonical JSONL audit chain where each record is cryptographically linked to its predecessor via SHA-256 hashes, ensuring tamper-evident logging of security-relevant actions.
The core types are AuditRecord (the canonical record schema with deterministic JSON serialization) and AuditWriter (a daemon-owned append-only writer with fsync durability).
Record chain ¶
Each record carries seq, prev_hash, record_hash, and metadata fields.
- record_hash = SHA-256(canonical JSON of the record, omitting record_hash)
- prev_hash for seq=1 is the empty string (genesis)
- prev_hash for seq>1 is the record_hash of the preceding record
Writer guarantees ¶
- Append-only: records are appended to a JSONL file, one per line.
- Serialized: a mutex ensures only one Append executes at a time.
- Durable: each line is fsynced immediately after writing.
- Fail-closed: if the underlying file write or fsync fails, Append returns an error so the caller can abort the guarded operation.
- Head reconstruction: on open, the writer replays the file to find the latest seq and record_hash.
Checkpoints ¶
Checkpoints are signed snapshots of the audit chain head created at a configurable cadence or on demand. Each checkpoint records the head anchor (seq + record_hash) at a point in time, is cryptographically linked to the previous checkpoint, and is signed with the daemon's audit signing key. The checkpoint chain provides a separate signed attestation that can be used to verify the integrity of the audit log at the checkpointed point.
Audit Export Bundle ¶
ExportAuditBundle creates a portable bundle containing a copy of the audit JSONL, checkpoints JSONL, the daemon public key (PEM), and a signed manifest tying them together. The manifest is signed with the daemon's audit signing key and includes metadata (record count, head anchor, checkpoint count, public key fingerprint).
VerifyAuditBundle performs offline verification of a bundle using only the bundle contents and the expected daemon audit public key fingerprint. Verification proves:
- The bundled audit chain is internally consistent (hash chain intact).
- The bundled checkpoint chain is internally consistent.
- Checkpoint signatures are valid against the bundled public key.
- The manifest signature is valid for the expected daemon audit key.
Offline verification does NOT prove global transparency-log anchoring. It proves bundle integrity at export time only.
Index ¶
- Constants
- func GenerateCheckpointKey() (privateKeyDER []byte, publicKey *ecdsa.PublicKey, err error)
- func LoadOrGenerateCheckpointKey(path string) (privateKeyDER []byte, publicKey *ecdsa.PublicKey, err error)
- func PubKeyFingerprint(pubKeyDER []byte) string
- func PublicKeyFromCheckpointKeyDER(keyDER []byte) (*ecdsa.PublicKey, error)
- func RebuildSQLiteIndex(auditPath, dbPath string) error
- func WriteCheckpointJSONL(path string, cp *CheckpointRecord) error
- type AuditAppender
- type AuditRecord
- type AuditWriter
- type BundleManifest
- type BundlePaths
- type CheckpointError
- type CheckpointErrorType
- type CheckpointManager
- func (m *CheckpointManager) CheckpointSeq() int64
- func (m *CheckpointManager) Close() error
- func (m *CheckpointManager) CreateCheckpoint(auditHeadSeq int64, auditHeadHash string) (*CheckpointRecord, error)
- func (m *CheckpointManager) LatestAnchor() (int64, string)
- func (m *CheckpointManager) ShouldCheckpoint(auditHeadSeq int64) bool
- type CheckpointRecord
- type ExportBundleOptions
- type HostedContext
- type SQLiteIndexer
- func (ix *SQLiteIndexer) Close() error
- func (ix *SQLiteIndexer) QueryByEventType(eventType string, limit int) ([]AuditRecord, error)
- func (ix *SQLiteIndexer) QueryBySeq(seq int64) (*AuditRecord, error)
- func (ix *SQLiteIndexer) Rebuild(auditPath string) error
- func (ix *SQLiteIndexer) RecordCount() (int, error)
- type VerificationResult
- type VerifyAuditBundleResult
Constants ¶
const DefaultCheckpointCadence int64 = 100
DefaultCheckpointCadence is the number of audit records between automatic checkpoints.
const EventTypeAgentForked = "agent_forked"
const EventTypeImmutableViolation = "immutable_violation"
const EventTypeInstallAliasChanged = "install_alias_changed"
const EventTypeInstallCredentialMapped = "install_credential_mapped"
const EventTypeInstallDowngradeAllowed = "install_downgrade_allowed"
const EventTypeInstallPolicyApproved = "install_policy_approved"
Install consent event type constants (B23).
const EventTypeInstallRemoved = "install_removed"
const EventTypeMCPToolCall = "mcp_tool_call"
const EventTypeMCPToolDenied = "mcp_tool_denied"
const EventTypePublisherIdentityCreated = "publisher_identity_created"
Publisher identity event type constants.
const EventTypePublisherIdentityExported = "publisher_identity_exported"
const EventTypePublisherIdentityImported = "publisher_identity_imported"
const EventTypePublisherIdentityRotated = "publisher_identity_rotated"
const EventTypePublisherKeyConflict = "publisher_key_conflict"
const EventTypePublisherRemoved = "publisher_removed"
const EventTypePublisherTrusted = "publisher_trusted"
Trust store event type constants.
const EventTypeSecretInjected = "secret_injected"
const EventTypeSecretLeased = "secret_leased"
const EventTypeSecretRead = "secret_read"
Variables ¶
This section is empty.
Functions ¶
func GenerateCheckpointKey ¶
GenerateCheckpointKey creates a new ECDSA P-256 key pair for checkpoint signing. The private key is returned as PKCS#8 DER bytes for storage. Use x509.ParsePKCS8PrivateKey to recover the ecdsa.PrivateKey.
func LoadOrGenerateCheckpointKey ¶
func LoadOrGenerateCheckpointKey(path string) (privateKeyDER []byte, publicKey *ecdsa.PublicKey, err error)
LoadOrGenerateCheckpointKey loads an encrypted ECDSA key from path, or generates and persists a new encrypted key if the file does not exist. The key is encrypted at rest with AES-256-GCM using a passphrase-derived key. If the file exists in legacy unencrypted DER format, it is encrypted and rewritten atomically on load.
func PubKeyFingerprint ¶
PubKeyFingerprint computes the hex-encoded SHA-256 fingerprint of a PKIX DER-encoded public key. This is the value stored in a BundleManifest and used by offline verifiers to identify the expected daemon audit key.
func PublicKeyFromCheckpointKeyDER ¶
PublicKeyFromCheckpointKeyDER parses a PKCS#8 ECDSA private key DER blob and returns its public key.
func RebuildSQLiteIndex ¶
RebuildSQLiteIndex is a convenience function that opens a SQLite index, rebuilds it from the given audit JSONL path, and closes it.
func WriteCheckpointJSONL ¶
func WriteCheckpointJSONL(path string, cp *CheckpointRecord) error
WriteCheckpointJSONL serializes a checkpoint record as a single JSONL line and appends it to the given file path. The file is created if it does not exist.
Types ¶
type AuditAppender ¶
type AuditAppender interface {
Append(record AuditRecord) error
}
AuditAppender is implemented by audit sinks that accept audit records.
type AuditRecord ¶
type AuditRecord struct {
Seq int64 `json:"seq"`
PrevHash string `json:"prev_hash"`
RecordHash string `json:"record_hash"`
Timestamp string `json:"timestamp"`
EventType string `json:"event_type"`
DeploymentMode string `json:"deployment_mode"`
Actor string `json:"actor"`
Payload map[string]interface{} `json:"payload"`
HostedContext *HostedContext `json:"hosted_context,omitempty"`
}
AuditRecord represents a single entry in the audit log. Each record is cryptographically linked to its predecessor via the hash chain.
Fields:
- Seq: monotonically increasing sequence number (1-based).
- PrevHash: hex-encoded SHA-256 of the previous record's canonical JSON (empty string for the genesis record at seq=1).
- RecordHash: hex-encoded SHA-256 of this record's canonical JSON (with record_hash omitted from the hash input).
- Timestamp: RFC 3339 formatted timestamp of the event.
- EventType: a short string identifying the kind of event.
- DeploymentMode: either "local" or "hosted".
- Actor: the identity that triggered the event.
- Payload: arbitrary structured data associated with the event.
- HostedContext: optional deployment context; must be non-nil when DeploymentMode is "hosted".
func (*AuditRecord) CanonicalMarshal ¶
func (r *AuditRecord) CanonicalMarshal() ([]byte, error)
CanonicalMarshal produces deterministic JSON for hashing. It serializes the record without the RecordHash field, with all map keys sorted lexicographically, and with no extra whitespace.
func (*AuditRecord) ComputeRecordHash ¶
func (r *AuditRecord) ComputeRecordHash() (string, error)
ComputeRecordHash is an exported wrapper for computeRecordHash.
type AuditWriter ¶
type AuditWriter struct {
// contains filtered or unexported fields
}
AuditWriter is a daemon-owned append-only writer for the audit JSONL file. It serializes all writes under a mutex, fsyncs after each line, and maintains a current head anchor (seq + record_hash) for chaining.
The writer is fail-closed: any write or fsync error is propagated to the caller so the guarded operation can be aborted.
func NewAuditWriter ¶
func NewAuditWriter(path string) (*AuditWriter, error)
NewAuditWriter opens or creates the JSONL file at path and reconstructs the head anchor by replaying all existing records. If the file does not exist, it is created. The writer is ready for Append calls immediately.
If the chain is broken (e.g. tampered or corrupted), this function returns an error. Use NewAuditWriterRecoverable if you want automatic recovery (truncate at the last valid record) instead of failing.
func NewAuditWriterRecoverable ¶ added in v0.2.0
func NewAuditWriterRecoverable(path string) (*AuditWriter, error)
NewAuditWriterRecoverable opens the audit file with automatic corruption recovery. If the chain is broken (e.g. from an unclean shutdown mid-write), the writer truncates the file at the last valid record and re-replays. This prevents the daemon from crash-looping forever on a corrupted tail — the valid prefix is preserved, the broken tail is lost.
Use this for daemon startup. Use NewAuditWriter for tests and strict integrity checking (where tampering should be detected, not repaired).
func NewAuditWriterWithCheckpoints ¶
func NewAuditWriterWithCheckpoints(path string, checkpointPath string, cadence int64, keyDER []byte) (*AuditWriter, error)
NewAuditWriterWithCheckpoints opens the audit JSONL at path and a signed checkpoint manager at checkpointPath. cadence is the record interval for automatic checkpoints (values <= 0 use DefaultCheckpointCadence). keyDER is the PKCS#8 ECDSA signing key.
func (*AuditWriter) Append ¶
func (w *AuditWriter) Append(record AuditRecord) error
Append serializes a new record to the JSONL file under a mutex. It sets the record's Seq, PrevHash, and RecordHash fields, writes the canonical JSON line, fsyncs, and updates the head anchor.
The record's Seq is set to the current head seq + 1, and PrevHash is set to the current head hash. The caller-provided fields (Timestamp, EventType, DeploymentMode, Actor, Payload, HostedContext) are preserved.
If the writer has been closed, or if the write or fsync fails, an error is returned and the head anchor is NOT updated (fail-closed).
func (*AuditWriter) CheckpointPublicKey ¶
func (w *AuditWriter) CheckpointPublicKey() (*ecdsa.PublicKey, error)
CheckpointPublicKey returns the ECDSA public key for verifying signed checkpoints.
func (*AuditWriter) CheckpointsPath ¶
func (w *AuditWriter) CheckpointsPath() string
CheckpointsPath returns the checkpoint JSONL path when checkpoints are enabled.
func (*AuditWriter) Close ¶
func (w *AuditWriter) Close() error
Close flushes and closes the underlying file. After Close, the writer must not be used. It is safe to call Close multiple times (the second call is a no-op after the file is closed).
func (*AuditWriter) CurrentHead ¶
func (w *AuditWriter) CurrentHead() (seq int64, hash string)
CurrentHead returns the current head anchor (seq and record_hash) of the audit chain. Returns (0, "") if no records have been written.
type BundleManifest ¶
type BundleManifest struct {
BundleVersion int `json:"bundle_version"`
ExportTimestamp string `json:"export_timestamp"`
AuditRecordCount int64 `json:"audit_record_count"`
AuditHeadSeq int64 `json:"audit_head_seq"`
AuditHeadHash string `json:"audit_head_hash"`
CheckpointCount int `json:"checkpoint_count"`
LatestCpSeq int64 `json:"latest_cp_seq"`
LatestCpHash string `json:"latest_cp_hash"`
PubKeyFingerprint string `json:"pub_key_fingerprint"`
ManifestHash string `json:"manifest_hash"`
Signature []byte `json:"signature"`
}
BundleManifest is the signed metadata for an audit export bundle. It captures the audit chain head, checkpoint state, and public key identity at the time of export, all tied together by a signature from the daemon's audit signing key.
Offline verification of a bundle proves:
- The bundled audit JSONL and checkpoints match the manifest metadata.
- The audit hash chain is internally consistent (no gaps, no tampered records in the chain).
- The checkpoint chain is internally consistent (no missing checkpoints, no broken checkpoint-to-checkpoint links).
- The checkpoint signatures are valid against the bundled public key.
- The manifest is signed by the expected daemon audit key.
It does NOT prove:
- That the bundle was ever published to a global transparency log.
- Global ordering of events across daemon instances.
- That no records were pruned from the audit chain before the oldest checkpoint in the bundle.
func ExportAuditBundle ¶
func ExportAuditBundle(bundleDir string, opt *ExportBundleOptions) (*BundleManifest, error)
ExportAuditBundle creates a portable audit bundle directory containing a copy of the audit JSONL, checkpoints JSONL, the daemon public key (PEM), and a signed manifest that ties them together.
The bundle proves bundle-integrity and audit-chain integrity at export time. It does NOT constitute a global transparency-log anchoring claim. See BundleManifest's doc comment for details on what this verification proves and what it does not.
The audit chain and checkpoint chain are verified before export; if either has integrity issues the export is aborted with an error.
func (*BundleManifest) VerifyManifestSignature ¶
func (m *BundleManifest) VerifyManifestSignature(pub *ecdsa.PublicKey) bool
VerifyManifestSignature checks that the manifest's signature is valid for its ManifestHash using the given ECDSA P-256 public key.
type BundlePaths ¶
type BundlePaths struct {
Dir string
ManifestPath string
AuditPath string
CheckpointsPath string
PubKeyPath string
}
BundlePaths holds the standard file paths within an audit bundle directory.
type CheckpointError ¶
type CheckpointError struct {
Type CheckpointErrorType `json:"type"`
Message string `json:"message"`
Line int `json:"line,omitempty"`
Seq int64 `json:"seq,omitempty"`
}
CheckpointError carries structured information about a verification failure.
func (*CheckpointError) Error ¶
func (e *CheckpointError) Error() string
type CheckpointErrorType ¶
type CheckpointErrorType string
CheckpointErrorType classifies verification failures.
const ( // ErrTypeTamperMiddle means a record in the middle of the chain was modified. ErrTypeTamperMiddle CheckpointErrorType = "tamper_middle" // ErrTypeTailTruncation means records at the tail were removed. ErrTypeTailTruncation CheckpointErrorType = "tail_truncation" // ErrTypeReorder means records were reordered. ErrTypeReorder CheckpointErrorType = "reorder" // ErrTypeMissingCheckpoint means a checkpoint is missing from the chain. ErrTypeMissingCheckpoint CheckpointErrorType = "missing_checkpoint" // ErrTypeSignature means a checkpoint signature is invalid. ErrTypeSignature CheckpointErrorType = "invalid_signature" // ErrTypeCheckpointChain means the checkpoint-to-checkpoint hash chain is broken. ErrTypeCheckpointChain CheckpointErrorType = "broken_checkpoint_chain" )
type CheckpointManager ¶
type CheckpointManager struct {
// contains filtered or unexported fields
}
CheckpointManager creates, stores, and retrieves signed checkpoint records for the audit chain. It supports both fixed-cadence checkpointing (every N records) and on-demand export-time checkpointing.
The manager maintains a separate JSONL file for checkpoints, each signed with the daemon's audit signing key. The checkpoint file path is typically named <audit_path>.checkpoints.
func NewCheckpointManager ¶
func NewCheckpointManager(path string, cadence int64, keyDER []byte) (*CheckpointManager, error)
NewCheckpointManager opens or creates a checkpoint file at the given path. cadence is the number of audit records between automatic checkpoints (0 means no automatic checkpointing). If keyDER is non-nil, it is parsed as a PKCS#8 ECDSA private key for signing.
func (*CheckpointManager) CheckpointSeq ¶
func (m *CheckpointManager) CheckpointSeq() int64
CheckpointSeq returns the current checkpoint sequence number.
func (*CheckpointManager) Close ¶
func (m *CheckpointManager) Close() error
Close closes the underlying checkpoint file.
func (*CheckpointManager) CreateCheckpoint ¶
func (m *CheckpointManager) CreateCheckpoint(auditHeadSeq int64, auditHeadHash string) (*CheckpointRecord, error)
CreateCheckpoint creates a signed checkpoint for the given head anchor (seq and record_hash) and appends it to the checkpoint file. If a signer is configured, the checkpoint is signed; otherwise it is stored unsigned (useful for testing).
Returns the created checkpoint record.
func (*CheckpointManager) LatestAnchor ¶
func (m *CheckpointManager) LatestAnchor() (int64, string)
LatestAnchor returns the latest checkpoint's head anchor (seq and head anchor hash). Returns (0, "") if no checkpoints have been created.
func (*CheckpointManager) ShouldCheckpoint ¶
func (m *CheckpointManager) ShouldCheckpoint(auditHeadSeq int64) bool
ShouldCheckpoint returns true if a checkpoint should be created based on the current audit head seq and the configured cadence.
type CheckpointRecord ¶
type CheckpointRecord struct {
Seq int64 `json:"seq"`
HeadAnchorSeq int64 `json:"head_anchor_seq"`
HeadAnchorHash string `json:"head_anchor_hash"`
PrevCheckpointHash string `json:"prev_checkpoint_hash"`
CheckpointHash string `json:"checkpoint_hash"`
Signature []byte `json:"signature"`
Timestamp string `json:"timestamp"`
}
CheckpointRecord is a signed snapshot of the audit hash chain at a specific sequence number. Each checkpoint captures the head anchor (seq + record_hash) of the audit chain at a point in time, is cryptographically linked to the previous checkpoint, and is signed with the daemon's audit signing key.
The checkpoint chain is independent of the audit record chain but provides a separate signed attestation that can be used to verify the integrity of the audit log at the checkpointed point.
func NewCheckpoint ¶
func NewCheckpoint(seq int64, headAnchorSeq int64, headAnchorHash string, prevCheckpointHash string) *CheckpointRecord
NewCheckpoint creates a new CheckpointRecord for the given head anchor. The checkpoint is assigned the given checkpoint seq number. If prevCheckpointHash is empty, this is treated as the genesis checkpoint. Sign must be called separately to sign the checkpoint.
func (*CheckpointRecord) CanonicalMarshal ¶
func (c *CheckpointRecord) CanonicalMarshal() ([]byte, error)
CanonicalMarshal produces deterministic JSON for hashing and signing. It serializes the checkpoint without the CheckpointHash and Signature fields, with no extra whitespace.
func (*CheckpointRecord) Sign ¶
func (c *CheckpointRecord) Sign(key *ecdsa.PrivateKey) error
Sign signs the checkpoint using the given ECDSA P-256 private key. It sets CheckpointHash to the self-hash and Signature to the ASN.1 DER-encoded signature. The signature is computed over SHA-256(checkpointHash).
func (*CheckpointRecord) VerifySignature ¶
func (c *CheckpointRecord) VerifySignature(pub *ecdsa.PublicKey) bool
VerifySignature checks that the checkpoint's signature is valid for the checkpoint hash using the given ECDSA P-256 public key.
type ExportBundleOptions ¶
type ExportBundleOptions struct {
// AuditPath is the path to the audit JSONL file.
AuditPath string
// CheckpointPath is the path to the checkpoint JSONL file.
CheckpointPath string
// SigningKey is the ECDSA P-256 private key used to sign the manifest.
SigningKey *ecdsa.PrivateKey
// PubKeyDER is the PKIX/SPKI DER-encoded public key corresponding to the
// signing key. This is the key whose fingerprint is recorded in the
// manifest and against which offline verifiers check signatures.
PubKeyDER []byte
}
ExportBundleOptions configures the audit bundle export.
type HostedContext ¶
type HostedContext struct {
TenantID string `json:"tenant_id"`
ProjectID string `json:"project_id"`
Region string `json:"region"`
RuntimeProvider string `json:"runtime_provider"`
}
HostedContext captures optional deployment context for hosted deployments. It is serialized in the JSONL record when DeploymentMode is "hosted".
type SQLiteIndexer ¶
type SQLiteIndexer struct {
// contains filtered or unexported fields
}
SQLiteIndexer manages an SQLite index of audit records that is rebuildable from the JSONL audit file. The index supports fast queries for record lookup, chain inspection, and export.
func NewSQLiteIndexer ¶
func NewSQLiteIndexer(dbPath string) (*SQLiteIndexer, error)
NewSQLiteIndexer opens or creates an SQLite index at the given path. If the index already exists, it is opened without re-importing data. Callers should call Rebuild to ensure the index is current.
func (*SQLiteIndexer) Close ¶
func (ix *SQLiteIndexer) Close() error
Close closes the SQLite database connection.
func (*SQLiteIndexer) QueryByEventType ¶
func (ix *SQLiteIndexer) QueryByEventType(eventType string, limit int) ([]AuditRecord, error)
QueryByEventType retrieves all records matching the given event type, ordered by seq ascending. Limit bounds the result set (0 = no limit).
func (*SQLiteIndexer) QueryBySeq ¶
func (ix *SQLiteIndexer) QueryBySeq(seq int64) (*AuditRecord, error)
QueryBySeq retrieves a single record by sequence number.
func (*SQLiteIndexer) Rebuild ¶
func (ix *SQLiteIndexer) Rebuild(auditPath string) error
Rebuild drops all existing data and re-imports from the JSONL file. The audit chain is read and verified during import; if the chain is broken, the records up to the break point are imported and the error is returned so the caller can decide how to handle it.
func (*SQLiteIndexer) RecordCount ¶
func (ix *SQLiteIndexer) RecordCount() (int, error)
RecordCount returns the number of records in the SQLite index.
type VerificationResult ¶
type VerificationResult struct {
AuditRecordCount int64 `json:"audit_record_count"`
AuditHeadSeq int64 `json:"audit_head_seq"`
AuditHeadHash string `json:"audit_head_hash"`
CheckpointCount int `json:"checkpoint_count"`
LatestAnchorSeq int64 `json:"latest_anchor_seq"`
LatestAnchorHash string `json:"latest_anchor_hash"`
Issues []*CheckpointError `json:"issues,omitempty"`
// Replayed records if the caller wants them (nil if chain is broken).
Records []AuditRecord `json:"-"`
}
VerificationResult holds the outcome of verifying an audit chain against its checkpoints. A result passes when Issues is empty.
func VerifyAuditChain ¶
func VerifyAuditChain(auditPath, checkpointsPath string, pubKey *ecdsa.PublicKey) (*VerificationResult, error)
VerifyAuditChain reads the audit JSONL file and checkpoints JSONL file and verifies the integrity of the audit chain against the latest checkpoint.
If pubKey is non-nil, checkpoint signatures are also verified.
Returns a VerificationResult with Issues populated if any integrity violations are found. An error is returned only for I/O failures.
type VerifyAuditBundleResult ¶
type VerifyAuditBundleResult struct {
Manifest *BundleManifest `json:"manifest,omitempty"`
ManifestValid bool `json:"manifest_valid"`
FingerprintMatch bool `json:"fingerprint_match"`
ChainResult *VerificationResult
}
VerifyAuditBundleResult holds the outcome of verifying a bundle.
func VerifyAuditBundle ¶
func VerifyAuditBundle(bundleDir string, expectedFingerprint string, verifySignature bool) (*VerifyAuditBundleResult, error)
VerifyAuditBundle verifies a previously exported audit bundle directory. It checks:
- The manifest is parseable and self-consistent (ManifestHash matches).
- The manifest signature is valid (if pubKey is non-nil).
- The bundled public key fingerprint matches the manifest's fingerprint.
- The bundled audit chain hash integrity via VerifyAuditChain.
- Checkpoint signatures match the bundled public key.
expectedFingerprint is the hex-encoded SHA-256 of the expected daemon audit public key's PKIX DER bytes. If empty, fingerprint checking is skipped (useful when the fingerprint is verified out-of-band).
The result's ChainResult.Issues contains any audit chain or checkpoint chain integrity violations found.