1// Copyright 2024 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cryptotest
6
7import (
8	"bytes"
9	"hash"
10	"io"
11	"math/rand"
12	"testing"
13	"time"
14)
15
16type MakeHash func() hash.Hash
17
18// TestHash performs a set of tests on hash.Hash implementations, checking the
19// documented requirements of Write, Sum, Reset, Size, and BlockSize.
20func TestHash(t *testing.T, mh MakeHash) {
21
22	// Test that Sum returns an appended digest matching output of Size
23	t.Run("SumAppend", func(t *testing.T) {
24		h := mh()
25		rng := newRandReader(t)
26
27		emptyBuff := []byte("")
28		shortBuff := []byte("a")
29		longBuff := make([]byte, h.BlockSize()+1)
30		rng.Read(longBuff)
31
32		// Set of example strings to append digest to
33		prefixes := [][]byte{nil, emptyBuff, shortBuff, longBuff}
34
35		// Go to each string and check digest gets appended to and is correct size.
36		for _, prefix := range prefixes {
37			h.Reset()
38
39			sum := getSum(t, h, prefix) // Append new digest to prefix
40
41			// Check that Sum didn't alter the prefix
42			if !bytes.Equal(sum[0:len(prefix)], prefix) {
43				t.Errorf("Sum alters passed buffer instead of appending; got %x, want %x", sum[0:len(prefix)], prefix)
44			}
45
46			// Check that the appended sum wasn't affected by the prefix
47			if expectedSum := getSum(t, h, nil); !bytes.Equal(sum[len(prefix):], expectedSum) {
48				t.Errorf("Sum behavior affected by data in the input buffer; got %x, want %x", sum[len(prefix):], expectedSum)
49			}
50
51			// Check size of append
52			if got, want := len(sum)-len(prefix), h.Size(); got != want {
53				t.Errorf("Sum appends number of bytes != Size; got %v , want %v", got, want)
54			}
55		}
56	})
57
58	// Test that Hash.Write never returns error.
59	t.Run("WriteWithoutError", func(t *testing.T) {
60		h := mh()
61		rng := newRandReader(t)
62
63		emptySlice := []byte("")
64		shortSlice := []byte("a")
65		longSlice := make([]byte, h.BlockSize()+1)
66		rng.Read(longSlice)
67
68		// Set of example strings to append digest to
69		slices := [][]byte{emptySlice, shortSlice, longSlice}
70
71		for _, slice := range slices {
72			writeToHash(t, h, slice) // Writes and checks Write doesn't error
73		}
74	})
75
76	t.Run("ResetState", func(t *testing.T) {
77		h := mh()
78		rng := newRandReader(t)
79
80		emptySum := getSum(t, h, nil)
81
82		// Write to hash and then Reset it and see if Sum is same as emptySum
83		writeEx := make([]byte, h.BlockSize())
84		rng.Read(writeEx)
85		writeToHash(t, h, writeEx)
86		h.Reset()
87		resetSum := getSum(t, h, nil)
88
89		if !bytes.Equal(emptySum, resetSum) {
90			t.Errorf("Reset hash yields different Sum than new hash; got %x, want %x", emptySum, resetSum)
91		}
92	})
93
94	// Check that Write isn't reading from beyond input slice's bounds
95	t.Run("OutOfBoundsRead", func(t *testing.T) {
96		h := mh()
97		blockSize := h.BlockSize()
98		rng := newRandReader(t)
99
100		msg := make([]byte, blockSize)
101		rng.Read(msg)
102		writeToHash(t, h, msg)
103		expectedDigest := getSum(t, h, nil) // Record control digest
104
105		h.Reset()
106
107		// Make a buffer with msg in the middle and data on either end
108		buff := make([]byte, blockSize*3)
109		endOfPrefix, startOfSuffix := blockSize, blockSize*2
110
111		copy(buff[endOfPrefix:startOfSuffix], msg)
112		rng.Read(buff[:endOfPrefix])
113		rng.Read(buff[startOfSuffix:])
114
115		writeToHash(t, h, buff[endOfPrefix:startOfSuffix])
116		testDigest := getSum(t, h, nil)
117
118		if !bytes.Equal(testDigest, expectedDigest) {
119			t.Errorf("Write affected by data outside of input slice bounds; got %x, want %x", testDigest, expectedDigest)
120		}
121	})
122
123	// Test that multiple calls to Write is stateful
124	t.Run("StatefulWrite", func(t *testing.T) {
125		h := mh()
126		rng := newRandReader(t)
127
128		prefix, suffix := make([]byte, h.BlockSize()), make([]byte, h.BlockSize())
129		rng.Read(prefix)
130		rng.Read(suffix)
131
132		// Write prefix then suffix sequentially and record resulting hash
133		writeToHash(t, h, prefix)
134		writeToHash(t, h, suffix)
135		serialSum := getSum(t, h, nil)
136
137		h.Reset()
138
139		// Write prefix and suffix at the same time and record resulting hash
140		writeToHash(t, h, append(prefix, suffix...))
141		compositeSum := getSum(t, h, nil)
142
143		// Check that sequential writing results in the same as writing all at once
144		if !bytes.Equal(compositeSum, serialSum) {
145			t.Errorf("two successive Write calls resulted in a different Sum than a single one; got %x, want %x", compositeSum, serialSum)
146		}
147	})
148}
149
150// Helper function for writing. Verifies that Write does not error.
151func writeToHash(t *testing.T, h hash.Hash, p []byte) {
152	t.Helper()
153
154	before := make([]byte, len(p))
155	copy(before, p)
156
157	n, err := h.Write(p)
158	if err != nil || n != len(p) {
159		t.Errorf("Write returned error; got (%v, %v), want (nil, %v)", err, n, len(p))
160	}
161
162	if !bytes.Equal(p, before) {
163		t.Errorf("Write modified input slice; got %x, want %x", p, before)
164	}
165}
166
167// Helper function for getting Sum. Checks that Sum doesn't change hash state.
168func getSum(t *testing.T, h hash.Hash, buff []byte) []byte {
169	t.Helper()
170
171	testBuff := make([]byte, len(buff))
172	copy(testBuff, buff)
173
174	sum := h.Sum(buff)
175	testSum := h.Sum(testBuff)
176
177	// Check that Sum doesn't change underlying hash state
178	if !bytes.Equal(sum, testSum) {
179		t.Errorf("successive calls to Sum yield different results; got %x, want %x", sum, testSum)
180	}
181
182	return sum
183}
184
185func newRandReader(t *testing.T) io.Reader {
186	seed := time.Now().UnixNano()
187	t.Logf("Deterministic RNG seed: 0x%x", seed)
188	return rand.New(rand.NewSource(seed))
189}
190