integrity

package
v0.143.1 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package integrity is the cryptographic core of DataTug Perimeter audit (permission model §19.4, architecture §12, API contract §3.4). It is pure: no storage, no network, no clock. Every time it uses is passed in.

What it provides

  • Event leaves. An Event is encoded field by field in a fixed, versioned order (EncodingVersion, FieldNames). Each field gets a salted commitment: HMAC-SHA-256 keyed by a per-field salt derived from the event's random Salt. The leaf is a hash over all commitments, so a field can be redacted by keeping only its commitment (DisclosedEvent.Redact) and the leaf still verifies. Disclosing one field reveals only that field's salt, never the event salt, so redacted values stay hidden. ExpireAt (the Firestore TTL field) is not part of an event.
  • Stored events and erasure. A StoredEvent is what the store keeps: the event, its salt and the commitments of erased fields. StoredEvent.Erase drops a stored value (for example a free-text reason naming a person) and keeps its commitment, so StoredEvent.Leaf and StoredEvent.Disclose still produce the sealed leaf.
  • Leaf order. Firestore has no insertion order, so the order is defined here: OrderLeaves sorts a window's events by CompareEvents (at, then id; the leaf hash breaks a full tie). Sealer and exporter both call it, so leaf positions agree.
  • Windows. Events fall into 5-minute windows per Space and project (WindowID) by arrival time. A window's Merkle tree (Tree, RFC 9162 shape) is built over its ordered leaves; an empty window has the root of the empty tree. The window root (RootStatement.Root) commits to the window identity, the previous window's root, the Merkle root, the leaf count and the retention expiry, so the windows form a chain and each window is signed once. A window is sealed only once it is WindowID.Closed: its end plus SealGrace, so an event stamped at the last instant but committed late still lands before sealing.
  • Signed statements. Signer and Verifier abstract the signing key (production: a Cloud KMS key held by the platform operator; tests and local tools: Ed25519Signer, Ed25519Verifier). Sign produces a Signed statement for window roots (RootStatement, which carries the expiry, so a later absence is pre-explained by the same signature), retention tombstones (TombstoneStatement, never dated before the signed expiry) and legal-hold place/release records (HoldStatement; a release names two distinct people and only lifts a placement of the same hold and range made no later than the release).
  • Verification. VerifyExport checks an Export: signatures, the chain of previous-root links, contiguity of the claimed window range, optional independent anchors (roots in VerifyOptions.Anchors, holds in VerifyOptions.AnchoredHolds), tombstones against legal holds, and every included event's commitments, inclusion proof, canonical order and revealed location (window, Space, project). The Report says exactly what is proven: every included event is authentic; a window is complete when every event (leaf) is included, and disclosed when no included event hides a field. Hidden fields are listed as "redacted" or "erased" by the exporter's unauthenticated label, so neither label changes a verdict. Hold coverage is independent only when the anchored hold set was supplied. Failures are a *VerifyError naming the first broken window, matching one sentinel error per tamper class (see errors.go).
  • Pseudonyms. NewActorRef makes random pseudonymous actor ids; IdentityMap is the reference (in-memory) shape of the erasable pseudonym → user map; VerifyErasure checks that an export still verifies and shows no trace of an erased user id.

How the other layers call it

  • Backend writer (request path): pick WindowFor(space, project, arrival time), map each person to an ActorRef through the identity map, draw a NewSalt, and insert the event with its salt. Nothing is hashed or signed on the request path.
  • Erasure: delete the identity-map entry; for free text that names the person, call StoredEvent.Erase on the stored record and write back the event and its Erased commitments.
  • Sealer task (Cloud Tasks, sealing service account): for each unsealed window that is WindowID.Closed, in order, read its stored events, call OrderLeaves, Seal with the previous root and the retention expiry (at least RetentionFloor after the window ends), Sign the RootStatement with the KMS-backed Signer (one signature per window) and write it to the anchor bucket. Setting expireAt on the events is a separate write (see the spec note on Firestore access). A retention deletion is recorded by a signed TombstoneStatement carrying the window's seal; a legal hold by signed [HoldStatement]s in the anchor bucket.
  • Exporter (audit/exports route): call OrderLeaves on the window's stored events, build one NewTree, StoredEvent.Disclose the selected events, apply reader redactions with DisclosedEvent.Redact, attach [Tree.Proof]s and the signed statements as an Export (JSON tags are stable).
  • CLI verifier (`datatug access audit verify <export>`): decode the Export, build an Ed25519Verifier (or a KMS public-key verifier) from the operator's public key, pass roots and the complete hold set read from the anchor bucket in VerifyOptions, and print the Report.

Future option: signed checkpoints

