Help us shape the future of Go! Take the 2026 Go Developer Survey

verify

package
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package verify provides hash chain verification for Chronicle audit streams.

What Verification Does

Chronicle records events in a SHA-256 hash chain: every event's Hash field is computed over its own content plus the previous event's Hash. Verification re-derives each hash from scratch and compares it against the stored value, making two classes of tampering detectable:

  • Gaps: sequence numbers are missing, indicating events were deleted
  • Tampered: a stored hash does not match the recomputed value, indicating the event's content was modified after recording

A chain that passes verification has Valid = true, Gaps = nil, and Tampered = nil.

Erased events (GDPR crypto-erasure) do not break the chain: hashes are computed over event metadata (action, resource, timestamp, etc.), not the encrypted payload, so key destruction does not invalidate any hash.

Retention does delete events, so it needs its own answer. Before a policy purges anything, it appends a retention record to the same stream listing each removed event's sequence, prev_hash and hash (see audit.NewRetentionRecord). Verification reports a missing sequence an authentic record lists as Retained, and links the chain across it from those hashes; anything else missing is still a Gap. A record is authentic when its own digest verifies under the stream's pin, so it is exactly as hard to forge as any other event in the chain.

Types

Input defines the verification request:

  • StreamID: the stream to verify
  • FromSeq: first sequence number to check (0 = genesis)
  • ToSeq: last sequence number to check (0 = the stream head)
  • AppID: required for scope enforcement
  • TenantID: required for scope enforcement
  • HeadSeq, HeadHash: the stream's recorded head, so a truncated tail is detectable rather than silently verifying whatever range is asked for

Report is returned by the verifier:

  • Valid: true if the full range has no gaps, no tampered hashes, no downgrades, and (when checked) the tail matches the recorded head
  • Verified: count of events successfully checked
  • Gaps: sequence numbers that are absent from the store and that no retention record accounts for
  • Retained: ranges of sequence numbers a retention policy removed, each naming the record that lists them; these do not affect Valid
  • Tampered: sequence numbers whose stored hash differs from recomputed
  • FirstEvent: sequence of the first checked event
  • LastEvent: sequence of the last checked event
  • Partial: true when the caller bounded the range rather than verifying genesis to head
  • HeadMatch: whether the verified tail's hash equals the recorded head
  • HeadChecked: whether HeadMatch was actually evaluated. False means no head was supplied, or the range was Partial; read HeadMatch only when this is true, since "false" otherwise means "not checked," not "checked and mismatched"
  • Coverage: the assurance Level over each span of the verified range

Store

Store provides the data access needed for verification:

The composite [store.Store] embeds this interface, so any Chronicle backend satisfies it automatically.

Usage

Verification is typically triggered via the Admin HTTP API (POST /chronicle/verify) or directly through [chronicle.Chronicle.VerifyChain]:

report, err := c.VerifyChain(ctx, &verify.Input{
    StreamID:  streamID,
    AppID:     "myapp",
    TenantID:  "tenant-1",
})
if !report.Valid {
    log.Printf("chain broken: gaps=%v tampered=%v", report.Gaps, report.Tampered)
}

Package verify provides hash chain verification types and logic.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LevelRank

func LevelRank(l Level) int

LevelRank orders the levels so callers can compare assurance.

Types

type CheckpointResult

type CheckpointResult struct {
	ID      string `json:"id"`
	FromSeq uint64 `json:"from_seq"`
	ToSeq   uint64 `json:"to_seq"`

	// SignatureValid is whether the stored payload verifies under the key that
	// signed it. False means the checkpoint was edited after signing.
	SignatureValid bool `json:"signature_valid"`

	// HashMatch is whether the event now at ToSeq still hashes to the recorded
	// ToHash. False means the chain was rewritten after this checkpoint.
	//
	// Read it only when HashChecked is true. CheckpointsInRange overlaps
	// rather than contains, so a covering checkpoint's ToSeq can land outside
	// the range this verification actually fetched; when that happens
	// HashMatch stays at its false zero value, and HashChecked says so, so a
	// caller cannot mistake "not checked" for "checked and mismatched".
	HashMatch bool `json:"hash_match"`

	// HashChecked is whether HashMatch was actually evaluated against a
	// fetched event, as opposed to left at its zero value because ToSeq fell
	// outside the verified range.
	HashChecked bool `json:"hash_checked"`

	// ContinuityOK is whether this checkpoint follows the previous one without
	// a gap. False means a checkpoint was removed.
	//
	// Read it only when ContinuityChecked is true, for the same reason as
	// HashMatch/HashChecked: a checkpoint's immediate predecessor may not be
	// part of what this verification fetched, in which case continuity
	// cannot be determined from what is in hand.
	ContinuityOK bool `json:"continuity_ok"`

	// ContinuityChecked is whether ContinuityOK was actually evaluated. It is
	// true for a stream's first checkpoint (FromSeq 1, empty PrevCheckpoint,
	// which is continuous by definition) and for any checkpoint whose
	// immediate predecessor was also fetched by this verification.
	ContinuityChecked bool `json:"continuity_checked"`

	Note string `json:"note,omitempty"`
}

