Documentation
¶
Overview ¶
Package hashchain implements an append-only, tamper-evident log in which every entry commits to the hash of the entry before it.
A Chain computes and verifies hashes; a Store persists entries. The package ships an in-memory Store (NewMemoryStore) and a Postgres Store in the pgstore subpackage. Entries are hashed over a canonical, versioned binary encoding described in Codec and in the README.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/Aquilabot/hashchain"
)
func main() {
ctx := context.Background()
chain, err := hashchain.New(hashchain.NewMemoryStore(), hashchain.Config{
Key: []byte("load-me-from-a-secret-store"), // optional: keyed (HMAC) chain
Name: "billing-audit",
})
if err != nil {
panic(err)
}
e, err := chain.Append(ctx, hashchain.Record{Kind: "invoice.paid", Payload: []byte(`{"invoice":"inv_1"}`)})
if err != nil {
panic(err)
}
fmt.Println("appended seq", e.Seq)
report, err := chain.Verify(ctx)
var broken *hashchain.VerifyError
switch {
case errors.As(err, &broken):
fmt.Println("tampered at seq", broken.Seq, broken.Reason)
case err != nil:
panic(err)
default:
fmt.Println("verified", report.Entries, "entries; head seq", report.Last.Seq)
}
}
Output: appended seq 1 verified 2 entries; head seq 1
Index ¶
- Constants
- Variables
- func AppendV1(dst []byte, e *Entry) ([]byte, error)
- type Chain
- func (c *Chain) Append(ctx context.Context, rec Record) (Entry, error)
- func (c *Chain) AppendBatch(ctx context.Context, recs ...Record) ([]Entry, error)
- func (c *Chain) Head(ctx context.Context) (Checkpoint, error)
- func (c *Chain) Verify(ctx context.Context) (Report, error)
- func (c *Chain) VerifyFrom(ctx context.Context, cp Checkpoint) (Report, error)
- func (c *Chain) VerifyRange(ctx context.Context, from, to uint64) (Report, error)
- type Checkpoint
- type Codec
- type Config
- type Entry
- type HashMatcher
- type MemoryStore
- type Record
- type Report
- type Store
- type V1Codec
- type VerifyError
Examples ¶
Constants ¶
const GenesisKind = "hashchain.genesis"
GenesisKind is the Kind of the genesis entry written at Seq 0.
const V1 uint8 = 1
V1 is the version number of the canonical binary encoding.
Variables ¶
var ( ErrKindTooLong = errors.New("hashchain: kind longer than 2^32-1 bytes") ErrPayloadTooLong = errors.New("hashchain: payload longer than 2^32-1 bytes") ErrPrevHashTooLong = errors.New("hashchain: prev hash longer than 255 bytes") ErrMalformed = errors.New("hashchain: malformed v1 encoding") )
Encoding errors.
var ( // ErrHashMismatch: the stored hash does not match the recomputed hash. // The entry's content or its stored hash was modified. ErrHashMismatch = errors.New("hash mismatch") // ErrBrokenLink: the entry's PrevHash differs from the previous entry's // Hash. An entry was inserted, replaced or rehashed before this one. ErrBrokenLink = errors.New("prev hash does not match previous entry") // ErrSequence: sequence numbers are not contiguous (deleted, inserted or // reordered entries). ErrSequence = errors.New("sequence gap or reorder") // ErrGenesis: the first entry is not a valid genesis for this chain's // configuration (wrong hash function, key, or missing genesis). ErrGenesis = errors.New("invalid genesis entry") // ErrTruncated: entries that must exist (up to the requested end, or // the trusted checkpoint) are missing. ErrTruncated = errors.New("chain truncated") // ErrCheckpointMismatch: the stored entry at a trusted checkpoint has a // different hash than the checkpoint. ErrCheckpointMismatch = errors.New("checkpoint hash mismatch") // ErrUnknownVersion: no Codec is registered for the entry's version. ErrUnknownVersion = errors.New("unknown encoding version") // ErrEncoding: the entry could not be encoded (codec error). ErrEncoding = errors.New("entry cannot be encoded") )
Reasons a verification can fail. A *VerifyError wraps exactly one of them, so callers can use errors.Is(err, hashchain.ErrHashMismatch) and so on.
Functions ¶
Types ¶
type Chain ¶
type Chain struct {
// contains filtered or unexported fields
}
Chain appends and verifies entries of one hash chain stored in a Store. A Chain is safe for concurrent use; fork-freedom across processes is the Store's responsibility (see Store.Append).
func (*Chain) AppendBatch ¶
AppendBatch adds records atomically, in order, under a single store transaction. It is much cheaper per record than repeated Append calls on stores with per-transaction overhead.
func (*Chain) Head ¶
func (c *Chain) Head(ctx context.Context) (Checkpoint, error)
Head returns the checkpoint of the last entry without verifying anything. The zero Checkpoint means the chain is empty.
func (*Chain) Verify ¶
Verify checks every entry from the start of the chain to the head that exists when Verify starts. It streams entries from the store and keeps O(1) memory.
Verify cannot detect that entries were removed from the end of the chain, or that the whole chain was rewritten; compare the returned Last checkpoint with an externally anchored one, or use VerifyFrom.
func (*Chain) VerifyFrom ¶
VerifyFrom verifies the chain from a trusted checkpoint (for example one previously published to an external anchor) up to the current head. It fails with ErrCheckpointMismatch if the stored entry at cp.Seq does not have cp.Hash, and with ErrTruncated if the chain no longer reaches cp.Seq. The zero Checkpoint verifies the whole chain.
func (*Chain) VerifyRange ¶
VerifyRange checks entries from..to (inclusive). Entry from-1, if any, is used as the trusted predecessor: its hash is not recomputed but entry from must link to it. When from is the first sequence number of the chain the genesis rules are checked instead. Missing entries up to to are reported as ErrTruncated.
type Checkpoint ¶
Checkpoint identifies a chain state by the sequence number and hash of its last entry. Publishing checkpoints outside the store (external anchoring) is what lets a verifier detect a rewritten or truncated chain.
func (Checkpoint) IsZero ¶
func (c Checkpoint) IsZero() bool
IsZero reports whether c is the empty checkpoint (no entries).
type Codec ¶
type Codec interface {
// Version is the Entry.Version this codec handles.
Version() uint8
// AppendPreimage appends the canonical encoding of e to dst. It must not
// read e.Hash. The result must be a pure function of the other fields.
AppendPreimage(dst []byte, e *Entry) ([]byte, error)
}
Codec produces the byte string ("preimage") that is hashed for an entry. Each codec owns one Entry.Version value, so a chain can hold entries written with different encodings and still verify all of them.
type Config ¶
type Config struct {
// Hash is the digest function. Zero means crypto.SHA256. The function
// must be linked into the binary (import its package, e.g. crypto/sha512).
Hash crypto.Hash
// Key, when non-empty, turns the chain into a keyed chain: every digest
// is HMAC(Key, preimage) instead of Hash(preimage). Without the key
// nobody can forge or verify entries.
Key []byte
// Codec encodes new entries. Zero means V1Codec.
Codec Codec
// ExtraCodecs are additional codecs accepted during verification, for
// chains that contain entries from older encodings.
ExtraCodecs []Codec
// NoGenesis disables the genesis entry: the first application entry
// gets Seq 1 and an empty PrevHash. Use it only for chains created
// without one (for example, pre-existing legacy chains); a genesis
// entry binds the chain to its hash configuration and Name.
NoGenesis bool
// Name is committed to by the genesis entry. Two chains with different
// names cannot have their entries spliced into each other even when
// they share a key.
Name string
// Now returns the current time. Zero means time.Now.
Now func() time.Time
}
Config configures a Chain. The zero value is valid: SHA-256, unkeyed, v1 encoding, with a genesis entry.
type Entry ¶
type Entry struct {
// Seq is the position of the entry in the chain. The genesis entry has
// Seq 0 and the first application entry has Seq 1. Sequence numbers are
// contiguous.
Seq uint64
// Version selects the Codec used to hash the entry.
Version uint8
// Time is the append time, in UTC, truncated to whole microseconds.
Time time.Time
// Kind is an application-defined label, for example "user.login". It must
// be valid UTF-8, non-empty and free of NUL bytes.
Kind string
// Payload is opaque application data. The chain never interprets it.
Payload []byte
// PrevHash is the Hash of the entry at Seq-1, or empty for the first
// entry of the chain.
PrevHash []byte
// Hash is the digest of the entry's canonical encoding.
Hash []byte
}
Entry is one record of a chain as it is persisted.
type HashMatcher ¶
HashMatcher is an optional interface for codecs whose stored form cannot always reproduce the exact preimage, for example a legacy format that hashed nanosecond timestamps but persisted microseconds. When a codec implements it, verification calls MatchHash instead of comparing sum(AppendPreimage(e)) with e.Hash.
sum hashes a preimage with the chain's configured hash (and key) and returns a digest that is only valid until the next call.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is a Store backed by a slice. It is safe for concurrent use and is intended for tests, examples and short-lived processes.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty MemoryStore.
type Record ¶
type Record struct {
// Kind is required; see Entry.Kind.
Kind string
// Payload is optional opaque data.
Payload []byte
// Time is optional. The zero value means "now" (Config.Now). It is
// converted to UTC and truncated to microseconds before hashing.
Time time.Time
}
Record is the input to Chain.Append.
type Report ¶
type Report struct {
// Entries is the number of entries whose hash was recomputed.
Entries uint64
// First and Last are the first and last verified entries. Both are zero
// when nothing was verified.
First Checkpoint
Last Checkpoint
}
Report summarizes a successful verification.
type Store ¶
type Store interface {
// Append reads the current head (nil when the chain is empty), calls
// build with it while holding exclusive write access to the chain, and
// atomically persists the returned entries. The head passed to build is
// guaranteed to carry Seq and Hash; other fields may be empty. If build
// returns an error nothing is written and the error is returned.
Append(ctx context.Context, build func(head *Entry) ([]Entry, error)) error
// Head returns the last entry, or (nil, nil) when the chain is empty.
Head(ctx context.Context) (*Entry, error)
// Scan calls fn for every entry with from <= Seq <= to, in ascending Seq
// order, stopping at the first error. Implementations should stream
// rather than load the whole range. The *Entry and its slices are only
// valid during the call; fn must copy anything it keeps.
Scan(ctx context.Context, from, to uint64, fn func(*Entry) error) error
}
Store persists the entries of one chain.
Implementations must make Append atomic and mutually exclusive: while one Append runs build and writes its result, no other Append on the same chain (in this process or any other) may observe the old head. That is what keeps the chain from forking under concurrent writers.
type V1Codec ¶
type V1Codec struct{}
V1Codec is the canonical length-prefixed binary encoding (version 1):
"hashchain" 0x00 10 bytes, domain separation tag version 1 byte, 0x01 seq 8 bytes, uint64 big-endian time 8 bytes, int64 big-endian, Unix microseconds (UTC) len(kind) 4 bytes, uint32 big-endian kind len(kind) bytes, UTF-8 len(payload) 4 bytes, uint32 big-endian payload len(payload) bytes len(prev_hash) 1 byte prev_hash len(prev_hash) bytes
Every field has exactly one representation, so equal entries always encode to equal bytes and DecodeV1 accepts only canonical input.
func (V1Codec) AppendPreimage ¶
AppendPreimage implements Codec.
type VerifyError ¶
type VerifyError struct {
// Seq is the sequence number at which the chain breaks: the entry whose
// check failed, or the expected position of a missing entry.
Seq uint64
// Reason is one of the Err* sentinels above.
Reason error
// Expected and Actual hold the compared values for hash and link
// failures (digests), and are nil otherwise.
Expected, Actual []byte
// Detail is a human-readable explanation.
Detail string
// Err is an underlying error, for example from a Codec.
Err error
}
VerifyError points at the first entry where verification failed.
func (*VerifyError) Error ¶
func (e *VerifyError) Error() string
func (*VerifyError) Unwrap ¶
func (e *VerifyError) Unwrap() []error
Unwrap lets errors.Is match both the Reason sentinel and Err.