An export carries every window root in its range: about 105,000 per project per year, mostly empty windows. A later version could sign a checkpoint every N windows (a Merkle root over those window roots), so an export carries the checkpoints plus only the windows that hold included events, with a proof of each window root in its checkpoint. Not implemented; it would add a statement kind, not change the ones here.

Residual risks

  • Integrity starts at sealing (permission model §19.4). This package cannot detect changes made to a window before it was sealed.
  • A tombstone's DeletedAt is asserted by the operator's signer. Firestore TTL purges up to about a day after expireAt, so a hold placed after expiry but before the actual purge can be bypassed by backdating DeletedAt to the expiry. Closing this needs an independent deletion time, such as the anchor bucket's object creation time for the tombstone, compared against the holds.
  • A real-key re-signing of the last window, the first window's link, and hold coverage are checked independently only when the caller passes anchored roots and holds (VerifyOptions).

Index

Constants

View Source
const EncodingVersion uint16 = 1

EncodingVersion is the version of the field set and value encoding below. Any change to either needs a new version; leaves commit to it.

View Source
const RetentionFloor = 90 * 24 * time.Hour

RetentionFloor is the shortest retention a Space may configure (permission model §19.4: a 90-day floor). Seal refuses anything shorter, so a misconfigured sealer fails before it signs; verifiers pass it as VerifyOptions.MinRetention.

View Source
const SealGrace = time.Minute

SealGrace is how long after a window's end the sealer waits before it treats the window as closed. An event stamped at the window's last instant can commit later (commit latency, clock skew between instances); once a window is sealed it never reopens, so an event committed after sealing would be outside every root. Writers place an event by its arrival time, so the grace must exceed the longest commit latency plus clock skew.

View Source
const WindowDuration = 5 * time.Minute

WindowDuration is the length of a sealing window (permission model §19.4).

Variables

View Source
var (
	// ErrInvalidArgument reports a caller error (nil input, empty id).
	ErrInvalidArgument = errors.New("audit integrity: invalid argument")
	// ErrInvalidWindow reports a malformed window identity.
	ErrInvalidWindow = errors.New("audit integrity: invalid window")
	// ErrInvalidStatement reports a statement that fails its own rules.
	ErrInvalidStatement = errors.New("audit integrity: invalid statement")
	// ErrMalformedHash reports a hash that is not 64 hex characters.
	ErrMalformedHash = errors.New("audit integrity: malformed hash")

	// ErrUnsupportedVersion reports an event encoding version this package does not know.
	ErrUnsupportedVersion = errors.New("audit integrity: unsupported encoding version")
	// ErrMalformedEvent reports a disclosed event whose field set, order or
	// value encoding is not canonical.
	ErrMalformedEvent = errors.New("audit integrity: malformed event")
	// ErrUnknownField reports a field name outside the schema.
	ErrUnknownField = errors.New("audit integrity: unknown field")

	// ErrBadSignature reports a statement whose signature does not verify
	// (tampered statement, or a root re-signed with another key).
	ErrBadSignature = errors.New("audit integrity: bad signature")
	// ErrUnknownKey reports a signature by a key the verifier does not trust.
	ErrUnknownKey = errors.New("audit integrity: unknown signing key")
	// ErrFieldTampered reports a disclosed field whose value does not match its commitment.
	ErrFieldTampered = errors.New("audit integrity: field does not match its commitment")
	// ErrInclusionProof reports an event whose leaf is not in the window root.
	ErrInclusionProof = errors.New("audit integrity: inclusion proof does not verify")
	// ErrLeafReordered reports an event that is authentic but sits at another
	// position than the one it claims.
	ErrLeafReordered = errors.New("audit integrity: leaf reordered")
	// ErrLeafOrder reports events not listed in strictly ascending insertion order.
	ErrLeafOrder = errors.New("audit integrity: events not in insertion order")
	// ErrLeafIndex reports an event index outside its window.
	ErrLeafIndex = errors.New("audit integrity: leaf index out of range")
	// ErrEventMisplaced reports an event whose revealed window, Space,
	// project or time contradicts the window it is proven in.
	ErrEventMisplaced = errors.New("audit integrity: event names another window")
	// ErrIncompleteWindow reports a window exported as whole that lacks leaves
	// (for example a truncated window).
	ErrIncompleteWindow = errors.New("audit integrity: whole window is missing events")
	// ErrChainBroken reports a window whose previous-root link does not match.
	ErrChainBroken = errors.New("audit integrity: chain broken")
	// ErrMissingWindow reports a window missing from a claimed-contiguous range.
	ErrMissingWindow = errors.New("audit integrity: window missing from range")
	// ErrWindowSequence reports windows out of order, duplicated, outside the
	// range, or for another Space or project.
	ErrWindowSequence = errors.New("audit integrity: window sequence invalid")
	// ErrMissingSeal reports a window entry with neither a signed root nor a tombstone.
	ErrMissingSeal = errors.New("audit integrity: window has no signed root or tombstone")
	// ErrAnchorMismatch reports a root that differs from an independently anchored copy.
	ErrAnchorMismatch = errors.New("audit integrity: root differs from anchored root")
	// ErrStatementMismatch reports statements that do not fit together: a
	// window with both a root and a tombstone, a hold release that matches no
	// earlier placement of the same hold and range, or an export hold missing
	// from the anchored holds.
	ErrStatementMismatch = errors.New("audit integrity: statements disagree")
	// ErrForgedTombstone reports a tombstone that does not stand: bad
	// signature, deletion before the signed expiry or under a legal hold, or
	// events still present. The cause is attached.
	ErrForgedTombstone = errors.New("audit integrity: tombstone does not verify")
	// ErrRetentionTooShort reports a seal whose expiry is below the policy
	// floor given in [VerifyOptions.MinRetention].
	ErrRetentionTooShort = errors.New("audit integrity: retention shorter than policy allows")
	// ErrTombstoneBeforeExpiry reports a tombstone dated before the window expired.
	ErrTombstoneBeforeExpiry = errors.New("audit integrity: deleted before expiry")
	// ErrDeletedUnderHold reports a tombstone dated while a legal hold covered the window.
	ErrDeletedUnderHold = errors.New("audit integrity: deleted under legal hold")
	// ErrTombstoneHasEvents reports a tombstoned window exported with events.
	ErrTombstoneHasEvents = errors.New("audit integrity: tombstoned window has events")
	// ErrHoldReleaseNeedsTwo reports a hold release that names fewer than two distinct people.
	ErrHoldReleaseNeedsTwo = errors.New("audit integrity: hold release needs two people")
	// ErrIdentityLeak reports an erased user id still present in an export.
	ErrIdentityLeak = errors.New("audit integrity: erased identity present")
	// ErrPseudonymCollision reports repeated pseudonym collisions (a broken random source).
	ErrPseudonymCollision = errors.New("audit integrity: pseudonym collision")
)

