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
- Variables
- func CompareEvents(a, b *Event) int
- func FieldNames() []string
- func VerifyInclusion(leaf Hash, index, size uint64, proof []Hash, root Hash) error
- type Actor
- type ActorRef
- type Authority
- type BagRef
- type CaseRef
- type ChainStart
- type DisclosedEvent
- type DisclosedField
- type Ed25519Signer
- type Ed25519Verifier
- type Event
- type EventReport
- type ExecRef
- type Export
- type ExportEvent
- type ExportWindow
- type FieldError
- type Hash
- type HoldAction
- type HoldCoverage
- type HoldStatement
- type IdentityMap
- type ObjectRef
- type Operation
- type Parties
- type Report
- type Resource
- type RootStatement
- type Salt
- type Signed
- type Signer
- type StmtHMAC
- type StoredEvent
- type TombstoneStatement
- type Tree
- type Verifier
- type VerifyError
- type VerifyOptions
- type WindowID
- type WindowReport
Constants ¶
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.
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.
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.
const WindowDuration = 5 * time.Minute
WindowDuration is the length of a sealing window (permission model §19.4).
Variables ¶
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 ¶
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.
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.
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 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.
type Ed25519Verifier ¶
Ed25519Verifier trusts the listed public keys by key id.
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 ¶
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 ¶
Hash is a SHA-256 digest. It marshals as lowercase hex.
func InclusionProof ¶
InclusionProof returns the audit path of leaves[index]. To prove many leaves of one window, build a Tree once instead.
func LeafOf ¶
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 ¶
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 (Hash) MarshalText ¶
MarshalText implements encoding.TextMarshaler.
func (*Hash) UnmarshalText ¶
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.
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 ¶
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.
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 ¶
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.
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 (Signed[S]) Verify ¶
Verify validates the statement and checks its signature. A signature failure wraps ErrBadSignature (and ErrUnknownKey when the key is not trusted).
type Signer ¶
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 StoredEvent ¶
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 ¶
NewTree builds the tree over a copy of leaves, in canonical order (OrderLeaves).
type Verifier ¶
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 (WindowID) Closed ¶
Closed reports whether the sealer may seal w at time now: the window has ended and SealGrace has passed.
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.