CheckpointResult is what one covering checkpoint asserted and whether it holds.

type Coverage

type Coverage struct {
	FromSeq uint64 `json:"from_seq"`
	ToSeq   uint64 `json:"to_seq"`
	Level   Level  `json:"level"`
	Note    string `json:"note,omitempty"`
}

Coverage is the assurance level over one span of sequences.

type Input

type Input struct {
	StreamID id.ID
	FromSeq  uint64
	ToSeq    uint64
	AppID    string
	TenantID string

	// Pin is the stream's declared scheme and the sequence it applies from.
	// A zero Pin disables downgrade detection, which is only correct for
	// callers that genuinely have no stream in hand.
	Pin hash.Pin

	// HeadSeq and HeadHash are the stream's recorded head, supplied by the
	// caller because it already holds the stream row. They are what lets
	// verification notice a truncated tail: without them, deleting the newest
	// events passes on every range you think to ask about.
	HeadSeq  uint64
	HeadHash string
}

Input defines the parameters for a chain verification request.

type Level

type Level string

Level grades how much a range's integrity actually rests on. The four are ordered: each implies the ones below it.

A single boolean cannot express this, because assurance varies inside one stream. A chain migrated last month is unkeyed below its pin and keyed above it, and signed only as far as its last checkpoint. Flattening that into a green tick is what makes an auditor stop trusting the tool.

const (
	// LevelUnkeyed means digests are reproducible by anyone who can read the
	// store. Detects corruption, not tampering.
	LevelUnkeyed Level = "unkeyed"

	// LevelKeyed means digests depend on key material the store does not hold.
	LevelKeyed Level = "keyed"

	// LevelSigned means a signed checkpoint covers the range, so a rewrite
	// after the checkpoint was taken is provable.
	LevelSigned Level = "signed"

	// LevelAnchored means a covering checkpoint was also confirmed from an
	// external publisher. Nothing emits this yet; external anchoring is the
	// next piece of work.
	LevelAnchored Level = "anchored"
)

type Report

type Report struct {
	Valid    bool  `json:"valid"`
	Verified int64 `json:"verified"`

	// Gaps are missing sequences that nothing accounts for. An event deleted
	// by anything other than a retention policy lands here.
	Gaps []uint64 `json:"gaps"`

	Tampered   []uint64 `json:"tampered"`
	FirstEvent uint64   `json:"first_event"`
	LastEvent  uint64   `json:"last_event"`

	// Retained are missing sequences a retention policy removed, as ranges,
	// each naming the retention record that lists it. They do not make the
	// chain invalid: the record's own digest vouches for the hashes either
	// side of each one, and the chain is checked to link across them.
	//
	// A removed event's own digest cannot be recomputed once its content is
	// gone, so what these ranges assert is linkage, backed by the record,
	// and never content. A sequence lands here only if an authentic record
	// lists it; anything else missing is in Gaps.
	Retained []RetainedRange `json:"retained,omitempty"`

	// Downgrades are sequences whose event claims a weaker digest scheme than
	// its stream pins at that point. These are tampering, not old events.
	Downgrades []uint64 `json:"downgrades,omitempty"`

	// Tolerant are sequences resolved through the pre-migration fallback,
	// where the scheme was not recorded and had to be guessed.
	Tolerant []uint64 `json:"tolerant,omitempty"`

	// Partial is true when the caller bounded the range rather than verifying
	// genesis to head.
	Partial bool `json:"partial,omitempty"`

	// HeadMatch is whether the last verified hash equals the stream's recorded
	// head hash. False on a truncated chain.
	HeadMatch bool `json:"head_match"`

	// HeadChecked is whether the tail was compared to a recorded head, or
	// the range resolved empty against a stream that claims one.
	//
	// It is false whenever HeadMatch's zero value cannot be trusted: no head
	// was supplied, or the range was Partial and the comparison was skipped.
	// Read HeadMatch only when HeadChecked is true -- otherwise "false" means
	// "not checked", not "checked and mismatched".
	//
	// The one case where true does not mean two hashes were compared is an
	// empty resolved range with a non-zero HeadSeq. There is no tail to
	// compare, and the report is already invalid: a stream that claims a head
	// and produced no events in range cannot have been verified up to it. So
	// HeadChecked true with HeadMatch false says the tail did not reach the
	// claimed head, which is exactly what happened, rather than pretending a
	// comparison ran.
	HeadChecked bool `json:"head_checked"`

	// HeadSeq is the stream's recorded head at verification time.
	HeadSeq uint64 `json:"head_seq"`

	// Coverage grades assurance by span. Read it rather than Valid alone.
	Coverage []Coverage `json:"coverage,omitempty"`

	// Checkpoints is what each covering checkpoint asserted and whether it held.
	Checkpoints []CheckpointResult `json:"checkpoints,omitempty"`

	// CheckpointsChecked is whether this verification consulted a checkpoint
	// store at all. It is false for a verifier built without one, and for a
	// backend that refuses to hold checkpoints.
	//
	// Checkpoints omits itself when empty, so without this an auditor cannot
	// tell "this stream has no checkpoints" from "nothing looked". Those are
	// very different answers to give someone asking whether a log can be
	// trusted.
	CheckpointsChecked bool `json:"checkpoints_checked"`

	// CheckpointHeadOK is whether the stream's latest checkpoint is
	// consistent with the head the caller claimed. False means a signed
	// checkpoint asserts the chain once reached a sequence past that head, so
	// events have been removed from the tail and the head row moved down to
	// hide it.
	//
	// Read it only when CheckpointHeadChecked is true, for the same reason as
	// HeadMatch/HeadChecked.
	CheckpointHeadOK bool `json:"checkpoint_head_ok"`

	// CheckpointHeadChecked is whether that comparison actually ran. It is
	// false when there is no checkpoint store, no checkpoint for this stream,
	// or no checkpoint whose signature still verifies -- all of which are
	// "no opinion", not "consistent".
	CheckpointHeadChecked bool `json:"checkpoint_head_checked"`
}