Sentinel errors. Each tamper class has its own sentinel; a *VerifyError matches it with errors.Is.

Functions

func CompareEvents

func CompareEvents(a, b *Event) int

CompareEvents is the canonical leaf order: by At, then by ID.

func FieldNames

func FieldNames() []string

FieldNames returns the committed field names of EncodingVersion, in commitment order.

func VerifyInclusion

func VerifyInclusion(leaf Hash, index, size uint64, proof []Hash, root Hash) error

VerifyInclusion checks that leaf sits at index in the tree of size leaves whose root is root (RFC 9162 §2.1.3.2).

Types

type Actor

type Actor struct {
	Ref              ActorRef `json:"ref"`
	Kind             string   `json:"kind"` // user | agent | service
	CredentialFamily string   `json:"credentialFamily"`
	ClientID         string   `json:"clientId"`
	OnBehalfOf       ActorRef `json:"onBehalfOf,omitempty"`
}

Actor is who acted.

type ActorRef

type ActorRef string

ActorRef is a Space-scoped pseudonymous person id ("act-…"). It is random, never derived from the user id, and resolvable only through the erasable identity map.

func NewActorRef

func NewActorRef(r io.Reader) (ActorRef, error)

NewActorRef draws a 128-bit random pseudonym from r, or from crypto/rand when r is nil.

type Authority

type Authority struct {
	Grants             []string `json:"grants,omitempty"`
	Decisions          []string `json:"decisions,omitempty"`
	Standing           []string `json:"standing,omitempty"`
	Roles              []string `json:"roles,omitempty"`
	TeamBasis          []string `json:"teamBasis,omitempty"`
	PolicyFingerprint  string   `json:"policyFingerprint,omitempty"`
	ProtectionRevision *int64   `json:"protectionRevision,omitempty"`
}

Authority is the resolution the boundary used.

type BagRef

type BagRef struct {
	Session  string `json:"session"`
	Bag      string `json:"bag"`
	Revision int64  `json:"revision"`
}

BagRef identifies the Context Bag revision a scope came from.

type CaseRef

type CaseRef struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
}

CaseRef links an incident or other case.

type ChainStart

type ChainStart string

ChainStart says what the first window's previous-root link was checked against.

const (
	// ChainGenesis: the first window is the first ever sealed (zero previous root).
	ChainGenesis ChainStart = "genesis"
	// ChainAnchored: the previous root matched an anchored root.
	ChainAnchored ChainStart = "anchored"
	// ChainUnverified: nothing to check the first link against.
	ChainUnverified ChainStart = "unverified"
)

