diff --git a/keccak.go b/keccak.go index bc3ae7a..62c88e1 100644 --- a/keccak.go +++ b/keccak.go @@ -9,7 +9,14 @@ package keccak //go:generate go run gen_keccakf_bmi2.go //go:generate go run github.com/klauspost/asmfmt/cmd/asmfmt@v1.3.2 -w keccakf_amd64_bmi2.s -import "hash" +import ( + "encoding" + "errors" + "hash" + "slices" + + "golang.org/x/crypto/sha3" +) // KeccakState wraps the keccak hasher. In addition to the usual hash methods, it also supports // Read to get a variable amount of data from the hash state. Read is faster than Sum @@ -21,10 +28,148 @@ type KeccakState interface { const rate = 136 // sponge rate for Keccak-256: (1600 - 2*256) / 8 -var _ KeccakState = (*Hasher)(nil) +var ( + _ KeccakState = (*Hasher)(nil) + _ encoding.BinaryMarshaler = (*Hasher)(nil) + _ encoding.BinaryAppender = (*Hasher)(nil) + _ encoding.BinaryUnmarshaler = (*Hasher)(nil) +) // NewFastKeccak returns a new Keccak-256 hasher. The zero value of Hasher is // equally usable and avoids the allocation. func NewFastKeccak() *Hasher { return &Hasher{} } + +// Canonical marshaled-state format, identical no matter which implementation +// (native assembly or the x/crypto/sha3 fallback) produced it — the active +// implementation is a runtime CPU-feature decision, so the encoding must not +// depend on it: +// +// magic (4) || state (200) || flags (1) || pos (1) +// +// state always has any partially absorbed input already XORed in; flags bit 0 +// is the sponge direction (0 = absorbing, 1 = squeezing); pos is the byte +// position within the current block (absorb offset or squeeze read offset). +const ( + marshalMagic = "fk1\x01" + marshaledSize = len(marshalMagic) + 200 + 1 + 1 +) + +var errInvalidState = errors.New("keccak: invalid hash state") + +// appendState appends the canonical encoding of a sponge state. +func appendState(b []byte, state *[200]byte, squeezing bool, pos int) []byte { + b = slices.Grow(b, marshaledSize) + b = append(b, marshalMagic...) + b = append(b, state[:]...) + var flags byte + if squeezing { + flags = 1 + } + return append(b, flags, byte(pos)) +} + +// parseState validates and decodes a canonical encoding. +// +// pos == rate is valid only while squeezing: the x/crypto implementation +// permutes lazily on the next Read, so a state squeezed to an exact block +// boundary persists with pos == rate. The native decoder normalizes it by +// permuting eagerly. +func parseState(data []byte) (state [200]byte, squeezing bool, pos int, err error) { + if len(data) != marshaledSize || string(data[:len(marshalMagic)]) != marshalMagic { + return state, false, 0, errInvalidState + } + payload := data[len(marshalMagic):] + flags, posByte := payload[200], int(payload[201]) + squeezing = flags == 1 + if flags > 1 || posByte > rate || (!squeezing && posByte == rate) { + return state, false, 0, errInvalidState + } + copy(state[:], payload[:200]) + return state, squeezing, posByte, nil +} + +// x/crypto/sha3's own stable marshal layout for legacy Keccak +// (sha3/legacy_hash.go): magic || rate || state || n || direction. +const ( + xcMagic = "sha\x0b" + xcSize = len(xcMagic) + 1 + 200 + 1 + 1 +) + +var errXCFormat = errors.New("keccak: unexpected x/crypto/sha3 state format") + +// xcAppendState re-encodes a wrapped x/crypto/sha3 hasher's state in the +// canonical format. x/crypto keeps partial input XORed into the state with n +// as the block offset, which matches the canonical layout directly. +func xcAppendState(b []byte, xc KeccakState) ([]byte, error) { + // On error return b unchanged, per the encoding.BinaryAppender contract, + // so a caller's existing prefix is not dropped. + m, ok := xc.(encoding.BinaryMarshaler) + if !ok { + return b, errXCFormat + } + enc, err := m.MarshalBinary() + if err != nil { + return b, err + } + if len(enc) != xcSize || string(enc[:len(xcMagic)]) != xcMagic || int(enc[4]) != rate { + return b, errXCFormat + } + var state [200]byte + copy(state[:], enc[5:205]) + return appendState(b, &state, enc[206] == 1, int(enc[205])), nil +} + +// xcFromState builds a wrapped x/crypto/sha3 hasher holding the given +// canonical sponge state. +func xcFromState(state *[200]byte, squeezing bool, pos int) (KeccakState, error) { + // Mirror the layout xcAppendState decodes: magic || rate || state || n + // || direction. blob does not escape (x/crypto reads it and copies), + // so this is stack-resident. + var blob [xcSize]byte + copy(blob[:], xcMagic) + blob[4] = byte(rate) + copy(blob[5:205], state[:]) + blob[205] = byte(pos) + if squeezing { + blob[206] = 1 + } + + st, ok := sha3.NewLegacyKeccak256().(KeccakState) + if !ok { + return nil, errXCFormat + } + u, ok := st.(encoding.BinaryUnmarshaler) + if !ok { + return nil, errXCFormat + } + if err := u.UnmarshalBinary(blob[:]); err != nil { + return nil, err + } + return st, nil +} + +// cloneXC deep-copies a wrapped x/crypto/sha3 hasher via a marshal round-trip. +func cloneXC(xc KeccakState) (KeccakState, error) { + m, ok := xc.(encoding.BinaryMarshaler) + if !ok { + return nil, errXCFormat + } + enc, err := m.MarshalBinary() + if err != nil { + return nil, err + } + st, ok := sha3.NewLegacyKeccak256().(KeccakState) + if !ok { + return nil, errXCFormat + } + u, ok := st.(encoding.BinaryUnmarshaler) + if !ok { + return nil, errXCFormat + } + if err := u.UnmarshalBinary(enc); err != nil { + return nil, err + } + return st, nil +} diff --git a/keccak_asm.go b/keccak_asm.go index 37ce284..a69248e 100644 --- a/keccak_asm.go +++ b/keccak_asm.go @@ -111,6 +111,7 @@ func (s *sponge) padAndSqueeze() { s.state[s.absorbed] ^= 0x01 s.state[rate-1] ^= 0x80 keccakF1600(&s.state) + s.absorbed = 0 s.squeezing = true s.readIdx = 0 } @@ -221,6 +222,82 @@ func (h *Hasher) Read(out []byte) (int, error) { return h.sponge.Read(out) } +// Clone returns a copy of the hasher in its current state. The copy and the +// original evolve independently. +func (h *Hasher) Clone() (*Hasher, error) { + if useASM { + return &Hasher{sponge: h.sponge}, nil + } + if h.xc == nil { + return &Hasher{}, nil + } + xc, err := cloneXC(h.xc) + if err != nil { + return nil, err + } + return &Hasher{xc: xc}, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. The encoding is +// canonical: the same logical state marshals to the same bytes regardless of +// platform or active implementation, and can be restored anywhere. +func (h *Hasher) MarshalBinary() ([]byte, error) { + return h.AppendBinary(make([]byte, 0, marshaledSize)) +} + +// AppendBinary implements encoding.BinaryAppender. +func (h *Hasher) AppendBinary(b []byte) ([]byte, error) { + if !useASM { + if h.xc == nil { + // A zero-value hasher is the canonical zero state; avoid + // allocating (and retaining) a fallback state just to encode it. + var zero [200]byte + return appendState(b, &zero, false, 0), nil + } + return xcAppendState(b, h.xc) + } + s := &h.sponge + if s.squeezing { + return appendState(b, &s.state, true, s.readIdx), nil + } + // Canonical form keeps partial input XORed into the state. + state := s.state + xorIn(&state, s.buf[:s.absorbed]) + return appendState(b, &state, false, s.absorbed), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (h *Hasher) UnmarshalBinary(data []byte) error { + state, squeezing, pos, err := parseState(data) + if err != nil { + return err + } + if !useASM { + xc, err := xcFromState(&state, squeezing, pos) + if err != nil { + return err + } + h.xc = xc + return nil + } + // Partial input is already XORed into state, so buf stays zero: absorbing + // XORs buf into state at finalization, and XORing zeroes is a no-op. + s := sponge{state: state} + if squeezing { + if pos == rate { + // Block-boundary state from the lazy fallback implementation; + // the native sponge permutes eagerly. + keccakF1600(&s.state) + pos = 0 + } + s.squeezing, s.readIdx = true, pos + } else { + s.absorbed = pos + } + h.sponge = s + return nil +} + // xorIn XORs data into the first len(data) bytes of state using uint64 loads. func xorIn(state *[200]byte, data []byte) { for i := 0; i+8 <= len(data); i += 8 { diff --git a/keccak_default.go b/keccak_default.go index 2e8eee3..ef62c1d 100644 --- a/keccak_default.go +++ b/keccak_default.go @@ -69,3 +69,48 @@ func (h *Hasher) Read(out []byte) (int, error) { h.init() return h.h.Read(out) } + +// Clone returns a copy of the hasher in its current state. The copy and the +// original evolve independently. +func (h *Hasher) Clone() (*Hasher, error) { + if h.h == nil { + return &Hasher{}, nil + } + c, err := cloneXC(h.h) + if err != nil { + return nil, err + } + return &Hasher{h: c}, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. The encoding is +// canonical: the same logical state marshals to the same bytes regardless of +// platform or active implementation, and can be restored anywhere. +func (h *Hasher) MarshalBinary() ([]byte, error) { + return h.AppendBinary(make([]byte, 0, marshaledSize)) +} + +// AppendBinary implements encoding.BinaryAppender. +func (h *Hasher) AppendBinary(b []byte) ([]byte, error) { + if h.h == nil { + // A zero-value hasher is the canonical zero state; avoid allocating + // (and retaining) a backing state just to encode it. + var zero [200]byte + return appendState(b, &zero, false, 0), nil + } + return xcAppendState(b, h.h) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (h *Hasher) UnmarshalBinary(data []byte) error { + state, squeezing, pos, err := parseState(data) + if err != nil { + return err + } + st, err := xcFromState(&state, squeezing, pos) + if err != nil { + return err + } + h.h = st + return nil +} diff --git a/keccak_fallback_test.go b/keccak_fallback_test.go new file mode 100644 index 0000000..f976d41 --- /dev/null +++ b/keccak_fallback_test.go @@ -0,0 +1,247 @@ +//go:build (amd64 || arm64) && !purego + +package keccak + +import ( + "bytes" + "slices" + "testing" +) + +// forceFallback routes the package through the x/crypto fallback arms of +// keccak_asm.go for the duration of a test. Those arms are compiled into +// every binary and taken at runtime on CPUs without BMI2 / SHA3 extensions +// (old Xeons, Graviton 2, ...), but no CI platform selects them naturally — +// purego builds compile keccak_default.go instead, which is different code. +// Tests using this must not call t.Parallel. +func forceFallback(t *testing.T) { + t.Helper() + old := useASM + useASM = false + t.Cleanup(func() { useASM = old }) +} + +// asFallback runs fn with the package routed through the x/crypto fallback, +// restoring whichever implementation was selected before. Unlike +// forceFallback it is scoped to a single call rather than a whole test, so +// one test can cross between implementations. t.Fatalf inside fn still runs +// the restore, since Goexit unwinds defers. +func asFallback(fn func()) { + old := useASM + useASM = false + defer func() { useASM = old }() + fn() +} + +var fallbackLens = []int{0, 1, 32, 135, 136, 137, 271, 272, 300} + +func fallbackData(n int) []byte { + b := make([]byte, n) + for i := range b { + b[i] = byte(i*17 + n) + } + return b +} + +// TestFallbackMatchesNative captures digests and squeeze streams from the +// active implementation, then re-runs everything through the fallback arms. +func TestFallbackMatchesNative(t *testing.T) { + type captured struct { + data []byte + digest [32]byte + stream []byte + } + var want []captured + for _, n := range fallbackLens { + data := fallbackData(n) + digest := Sum256(data) + var h Hasher + h.Write(data) + if got := h.Sum(nil); !bytes.Equal(got, digest[:]) { + t.Fatalf("len %d: native Sum = %x, want %x", n, got, digest) + } + stream := make([]byte, 300) + h.Read(stream) + want = append(want, captured{data, digest, stream}) + } + + forceFallback(t) + + for _, w := range want { + if got := Sum256(w.data); got != w.digest { + t.Fatalf("len %d: fallback Sum256 = %x, want %x", len(w.data), got, w.digest) + } + + // Streaming in uneven chunks, then non-destructive Sum/Sum256. + var h Hasher + for rest := w.data; len(rest) > 0; { + n := min(37, len(rest)) + h.Write(rest[:n]) + rest = rest[n:] + } + if got := h.Sum256(); got != w.digest { + t.Fatalf("len %d: fallback Hasher.Sum256 = %x, want %x", len(w.data), got, w.digest) + } + if got := h.Sum([]byte("prefix")); !bytes.Equal(got, append([]byte("prefix"), w.digest[:]...)) { + t.Fatalf("len %d: fallback Sum append mismatch", len(w.data)) + } + + // Squeezing. + stream := make([]byte, 300) + h.Read(stream) + if !bytes.Equal(stream, w.stream) { + t.Fatalf("len %d: fallback Read stream diverged", len(w.data)) + } + + // Reset and reuse. + h.Reset() + h.Write(w.data) + if got := h.Sum256(); got != w.digest { + t.Fatalf("len %d: fallback after Reset = %x, want %x", len(w.data), got, w.digest) + } + } +} + +// TestFallbackCloneAndMarshal exercises the fallback arms of Clone and the +// binary marshaling round-trip, mid-absorb and mid-squeeze. +func TestFallbackCloneAndMarshal(t *testing.T) { + forceFallback(t) + + prefix, suffix := fallbackData(150), fallbackData(41) + whole := Sum256(slices.Concat(prefix, suffix)) + + var h Hasher + h.Write(prefix) + + c, err := h.Clone() + if err != nil { + t.Fatalf("Clone: %v", err) + } + c.Write(suffix) + if got := c.Sum256(); got != whole { + t.Fatalf("fallback clone: %x, want %x", got, whole) + } + + enc, err := h.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + var restored Hasher + if err := restored.UnmarshalBinary(enc); err != nil { + t.Fatalf("UnmarshalBinary: %v", err) + } + restored.Write(suffix) + if got := restored.Sum256(); got != whole { + t.Fatalf("fallback marshal round-trip: %x, want %x", got, whole) + } + + // Mid-squeeze round-trip, including the lazy block-boundary state + // (pos == rate) that only the fallback implementation produces. + for _, pre := range []int{0, 50, rate} { + var s Hasher + s.Write(prefix) + s.Read(make([]byte, pre)) + enc, err := s.MarshalBinary() + if err != nil { + t.Fatalf("pre %d: MarshalBinary: %v", pre, err) + } + var r Hasher + if err := r.UnmarshalBinary(enc); err != nil { + t.Fatalf("pre %d: UnmarshalBinary: %v", pre, err) + } + a, b := make([]byte, 200), make([]byte, 200) + s.Read(a) + r.Read(b) + if !bytes.Equal(a, b) { + t.Fatalf("pre %d: fallback mid-squeeze round-trip diverged", pre) + } + } + + // Zero-value hasher marshals without a backing state. + var zero Hasher + encZero, err := zero.MarshalBinary() + if err != nil { + t.Fatalf("zero MarshalBinary: %v", err) + } + var zr Hasher + if err := zr.UnmarshalBinary(encZero); err != nil { + t.Fatalf("zero UnmarshalBinary: %v", err) + } + if got, want := zr.Sum256(), Sum256(nil); got != want { + t.Fatalf("zero round-trip: %x, want %x", got, want) + } +} + +// TestCrossImplementationMarshal proves the canonical format is portable +// between the native and fallback implementations in-process, in both +// directions — the scenario that motivated the single-format design (the +// same binary picks its implementation per host CPU). +func TestCrossImplementationMarshal(t *testing.T) { + if !useASM { + t.Skip("native implementation unavailable on this CPU") + } + data, suffix := fallbackData(200), fallbackData(77) + whole := Sum256(slices.Concat(data, suffix)) + + // Native → fallback. + var n Hasher + n.Write(data) + encNative, err := n.MarshalBinary() + if err != nil { + t.Fatalf("native MarshalBinary: %v", err) + } + asFallback(func() { + var f Hasher + if err := f.UnmarshalBinary(encNative); err != nil { + t.Fatalf("fallback UnmarshalBinary(native enc): %v", err) + } + f.Write(suffix) + if got := f.Sum256(); got != whole { + t.Fatalf("native->fallback: %x, want %x", got, whole) + } + }) + + // Fallback → native. + var encFallback []byte + asFallback(func() { + var f Hasher + f.Write(data) + var err error + encFallback, err = f.MarshalBinary() + if err != nil { + t.Fatalf("fallback MarshalBinary: %v", err) + } + }) + var n2 Hasher + if err := n2.UnmarshalBinary(encFallback); err != nil { + t.Fatalf("native UnmarshalBinary(fallback enc): %v", err) + } + n2.Write(suffix) + if got := n2.Sum256(); got != whole { + t.Fatalf("fallback->native: %x, want %x", got, whole) + } + + // The two encodings of the same logical state are byte-identical. + if !bytes.Equal(encNative, encFallback) { + t.Fatalf("encodings differ between implementations:\nnative: %x\nfallback: %x", encNative, encFallback) + } +} + +func TestHasherTrivia(t *testing.T) { + h := NewFastKeccak() + if h.Size() != 32 { + t.Fatalf("Size() = %d, want 32", h.Size()) + } + if h.BlockSize() != rate { + t.Fatalf("BlockSize() = %d, want %d", h.BlockSize(), rate) + } + + defer func() { + if recover() == nil { + t.Fatal("expected panic on Sum256 after Read") + } + }() + h.Write([]byte("x")) + h.Read(make([]byte, 32)) + h.Sum256() +} diff --git a/keccak_marshal_test.go b/keccak_marshal_test.go new file mode 100644 index 0000000..6876ba1 --- /dev/null +++ b/keccak_marshal_test.go @@ -0,0 +1,261 @@ +package keccak + +import ( + "bytes" + "encoding/hex" + "slices" + "testing" +) + +func TestCloneIndependent(t *testing.T) { + prefix := make([]byte, 100) + for i := range prefix { + prefix[i] = byte(i * 3) + } + suffixA := []byte("suffix-A") + suffixB := []byte("suffix-B, and a longer one at that") + + var h Hasher + h.Write(prefix) + c, err := h.Clone() + if err != nil { + t.Fatalf("Clone: %v", err) + } + + h.Write(suffixA) + c.Write(suffixB) + + if got, want := h.Sum256(), Sum256(slices.Concat(prefix, suffixA)); got != want { + t.Fatalf("original after clone: %x, want %x", got, want) + } + if got, want := c.Sum256(), Sum256(slices.Concat(prefix, suffixB)); got != want { + t.Fatalf("clone: %x, want %x", got, want) + } +} + +func TestClonePrefixFork(t *testing.T) { + // The motivating pattern: absorb a shared prefix once, fork per suffix. + prefix := make([]byte, rate*2+17) // spans block boundaries + for i := range prefix { + prefix[i] = byte(i) + } + var base Hasher + base.Write(prefix) + + for i := 0; i < 5; i++ { + suffix := bytes.Repeat([]byte{byte(i + 1)}, i*40) + c, err := base.Clone() + if err != nil { + t.Fatalf("Clone: %v", err) + } + c.Write(suffix) + if got, want := c.Sum256(), Sum256(slices.Concat(prefix, suffix)); got != want { + t.Fatalf("fork %d: %x, want %x", i, got, want) + } + } + // base must be unaffected. + if got, want := base.Sum256(), Sum256(prefix); got != want { + t.Fatalf("base after forks: %x, want %x", got, want) + } +} + +func TestCloneMidSqueeze(t *testing.T) { + var h Hasher + h.Write([]byte("squeeze me")) + first := make([]byte, 50) + h.Read(first) + + c, err := h.Clone() + if err != nil { + t.Fatalf("Clone: %v", err) + } + a := make([]byte, 100) + b := make([]byte, 100) + h.Read(a) + c.Read(b) + if !bytes.Equal(a, b) { + t.Fatalf("clone diverged mid-squeeze:\ngot: %x\nwant: %x", b, a) + } +} + +func TestCloneZeroValue(t *testing.T) { + var h Hasher + c, err := h.Clone() + if err != nil { + t.Fatalf("Clone: %v", err) + } + c.Write([]byte("hello")) + if got, want := c.Sum256(), Sum256([]byte("hello")); got != want { + t.Fatalf("clone of zero value: %x, want %x", got, want) + } + if got, want := h.Sum256(), Sum256(nil); got != want { + t.Fatalf("zero value after clone: %x, want %x", got, want) + } +} + +func TestMarshalRoundTrip(t *testing.T) { + data := make([]byte, rate*2+50) + for i := range data { + data[i] = byte(i * 7) + } + // Split points around block boundaries. + for _, split := range []int{0, 1, 17, rate - 1, rate, rate + 1, 2 * rate, len(data)} { + var h Hasher + h.Write(data[:split]) + enc, err := h.MarshalBinary() + if err != nil { + t.Fatalf("split %d: MarshalBinary: %v", split, err) + } + + var restored Hasher + if err := restored.UnmarshalBinary(enc); err != nil { + t.Fatalf("split %d: UnmarshalBinary: %v", split, err) + } + restored.Write(data[split:]) + if got, want := restored.Sum256(), Sum256(data); got != want { + t.Fatalf("split %d: %x, want %x", split, got, want) + } + } +} + +func TestMarshalRoundTripMidSqueeze(t *testing.T) { + for _, pre := range []int{0, 1, 50, rate - 1, rate, 200} { + var h Hasher + h.Write([]byte("marshal mid squeeze")) + skip := make([]byte, pre) + h.Read(skip) + + enc, err := h.MarshalBinary() + if err != nil { + t.Fatalf("pre %d: MarshalBinary: %v", pre, err) + } + var restored Hasher + if err := restored.UnmarshalBinary(enc); err != nil { + t.Fatalf("pre %d: UnmarshalBinary: %v", pre, err) + } + + a := make([]byte, 300) + b := make([]byte, 300) + h.Read(a) + restored.Read(b) + if !bytes.Equal(a, b) { + t.Fatalf("pre %d: restored squeeze diverged:\ngot: %x\nwant: %x", pre, b, a) + } + } +} + +func TestAppendBinary(t *testing.T) { + var h Hasher + h.Write([]byte("append")) + prefix := []byte("existing") + enc, err := h.AppendBinary(append([]byte(nil), prefix...)) + if err != nil { + t.Fatalf("AppendBinary: %v", err) + } + if !bytes.HasPrefix(enc, prefix) { + t.Fatalf("AppendBinary did not preserve prefix") + } + var restored Hasher + if err := restored.UnmarshalBinary(enc[len(prefix):]); err != nil { + t.Fatalf("UnmarshalBinary: %v", err) + } + if got, want := restored.Sum256(), h.Sum256(); got != want { + t.Fatalf("restored: %x, want %x", got, want) + } +} + +// badState is a KeccakState that does not implement encoding.BinaryMarshaler, +// so xcAppendState takes its error path. +type badState struct{ KeccakState } + +// TestAppendBinaryErrorKeepsPrefix checks the encoding.BinaryAppender contract: +// on error the input slice is returned unchanged, not dropped. +func TestAppendBinaryErrorKeepsPrefix(t *testing.T) { + prefix := []byte("existing") + b := append([]byte(nil), prefix...) + got, err := xcAppendState(b, badState{}) + if err == nil { + t.Fatal("expected an error from a state without BinaryMarshaler") + } + if !bytes.Equal(got, prefix) { + t.Fatalf("prefix dropped on error: got %q, want %q", got, prefix) + } +} + +func TestUnmarshalErrors(t *testing.T) { + var h Hasher + h.Write([]byte("valid")) + enc, err := h.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + + flagsOff, posOff := len(marshalMagic)+200, len(marshalMagic)+201 + mutate := func(off int, v byte) []byte { + bad := slices.Clone(enc) + bad[off] = v + return bad + } + for name, bad := range map[string][]byte{ + "nil": nil, + "empty": {}, + "short": []byte("fk"), + "bad magic": slices.Concat([]byte("nope"), enc[4:]), + "magic only": []byte(marshalMagic), + "truncated": enc[:len(enc)-1], + "trailing": append(slices.Clone(enc), 0), + "bad flags": mutate(flagsOff, 2), + "pos == rate while absorbing": mutate(posOff, rate), + "pos > rate": mutate(posOff, 255), + } { + var restored Hasher + if err := restored.UnmarshalBinary(bad); err == nil { + t.Fatalf("UnmarshalBinary(%s) = nil, want error", name) + } + } +} + +// TestMarshalCanonicalBytes pins the wire format: the same logical state must +// marshal to the same bytes on every platform and implementation (the CI +// matrix runs this on native asm builds and the purego fallback). If this +// test needs updating, the format changed — bump the magic version. +func TestMarshalCanonicalBytes(t *testing.T) { + sum := func(b []byte) string { + d := Sum256(b) + return hex.EncodeToString(d[:]) + } + + var h Hasher + enc, err := h.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + if got, want := hex.EncodeToString(enc[:8]), "666b310100000000"; got != want { + t.Fatalf("zero-state prefix = %s, want %s", got, want) + } + if len(enc) != marshaledSize { + t.Fatalf("len = %d, want %d", len(enc), marshaledSize) + } + // Digest of the full encoding is a compact cross-platform fingerprint. + if got, want := sum(enc), "7fe3c540ad846c71039a3ea6100496db5662e71e07178342bc638b301e523b1e"; got != want { + t.Fatalf("zero-state encoding fingerprint = %s, want %s", got, want) + } + + h.Write([]byte("canonical wire format")) + enc, err = h.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + if got, want := sum(enc), "447df4c4591c5cdca00f3b7248d5d3981d631067ca533f166ec5f01a0c019d00"; got != want { + t.Fatalf("absorbing encoding fingerprint = %s, want %s", got, want) + } + + h.Read(make([]byte, 50)) + enc, err = h.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + if got, want := sum(enc), "e0ae1516f534144ab2658af9050963ee3a65c4780d5bff5a31c58de5aa9a2fae"; got != want { + t.Fatalf("squeezing encoding fingerprint = %s, want %s", got, want) + } +}