Report is the result of a hash chain verification.

type RetainedRange

type RetainedRange struct {
	FromSeq uint64 `json:"from_seq"`
	ToSeq   uint64 `json:"to_seq"`

	// RecordSeq is the sequence of the retention record that lists them.
	RecordSeq uint64 `json:"record_seq"`

	// PolicyID is the retention policy the record says it acted under.
	PolicyID string `json:"policy_id,omitempty"`

	// Backfill names the archive the record was recovered from, when the
	// record was backfilled for a purge that predates retention records. It
	// is empty for a record the enforcer wrote at purge time.
	Backfill string `json:"backfill,omitempty"`
}

RetainedRange is a run of sequences that retention removed, all explained by the same retention record.

type Store

type Store interface {
	// EventRange returns events in a sequence range for chain verification.
	EventRange(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]*audit.Event, error)

	// Gaps detects missing sequence numbers in a range.
	Gaps(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]uint64, error)
}

Store provides data access for hash chain verification.

type Verifier

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

Verifier provides hash chain verification services.

func NewVerifier

func NewVerifier(store Store) *Verifier

NewVerifier creates a new Verifier with the given store.

It verifies under a zero-value, unkeyed chain, so it cannot recompute an HMAC digest. Callers that have a keyed chain in hand should use NewVerifierWithChain instead.

func NewVerifierWithChain

func NewVerifierWithChain(store Store, chain *hash.Chain) *Verifier

NewVerifierWithChain creates a Verifier that verifies under a specific chain.

A keyed chain needs its key provider to recompute a digest, so a verifier built with NewVerifier (which uses a plain chain) cannot check HMAC events.

func NewVerifierWithCheckpoints

func NewVerifierWithCheckpoints(store Store, chain *hash.Chain, cps checkpoint.Store, signer checkpoint.Signer) *Verifier

NewVerifierWithCheckpoints creates a Verifier that also checks signed checkpoints covering the range.

Both cps and signer may be nil, in which case verification behaves exactly as it did before checkpoints existed, and coverage tops out at keyed.

func (*Verifier) RetainedEntries

func (v *Verifier) RetainedEntries(ctx context.Context, input *Input) (map[uint64]audit.RetentionEntry, error)

RetainedEntries returns every removed sequence the stream's authentic retention records account for, keyed by sequence, with the hashes either side of each one.

It applies exactly the rules VerifyChain does: a record counts only if its own digest verifies under input.Pin, an entry at or after its record's sequence is dropped, and a sequence two records disagree about is left out. input needs StreamID, AppID, TenantID and Pin.

func (*Verifier) VerifyChain

func (v *Verifier) VerifyChain(ctx context.Context, input *Input) (*Report, error)

VerifyChain verifies the integrity of a hash chain for a stream within a sequence range. It checks that each event's hash correctly links to the previous event's hash.

Each call is one "verify chain" span carrying the verdict and its counts. A chain that fails verification is a finding, not an error, so the span only takes an error status when verification itself could not run.

Jump to

Keyboard shortcuts

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