Chain starts.

type DisclosedEvent

type DisclosedEvent struct {
	Version uint16           `json:"v"`
	Fields  []DisclosedField `json:"fields"`
}

DisclosedEvent is an event as exported: every field carries its commitment; revealed fields also carry the field salt and canonical value.

func Disclose

func Disclose(s Salt, e *Event) DisclosedEvent

Disclose returns e with every field revealed (no erased field). Use StoredEvent.Disclose once a field has been erased in the store.

func (DisclosedEvent) Erased

func (d DisclosedEvent) Erased() []string

Erased lists the fields whose value the store dropped, in schema order.

func (DisclosedEvent) Event

func (d DisclosedEvent) Event() (Event, error)

Event decodes the revealed fields of d after the same checks as DisclosedEvent.Leaf. Redacted fields are left at their zero value; use DisclosedEvent.Redacted to tell them from absent ones.

func (DisclosedEvent) Leaf

func (d DisclosedEvent) Leaf() (Hash, error)

Leaf checks d (version, field set and order, canonical values, every revealed value against its commitment) and returns its Merkle leaf. A failure is a *FieldError wrapping ErrUnsupportedVersion, ErrMalformedEvent or ErrFieldTampered.

func (DisclosedEvent) Redact

func (d DisclosedEvent) Redact(names ...string) (DisclosedEvent, error)

Redact returns a copy of d with the named fields replaced by their commitments. The leaf is unchanged. An unknown name is an error.

func (DisclosedEvent) Redacted

func (d DisclosedEvent) Redacted() []string

Redacted lists the fields hidden from this reader (not revealed and not erased), in schema order.

type DisclosedField

type DisclosedField struct {
	Name       string `json:"name"`
	Commitment Hash   `json:"commitment"`
	Salt       *Hash  `json:"salt,omitempty"`
	Value      []byte `json:"value,omitempty"`
	Erased     bool   `json:"erased,omitempty"`
}

DisclosedField is one committed field. Salt is nil when the field is not revealed; Value is then empty. Erased marks a field whose value the store dropped (StoredEvent.Erase) as opposed to one hidden from this reader. The label is the exporter's and is not authenticated: nothing signs it, so the verifier treats an erased field like any hidden one. It is only valid on an unrevealed field other than "id" and "at".

func (DisclosedField) Revealed

func (f DisclosedField) Revealed() bool

Revealed reports whether the field's value is disclosed.

type Ed25519Signer

type Ed25519Signer struct {
	// contains filtered or unexported fields
}

Ed25519Signer signs with an Ed25519 private key (tests, local tools).

func NewEd25519Signer

func NewEd25519Signer(keyID string, key ed25519.PrivateKey) *Ed25519Signer

NewEd25519Signer returns a signer for key under keyID.

func (*Ed25519Signer) KeyID

func (s *Ed25519Signer) KeyID() string

KeyID implements Signer.

func (*Ed25519Signer) Sign

func (s *Ed25519Signer) Sign(ctx context.Context, message []byte) ([]byte, error)

Sign implements Signer.

type Ed25519Verifier

type Ed25519Verifier map[string]ed25519.PublicKey

Ed25519Verifier trusts the listed public keys by key id.

func (Ed25519Verifier) Verify

func (v Ed25519Verifier) Verify(keyID string, message, signature []byte) error

Verify implements Verifier.

type Event

type Event struct {
	ID      string    `json:"id"`
	At      time.Time `json:"at"`
	Kind    string    `json:"kind"`              // access | activity
	Action  string    `json:"action"`            // access.approved | operation.authorized | ...
	Tier    string    `json:"tier"`              // server-enforced | client-reported | advisory
	Outcome string    `json:"outcome,omitempty"` // allow | deny | error (activity only)
	Reason  string    `json:"reason,omitempty"`  // fixed code on deny/error, or free text on access events

	Actor     Actor     `json:"actor"`
	Access    Parties   `json:"access"`
	Resource  Resource  `json:"resource"`
	Operation Operation `json:"operation"`
	Authority Authority `json:"authority"`
	CaseRef   *CaseRef  `json:"caseRef,omitempty"`
	Execution *ExecRef  `json:"executionRef,omitempty"`
	Statement *StmtHMAC `json:"statementDigest,omitempty"`
	Window    string    `json:"window"`               // WindowID.Key of the arrival window
	Occurred  time.Time `json:"occurredAt,omitempty"` // original time of a late event
}

Event is one audit event in the shape of permission model §19.2. Every person-valued field holds an ActorRef, never a real user id.

