audit

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 25 Imported by: 0

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:

  1. The bundled audit chain is internally consistent (hash chain intact).
  2. The bundled checkpoint chain is internally consistent.
  3. Checkpoint signatures are valid against the bundled public key.
  4. 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

View Source
const DefaultCheckpointCadence int64 = 100

DefaultCheckpointCadence is the number of audit records between automatic checkpoints.

View Source
const EventTypeAgentForked = "agent_forked"
View Source
const EventTypeImmutableViolation = "immutable_violation"
View Source
const EventTypeInstallAliasChanged = "install_alias_changed"
View Source
const EventTypeInstallCredentialMapped = "install_credential_mapped"
View Source
const EventTypeInstallDowngradeAllowed = "install_downgrade_allowed"
View Source
const EventTypeInstallPolicyApproved = "install_policy_approved"

Install consent event type constants (B23).

View Source
const EventTypeInstallRemoved = "install_removed"
View Source
const EventTypeMCPToolCall = "mcp_tool_call"
View Source
const EventTypeMCPToolDenied = "mcp_tool_denied"
View Source
const EventTypePublisherIdentityCreated = "publisher_identity_created"

Publisher identity event type constants.

View Source
const EventTypePublisherIdentityExported = "publisher_identity_exported"
View Source
const EventTypePublisherIdentityImported = "publisher_identity_imported"
View Source
const EventTypePublisherIdentityRotated = "publisher_identity_rotated"
View Source
const EventTypePublisherKeyConflict = "publisher_key_conflict"
View Source
const EventTypePublisherRemoved = "publisher_removed"
View Source
const EventTypePublisherTrusted = "publisher_trusted"

Trust store event type constants.

View Source
const EventTypeSecretInjected = "secret_injected"
View Source
const EventTypeSecretLeased = "secret_leased"
View Source
const EventTypeSecretRead = "secret_read"

Variables

This section is empty.

Functions

func GenerateCheckpointKey

func GenerateCheckpointKey() (privateKeyDER []byte, publicKey *ecdsa.PublicKey, err error)

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

func PubKeyFingerprint(pubKeyDER []byte) string

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

func PublicKeyFromCheckpointKeyDER(keyDER []byte) (*ecdsa.PublicKey, error)

PublicKeyFromCheckpointKeyDER parses a PKCS#8 ECDSA private key DER blob and returns its public key.

func RebuildSQLiteIndex

func RebuildSQLiteIndex(auditPath, dbPath string) error

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:

  1. The bundled audit JSONL and checkpoints match the manifest metadata.
  2. The audit hash chain is internally consistent (no gaps, no tampered records in the chain).
  3. The checkpoint chain is internally consistent (no missing checkpoints, no broken checkpoint-to-checkpoint links).
  4. The checkpoint signatures are valid against the bundled public key.
  5. 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:

  1. The manifest is parseable and self-consistent (ManifestHash matches).
  2. The manifest signature is valid (if pubKey is non-nil).
  3. The bundled public key fingerprint matches the manifest's fingerprint.
  4. The bundled audit chain hash integrity via VerifyAuditChain.
  5. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL