Documentation
¶
Overview ¶
Package hash provides SHA-256 hash chain computation for audit events.
Index ¶
- func ComputeLegacy(prevHash string, event *audit.Event) string
- func Keyed(s Scheme) bool
- func Rank(s Scheme) int
- func Writable(s Scheme) bool
- type Chain
- func (c *Chain) Compute(ctx context.Context, prevHash string, event *audit.Event) (digest, keyID string, err error)
- func (c *Chain) Scheme() Scheme
- func (c *Chain) Verify(prevHash string, event *audit.Event) booldeprecated
- func (c *Chain) VerifyWithPin(ctx context.Context, prevHash string, event *audit.Event, pin Pin) (Result, error)
- type Pin
- type Result
- type Scheme
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ComputeLegacy ¶
ComputeLegacy reproduces the original hash scheme, which covered only timestamp, action, resource, category, resource ID, outcome, severity and metadata.
It exists so events written before the coverage was extended still verify; without it, upgrading would report every historical event as tampered. Never use it to write a new hash.
Falling back to it does not weaken current events: every later scheme covers its own name, so recomputing a tampered event under any of them yields a different digest than the one stored.
func Keyed ¶
Keyed reports whether a scheme's digest depends on key material the store does not hold. It is the question callers actually mean when they reach for an equality check against SchemeHMAC, and unlike that check it keeps giving the right answer as schemes are added.
func Rank ¶
Rank orders schemes by strength, so a stream pinned to one scheme can detect an event claiming anything weaker. Ranking rather than requiring equality is what lets a stream change scheme more than once without an epoch table.
It is exported because the same ordering decides two different questions, and they must never disagree: verification asks whether an event claims less than its stream pins, and the writer asks whether a newly configured scheme is strong enough to move that pin forward. An unknown or empty scheme ranks below every named one. The ordering puts keying ahead of content framing, and that is deliberate. Ranking the framing fix above SchemeHMAC would make a move from SchemeHMAC to SchemePlainV4 read as strengthening, so reconcileStreamPin would advance the pin while the deployment quietly stopped using its key. Losing the key is the larger loss, so every keyed scheme outranks every unkeyed one and the framing fix breaks the tie within each pair.
func Writable ¶
Writable reports whether NewChain will accept a scheme for writing. The three retired schemes still verify every row ever written under them; none of them produces another.
It exists so callers can order their own startup errors sensibly. A caller that checks "keyed but no key provider" before constructing the chain would otherwise tell an operator on chronicle/v3 to go and configure a key, when the scheme is gone and no key would bring it back.
Types ¶
type Chain ¶
type Chain struct {
// contains filtered or unexported fields
}
Chain computes SHA-256 or HMAC-SHA256 hashes linking events into a chain.
The zero value is a plain, unkeyed chain, which is what this type was before schemes existed and what several call sites still construct.
func NewChain ¶
NewChain returns a Chain writing under the given scheme.
SchemeHMAC needs a provider, because accepting one without is how a deployment ends up believing its chain is keyed while writing plain digests. SchemeLegacy is rejected outright: it exists to verify old rows, and writing a new one would deliberately drop the actor and source address from coverage.
Under SchemeHMAC the active key is resolved here, once, and construction fails if there is not one. A provider that loads cleanly is not the same as a provider that can sign: a keyset whose use is misspelled, or whose only hmac key is active: false, parses and validates perfectly and then has no active key for hmac. Without this check the process boots green and every single Record fails at runtime, which is the worst possible place to find out.
The key resolved here is deliberately discarded. Compute re-resolves per event so a rotation takes effect without a restart of the writer; this is a readiness check, not a cache.
func (*Chain) Compute ¶
func (c *Chain) Compute(ctx context.Context, prevHash string, event *audit.Event) (digest, keyID string, err error)
Compute generates the digest for an event, linking it to the previous hash, and returns the ID of the key used. The key ID is empty for unkeyed schemes.
The digest covers every field that carries accountability: who acted (UserID), from where (IP, UserAgent), under which request and session (RequestID, SessionID), under which tenant (AppID, TenantID), why (Reason), about whom (SubjectID), and the event's position in the stream (Sequence), as well as what happened. The request fields are covered whenever any of them is set; see contentV4.
func (*Chain) Verify
deprecated
Verify reports whether the event's stored Hash matches a recomputation, accepting either the current or the legacy unkeyed scheme.
Deprecated: it cannot see a stream's pin, so it cannot detect a downgrade. Use VerifyWithPin. This remains for callers that have no stream in hand.
func (*Chain) VerifyWithPin ¶
func (c *Chain) VerifyWithPin(ctx context.Context, prevHash string, event *audit.Event, pin Pin) (Result, error)
VerifyWithPin checks an event's stored digest against a recomputation, resolving the scheme from what the event records and cross-checking it against what its stream pins.
Recording the scheme in both places is what makes a downgrade visible. Per-event alone is forgeable, since an attacker relabels the event and recomputes under the weaker scheme. Per-stream alone is forgeable too, since one row update covers the whole chain. Together, an event claiming less than its stream promises at that sequence is evidence rather than history.
type Pin ¶
Pin is a stream's declared scheme and the sequence from which it applies. Events below Since predate the pin and are resolved tolerantly.
type Result ¶
type Result struct {
// OK is whether the stored digest matched a recomputation.
OK bool
// Scheme is the scheme the digest was actually verified under.
Scheme Scheme
// Downgrade is true when the event claims a weaker scheme than its stream
// pins at that sequence. Treat it as tampering, not as an old event.
Downgrade bool
// Tolerant is true when the event carried no scheme and was resolved by
// trying the historical schemes in turn.
Tolerant bool
}
Result reports what a verification concluded.
type Scheme ¶
type Scheme string
Scheme names how a digest was produced. It is recorded on every event so verification never has to guess, and so a weaker scheme cannot be substituted for a stronger one without the substitution being visible.
const ( // SchemeLegacy is the original digest, covering neither the actor nor the // source address. Verify-only; never write one. SchemeLegacy Scheme = "chronicle/v1" // SchemePlain is the unkeyed SHA-256 digest over delimiter-joined content. // // Verify-only; never write one. Its content encoding is ambiguous, which // lets an attacker move content across a field boundary without changing // the digest. See contentV4. Use SchemePlainV4 instead. SchemePlain Scheme = "chronicle/v2" // SchemeHMAC is HMAC-SHA256 over the same delimiter-joined content, under a // key the store does not hold. // // Verify-only; never write one. The key does not close the encoding hole: // the MAC covers the same ambiguous bytes, so the swap described in // contentV4 works without ever resolving the key. Use SchemeHMACV5. SchemeHMAC Scheme = "chronicle/v3" // SchemePlainV4 is the unkeyed SHA-256 digest over length-prefixed content. // Unkeyed still means reproducible by anyone who can write to the store, so // it detects corruption and not tampering. SchemePlainV4 Scheme = "chronicle/v4" // SchemeHMACV5 is HMAC-SHA256 over that same length-prefixed content, under // a key the store does not hold. This is the strongest digest on offer. SchemeHMACV5 Scheme = "chronicle/v5" )