ExpireAt (the Firestore TTL field) is deliberately absent: it is set at sealing and is not committed. Optional numbers are pointers so that zero and absent stay distinct.

type EventReport

type EventReport struct {
	Index uint64 `json:"index"`
	Leaf  Hash   `json:"leaf"`
	// Disclosed is true when every field of the event is revealed.
	Disclosed bool `json:"disclosed"`
	// Redacted and Erased are the hidden fields as the exporter labels them:
	// hidden from this reader, or dropped by the store (§19.4 erasure). The
	// labels are not authenticated; either way the field is proven only by
	// its commitment.
	Redacted []string `json:"redacted,omitempty"`
	Erased   []string `json:"erased,omitempty"`
}

EventReport is one authentic event.

type ExecRef

type ExecRef struct {
	StoreID string `json:"storeId"`
	Project string `json:"project"`
	ID      string `json:"id"`
}

ExecRef links an execution record.

type Export

type Export struct {
	Space   string                  `json:"space"`
	Project string                  `json:"project"`
	From    time.Time               `json:"from"` // start of the first window
	To      time.Time               `json:"to"`   // start of the last window
	Windows []ExportWindow          `json:"windows"`
	Holds   []Signed[HoldStatement] `json:"holds,omitempty"`
}

Export is what `POST audit/exports` produces and `datatug access audit verify` consumes: a claimed-contiguous range of windows of one Space and project, their signed statements, and a subset of their events with inclusion proofs.

type ExportEvent

type ExportEvent struct {
	Index uint64         `json:"index"`
	Proof []Hash         `json:"proof"`
	Event DisclosedEvent `json:"event"`
}

ExportEvent is one included event at its leaf index, with its proof.

type ExportWindow

type ExportWindow struct {
	Root      *Signed[RootStatement]      `json:"root,omitempty"`
	Tombstone *Signed[TombstoneStatement] `json:"tombstone,omitempty"`
	Whole     bool                        `json:"whole,omitempty"`
	Events    []ExportEvent               `json:"events,omitempty"`
}

ExportWindow is one window of an export. It carries exactly one of a signed root or a tombstone (which embeds the signed seal). Whole claims that every event of the window is included; the verifier fails the export if that is not so.

type FieldError

type FieldError struct {
	Field string
	Err   error
}

FieldError names the event field a failure belongs to.

func (*FieldError) Error

func (e *FieldError) Error() string

func (*FieldError) Unwrap

func (e *FieldError) Unwrap() error

Unwrap returns the underlying error.

type Hash

type Hash [sha256.Size]byte

Hash is a SHA-256 digest. It marshals as lowercase hex.

func InclusionProof

func InclusionProof(leaves []Hash, index uint64) ([]Hash, error)

InclusionProof returns the audit path of leaves[index]. To prove many leaves of one window, build a Tree once instead.

func LeafOf

func LeafOf(s Salt, e *Event) Hash

LeafOf returns the Merkle leaf of event e under salt s, with no erased field. Use StoredEvent.Leaf once a field has been erased in the store.

func MerkleRoot

func MerkleRoot(leaves []Hash) Hash

MerkleRoot returns the root over leaves in canonical order.

func OrderLeaves

func OrderLeaves(events []StoredEvent) ([]Hash, error)

OrderLeaves puts a window's events into canonical leaf order, in place, and returns their leaves in that order. Firestore has no insertion order, so the sealer and the exporter both call it on the window's events to agree on leaf positions: events sort by CompareEvents (At, then the server-assigned ID); a full tie, which unique IDs rule out, falls back to the leaf hash so the order is total.

func ParseHash

func ParseHash(s string) (Hash, error)

ParseHash parses the canonical lowercase hex form of a hash.

func (Hash) IsZero

func (h Hash) IsZero() bool

IsZero reports whether h is all zero bytes (the genesis previous root).

func (Hash) MarshalText

func (h Hash) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Hash) String

func (h Hash) String() string

String returns the lowercase hex form.

func (*Hash) UnmarshalText

func (h *Hash) UnmarshalText(b []byte) error

UnmarshalText implements encoding.TextUnmarshaler. Only the canonical lowercase 64-character hex form is accepted.

type HoldAction

type HoldAction string

HoldAction is what a HoldStatement records.

const (
	HoldPlace   HoldAction = "place"
	HoldRelease HoldAction = "release"
)

Hold actions.

type HoldCoverage

type HoldCoverage string

HoldCoverage says where the legal holds used to judge tombstones came from.

const (
	// HoldsFromAnchor: the complete hold set came from the independent anchor.
	HoldsFromAnchor HoldCoverage = "anchored"
	// HoldsFromExportOnly: only holds the exporter chose to include were
	// seen; a deletion under an omitted hold would not be detected.
	HoldsFromExportOnly HoldCoverage = "export-only"
)

Hold coverages.

type HoldStatement

type HoldStatement struct {
	HoldID  string     `json:"holdId"`
	Space   string     `json:"space"`
	Project string     `json:"project"`
	From    time.Time  `json:"from"`
	To      time.Time  `json:"to"`
	Action  HoldAction `json:"action"`
	At      time.Time  `json:"at"`
	Actors  []ActorRef `json:"actors"`
}

HoldStatement records placing or releasing a legal hold on the windows starting From through To. Actors are the pseudonymous people who placed or released it; a release names at least two distinct people. Who may do so is decided by the authorization layer, not here.

type IdentityMap

type IdentityMap struct {
	// contains filtered or unexported fields
}

IdentityMap is the reference, in-memory shape of the erasable pseudonym ↔ user map (production: spaces/{s}/ext/datatug/audit/_identity/actors/{ref}). It is safe for concurrent use.

func NewIdentityMap

func NewIdentityMap(r io.Reader) *IdentityMap

NewIdentityMap returns an empty map drawing pseudonyms from r (nil: crypto/rand).

func (*IdentityMap) Erase

func (m *IdentityMap) Erase(space, userID string) (ActorRef, bool)

Erase deletes the user's entry in space and returns the pseudonym that no longer resolves. Events and roots are untouched.

func (*IdentityMap) Pseudonym

func (m *IdentityMap) Pseudonym(space, userID string) (ActorRef, error)

Pseudonym returns the user's pseudonym in space, creating one on first use.

func (*IdentityMap) Resolve

func (m *IdentityMap) Resolve(space string, ref ActorRef) (string, bool)

Resolve returns the user behind ref in space, if not erased.

type ObjectRef

type ObjectRef struct {
	Namespace string `json:"namespace"`
	Name      string `json:"name"`
}

ObjectRef names one object (table, view, collection).

type Operation

type Operation struct {
	Op            string `json:"op,omitempty"`
	CorrelationID string `json:"correlationId,omitempty"`
	RecordCount   *int64 `json:"recordCount,omitempty"`
	Pages         *int64 `json:"pages,omitempty"`
	// Denials is the coalesced count of an operation.denied event.
	Denials *int64 `json:"denials,omitempty"`
}

Operation describes an activity event.

type Parties

type Parties struct {
	Requester   ActorRef `json:"requester,omitempty"`
	Approver    ActorRef `json:"approver,omitempty"`
	Beneficiary ActorRef `json:"beneficiary,omitempty"`
}

Parties are the people named by an access-control event (§19.4 erasure).

type Report

type Report struct {
	Space      string         `json:"space"`
	Project    string         `json:"project"`
	From       time.Time      `json:"from"`
	To         time.Time      `json:"to"`
	ChainStart ChainStart     `json:"chainStart"`
	Holds      HoldCoverage   `json:"holds"`
	Windows    []WindowReport `json:"windows"`
}

Report is what a successful verification proves.

func VerifyErasure

func VerifyErasure(exp *Export, opts VerifyOptions, erasedUserID string) (*Report, error)

VerifyErasure verifies exp and then checks that the erased user id appears in none of its revealed values. Events name people only by pseudonymous ActorRef, and erased free text is replaced by its commitment (StoredEvent.Erase), so deleting the identity-map entry leaves every event and root verifiable. A revealed value that contains the id is ErrIdentityLeak.

The id matches as a run of whole tokens, case-folded, where tokens are runs of letters and digits ("Firebase-UID-Alice" matches "firebase uid alice"; "al" does not match "approval"). This is a check, not a proof of absence: it cannot see the id inside a longer token, in another encoding (base64, hashes, percent-encoding), in redacted values, or in events the export does not include.

func VerifyExport

func VerifyExport(exp *Export, opts VerifyOptions) (*Report, error)

VerifyExport verifies exp and reports what it proves. Windows are checked in order; the first failure is returned as a *VerifyError naming the window.

func (*Report) Complete

func (r *Report) Complete() bool

Complete reports whether the export proves completeness of the whole range: every window is complete or tombstoned (its absence pre-explained). It says nothing about hidden fields; see Report.Disclosed.

func (*Report) Disclosed

func (r *Report) Disclosed() bool

Disclosed reports whether every included event of the range reveals every field.

func (*Report) Events

func (r *Report) Events() int

Events is the number of events proven authentic.

type Resource

type Resource struct {
	Space       string      `json:"space"`
	Project     string      `json:"project"`
	Environment string      `json:"environment,omitempty"`
	Connection  string      `json:"connection,omitempty"`
	Fingerprint string      `json:"fingerprint,omitempty"`
	Objects     []ObjectRef `json:"objects,omitempty"`
	Fields      []string    `json:"fields,omitempty"`
	Bag         *BagRef     `json:"bag,omitempty"`
}

Resource is what the event touched: references and names, never values.

type RootStatement

type RootStatement struct {
	Window     WindowID  `json:"window"`
	PrevRoot   Hash      `json:"prevRoot"` // zero for the first window ever sealed
	MerkleRoot Hash      `json:"merkleRoot"`
	LeafCount  uint64    `json:"leafCount"`
	ExpiresAt  time.Time `json:"expiresAt"` // the window's events expire (TTL) at this time
}

RootStatement is a sealed window: what the sealer signs, once per window, and anchors. It includes the retention expiry decided at sealing, so a later absence of the window's events is pre-explained (§19.4) by the same signature.

func Seal

func Seal(w WindowID, prev Hash, leaves []Hash, expiresAt time.Time) (RootStatement, error)

Seal builds the root statement of window w over leaves in canonical order (OrderLeaves), chained to prev (the previous window's RootStatement.Root, or zero) and expiring at expiresAt, which must be at least RetentionFloor after the window ends (ErrRetentionTooShort).

func (RootStatement) Root

func (s RootStatement) Root() Hash

Root is the window root: a hash over the window identity, the previous root, the Merkle root, the leaf count and the expiry. The next window commits to it.

type Salt

type Salt [32]byte

Salt is an event's random secret. It is stored with the event and never exported; exports carry per-field salts derived from it.

func NewSalt

func NewSalt(r io.Reader) (Salt, error)

NewSalt draws a salt from r, or from crypto/rand when r is nil.

type Signed

type Signed[S statement] struct {
	Statement S      `json:"statement"`
	KeyID     string `json:"keyId"`
	Signature []byte `json:"signature"`
}

Signed is a statement with its signature.

func Sign

func Sign[S statement](ctx context.Context, signer Signer, s S) (Signed[S], error)

Sign validates s and signs it with signer.

func (Signed[S]) Verify

func (x Signed[S]) Verify(v Verifier) error

Verify validates the statement and checks its signature. A signature failure wraps ErrBadSignature (and ErrUnknownKey when the key is not trusted).

type Signer

type Signer interface {
	KeyID() string
	Sign(ctx context.Context, message []byte) ([]byte, error)
}

Signer signs statement messages. Production: a Cloud KMS asymmetric key held by the platform operator and usable only by the sealing service account. KeyID names the key version so verifiers can pick the public key.

type StmtHMAC

type StmtHMAC struct {
	KeyVersion int64  `json:"keyVersion"`
	HMAC       string `json:"hmac"`
}

StmtHMAC is a keyed statement digest.

type StoredEvent

type StoredEvent struct {
	Salt   Salt
	Event  Event
	Erased map[string]Hash
}

StoredEvent is an event as the audit store keeps it: the event, its salt and, for every field erased after insertion, the commitment that replaces the dropped value (permission model §19.4: "an erasure can replace the stored reason with its commitment without breaking verification").

The store persists Erased next to the event. Leaf and Disclose use the stored commitment for an erased field, so the leaf, the window root and every later export still verify.

The dropped value cannot be recovered from the store or from any export. Residual risk: whoever holds the store's salt can still test guesses against the commitment (a dictionary attack on short or predictable text). Exports never carry the event salt, so readers of an export cannot.

func (*StoredEvent) Disclose

func (x *StoredEvent) Disclose() (DisclosedEvent, error)

Disclose returns the event with every field revealed except erased ones, which carry only their stored commitment.

func (*StoredEvent) Erase

func (x *StoredEvent) Erase(fields ...string) error

Erase drops the named fields' values from the stored event, keeping their commitments in Erased. Call it on the stored record (an erasure request); it is idempotent. "id" and "at" fix the leaf order and cannot be erased. An unknown or protected name is an error and changes nothing.

func (*StoredEvent) Leaf

func (x *StoredEvent) Leaf() (Hash, error)

Leaf returns the event's Merkle leaf, using stored commitments for erased fields.

type TombstoneStatement

type TombstoneStatement struct {
	Seal      RootStatement `json:"seal"`
	DeletedAt time.Time     `json:"deletedAt"`
}

TombstoneStatement records that a window's events were deleted by retention. It carries the window's seal, including the expiry signed at sealing, so the chain still verifies across the deleted window and the deletion can be checked against the expiry.

type Tree

type Tree struct {
	// contains filtered or unexported fields
}

Tree is a window's Merkle tree, built once: its root and every inclusion proof are then served without rehashing (O(n) to build, O(log n) a proof).

func NewTree

func NewTree(leaves []Hash) *Tree

NewTree builds the tree over a copy of leaves, in canonical order (OrderLeaves).

func (*Tree) Proof

func (t *Tree) Proof(index uint64) ([]Hash, error)

Proof returns the inclusion proof of the leaf at index.

func (*Tree) Root

func (t *Tree) Root() Hash

Root is the tree's root; the empty tree has SHA-256("").

func (*Tree) Size

func (t *Tree) Size() uint64

Size is the number of leaves.

type Verifier

type Verifier interface {
	Verify(keyID string, message, signature []byte) error
}

Verifier checks a signature made by the key named keyID. It returns an error wrapping ErrUnknownKey for an untrusted key.

type VerifyError

type VerifyError struct {
	// Window is the first broken window.
	Window WindowID
	// Event is the position of the failing event in the export window, or -1.
	Event int
	// Field is the failing field, if any.
	Field string
	// Err is the tamper-class sentinel.
	Err error
	// Cause is the underlying failure, if different from Err.
	Cause error
}

VerifyError is the error VerifyExport returns. It names the first broken window and, where it applies, the event (position in the export window) and field.

func (*VerifyError) Error

func (e *VerifyError) Error() string

func (*VerifyError) Unwrap

func (e *VerifyError) Unwrap() []error

Unwrap exposes both the sentinel and the cause to errors.Is and errors.As.

type VerifyOptions

type VerifyOptions struct {
	// Verifier holds the operator's public key(s). Required.
	Verifier Verifier
	// Anchors are window roots from an independent record (the anchor bucket,
	// or the Space's own copy), by [WindowID.Key]. A root that differs is
	// [ErrAnchorMismatch]; an anchor for the window before the range pins the
	// first previous-root link.
	Anchors map[string]Hash
	// AnchoredHolds are hold statements read from the anchor bucket, not from
	// the export. They are merged with the export's own; statements for
	// another Space or project are ignored.
	AnchoredHolds []Signed[HoldStatement]
	// HoldsAnchored declares that AnchoredHolds is the complete set of holds
	// for this Space and project in the anchor bucket. Tombstones are then
	// judged on the anchored holds only, every hold in the export must also
	// be in the anchor, and the report says hold coverage was independent.
	HoldsAnchored bool
	// MinRetention is the shortest retention policy allows (normally
	// [RetentionFloor]). A seal expiring earlier than its window's end plus MinRetention
	// is [ErrRetentionTooShort]. Zero skips the check.
	MinRetention time.Duration
}

VerifyOptions configure VerifyExport.

type WindowID

type WindowID struct {
	Space   string    `json:"space"`
	Project string    `json:"project"`
	Start   time.Time `json:"start"`
}

WindowID identifies one 5-minute window of one Space and project. Space-level events use an empty Project.

func WindowFor

func WindowFor(space, project string, t time.Time) WindowID

WindowFor returns the window an event arriving at t falls into.

func (WindowID) Closed

func (w WindowID) Closed(now time.Time) bool

Closed reports whether the sealer may seal w at time now: the window has ended and SealGrace has passed.

func (WindowID) End

func (w WindowID) End() time.Time

End is the instant the window ends (exclusive).

func (WindowID) Equal

func (w WindowID) Equal(o WindowID) bool

Equal reports whether both identify the same window.

func (WindowID) Key

func (w WindowID) Key() string

Key is the delimiter-safe window key used as the store document id and in the event's window field, e.g. "space=s1|project=shop|2026-11-03T21:05".

func (WindowID) Next

func (w WindowID) Next() WindowID

Next is the following window.

func (WindowID) Prev

func (w WindowID) Prev() WindowID

Prev is the preceding window.

func (WindowID) Validate

func (w WindowID) Validate() error

Validate checks that the window has a Space and a 5-minute-aligned start.

type WindowReport

type WindowReport struct {
	Window     WindowID  `json:"window"`
	Root       Hash      `json:"root"`
	LeafCount  uint64    `json:"leafCount"`
	ExpiresAt  time.Time `json:"expiresAt"`
	Tombstoned bool      `json:"tombstoned,omitempty"`
	// Complete is true when every event (leaf) of the window is included,
	// whatever is hidden inside them. Otherwise only the included events are
	// proven authentic; nothing is proven about omitted ones.
	Complete bool `json:"complete"`
	// Disclosed is true when no included event hides a field, whether the
	// exporter labels it redacted or erased.
	Disclosed bool          `json:"disclosed"`
	Events    []EventReport `json:"events,omitempty"`
}

WindowReport is the verdict for one window.

Jump to

Keyboard shortcuts

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