crypto,hash: add and implement hash.Cloner

Fixes #69521

Co-authored-by: qiulaidongfeng <2645477756@qq.com>
Change-Id: I6a6a465652f5ab7e6c9054e826e17df2b8b34e41
Reviewed-on: https://go-review.googlesource.com/c/go/+/675197
Reviewed-by: Roland Shoemaker <roland@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Filippo Valsorda 2025-05-21 23:55:43 +02:00 committed by Gopher Robot
parent de457fc4ea
commit edcde86990
24 changed files with 243 additions and 22 deletions

9
api/next/69521.txt Normal file
View File

@ -0,0 +1,9 @@
pkg crypto/sha3, method (*SHA3) Clone() (hash.Cloner, error) #69521
pkg hash, type Cloner interface { BlockSize, Clone, Reset, Size, Sum, Write } #69521
pkg hash, type Cloner interface, BlockSize() int #69521
pkg hash, type Cloner interface, Clone() (Cloner, error) #69521
pkg hash, type Cloner interface, Reset() #69521
pkg hash, type Cloner interface, Size() int #69521
pkg hash, type Cloner interface, Sum([]uint8) []uint8 #69521
pkg hash, type Cloner interface, Write([]uint8) (int, error) #69521
pkg hash/maphash, method (*Hash) Clone() (hash.Cloner, error) #69521

View File

@ -0,0 +1 @@
The new [SHA3.Clone] method implements [hash.Cloner](/pkg/hash#Cloner).

View File

@ -0,0 +1,2 @@
Hashes implementing the new [Cloner] interface can return a copy of their state.
All standard library [Hash] implementations now implement [Cloner].

View File

@ -0,0 +1 @@
The new [Hash.Clone] method implements [hash.Cloner](/pkg/hash#Cloner).

View File

@ -632,6 +632,18 @@ func TestHMACHash(t *testing.T) {
}
}
func TestExtraMethods(t *testing.T) {
h := New(sha256.New, []byte("key"))
cryptotest.NoExtraMethods(t, maybeCloner(h))
}
func maybeCloner(h hash.Hash) any {
if c, ok := h.(hash.Cloner); ok {
return &c
}
return &h
}
func BenchmarkHMACSHA256_1K(b *testing.B) {
key := make([]byte, 32)
buf := make([]byte, 1024)

View File

@ -5,6 +5,8 @@
package cryptotest
import (
"crypto/internal/boring"
"crypto/internal/fips140"
"hash"
"internal/testhash"
"io"
@ -18,6 +20,10 @@ type MakeHash func() hash.Hash
// TestHash performs a set of tests on hash.Hash implementations, checking the
// documented requirements of Write, Sum, Reset, Size, and BlockSize.
func TestHash(t *testing.T, mh MakeHash) {
if boring.Enabled || fips140.Version() == "v1.0" {
testhash.TestHashWithoutClone(t, testhash.MakeHash(mh))
return
}
testhash.TestHash(t, testhash.MakeHash(mh))
}

View File

@ -12,6 +12,7 @@ import (
"crypto/internal/fips140/sha256"
"crypto/internal/fips140/sha3"
"crypto/internal/fips140/sha512"
"errors"
"hash"
)
@ -29,6 +30,7 @@ type marshalable interface {
}
type HMAC struct {
// opad and ipad may share underlying storage with HMAC clones.
opad, ipad []byte
outer, inner hash.Hash
@ -128,6 +130,30 @@ func (h *HMAC) Reset() {
h.marshaled = true
}
// Clone implements [hash.Cloner] if the underlying hash does.
// Otherwise, it returns [errors.ErrUnsupported].
func (h *HMAC) Clone() (hash.Cloner, error) {
r := *h
ic, ok := h.inner.(hash.Cloner)
if !ok {
return nil, errors.ErrUnsupported
}
oc, ok := h.outer.(hash.Cloner)
if !ok {
return nil, errors.ErrUnsupported
}
var err error
r.inner, err = ic.Clone()
if err != nil {
return nil, errors.ErrUnsupported
}
r.outer, err = oc.Clone()
if err != nil {
return nil, errors.ErrUnsupported
}
return &r, nil
}
// New returns a new HMAC hash using the given [hash.Hash] type and key.
func New[H hash.Hash](h func() H, key []byte) *HMAC {
hm := &HMAC{keyLen: len(key)}

View File

@ -10,6 +10,7 @@ import (
"crypto/internal/fips140"
"crypto/internal/fips140deps/byteorder"
"errors"
"hash"
)
// The size of a SHA-256 checksum in bytes.
@ -115,6 +116,11 @@ func consumeUint32(b []byte) ([]byte, uint32) {
return b[4:], byteorder.BEUint32(b)
}
func (d *Digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *Digest) Reset() {
if !d.is224 {
d.h[0] = init0

View File

@ -10,6 +10,7 @@ import (
"crypto/internal/fips140"
"crypto/internal/fips140deps/byteorder"
"errors"
"hash"
)
const (
@ -194,6 +195,11 @@ func consumeUint64(b []byte) ([]byte, uint64) {
return b[8:], byteorder.BEUint64(b)
}
func (d *Digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
// New returns a new Digest computing the SHA-512 hash.
func New() *Digest {
d := &Digest{size: size512}

View File

@ -104,6 +104,11 @@ func consumeUint32(b []byte) ([]byte, uint32) {
return b[4:], byteorder.BEUint32(b[0:4])
}
func (d *digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
// New returns a new [hash.Hash] computing the MD5 checksum. The Hash
// also implements [encoding.BinaryMarshaler], [encoding.BinaryAppender] and
// [encoding.BinaryUnmarshaler] to marshal and unmarshal the internal

View File

@ -270,10 +270,17 @@ func TestMD5Hash(t *testing.T) {
}
func TestExtraMethods(t *testing.T) {
h := New()
h := maybeCloner(New())
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
}
func maybeCloner(h hash.Hash) any {
if c, ok := h.(hash.Cloner); ok {
return &c
}
return &h
}
var bench = New()
var buf = make([]byte, 1024*1024*8+1)
var sum = make([]byte, bench.Size())

View File

@ -93,6 +93,11 @@ func consumeUint32(b []byte) ([]byte, uint32) {
return b[4:], byteorder.BEUint32(b)
}
func (d *digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *digest) Reset() {
d.h[0] = init0
d.h[1] = init1

View File

@ -242,11 +242,18 @@ func TestSHA1Hash(t *testing.T) {
}
func TestExtraMethods(t *testing.T) {
h := New()
h := maybeCloner(New())
cryptotest.NoExtraMethods(t, &h, "ConstantTimeSum",
"MarshalBinary", "UnmarshalBinary", "AppendBinary")
}
func maybeCloner(h hash.Hash) any {
if c, ok := h.(hash.Cloner); ok {
return &c
}
return &h
}
var bench = New()
var buf = make([]byte, 8192)

View File

@ -403,18 +403,25 @@ func TestHash(t *testing.T) {
func TestExtraMethods(t *testing.T) {
t.Run("SHA-224", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha256", func(t *testing.T) {
h := New224()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New224())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
t.Run("SHA-256", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha256", func(t *testing.T) {
h := New()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
}
func maybeCloner(h hash.Hash) any {
if c, ok := h.(hash.Cloner); ok {
return &c
}
return &h
}
var bench = New()
func benchmarkSize(b *testing.B, size int) {

View File

@ -166,6 +166,12 @@ func (s *SHA3) UnmarshalBinary(data []byte) error {
return s.s.UnmarshalBinary(data)
}
// Clone implements [hash.Cloner].
func (d *SHA3) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
// SHAKE is an instance of a SHAKE extendable output function.
type SHAKE struct {
s sha3.SHAKE

View File

@ -42,13 +42,14 @@ var testShakes = map[string]struct {
"cSHAKE256": {NewCSHAKE256, "CSHAKE256", "CustomString"},
}
// decodeHex converts a hex-encoded string into a raw byte string.
func decodeHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
func TestSHA3Hash(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha3", func(t *testing.T) {
for name, f := range testDigests {
t.Run(name, func(t *testing.T) {
cryptotest.TestHash(t, func() hash.Hash { return f() })
})
}
})
}
// TestUnalignedWrite tests that writing data in an arbitrary pattern with

View File

@ -966,30 +966,37 @@ func TestHash(t *testing.T) {
func TestExtraMethods(t *testing.T) {
t.Run("SHA-384", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha512", func(t *testing.T) {
h := New384()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New384())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
t.Run("SHA-512/224", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha512", func(t *testing.T) {
h := New512_224()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New512_224())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
t.Run("SHA-512/256", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha512", func(t *testing.T) {
h := New512_256()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New512_256())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
t.Run("SHA-512", func(t *testing.T) {
cryptotest.TestAllImplementations(t, "sha512", func(t *testing.T) {
h := New()
cryptotest.NoExtraMethods(t, &h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
h := maybeCloner(New())
cryptotest.NoExtraMethods(t, h, "MarshalBinary", "UnmarshalBinary", "AppendBinary")
})
})
}
func maybeCloner(h hash.Hash) any {
if c, ok := h.(hash.Cloner); ok {
return &c
}
return &h
}
var bench = New()
var buf = make([]byte, 8192)

View File

@ -78,6 +78,11 @@ func (d *digest) UnmarshalBinary(b []byte) error {
return nil
}
func (d *digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
// Add p to the running checksum d.
func update(d digest, p []byte) digest {
s1, s2 := uint32(d&0xffff), uint32(d>>16)

View File

@ -194,6 +194,11 @@ func (d *digest) UnmarshalBinary(b []byte) error {
return nil
}
func (d *digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func update(crc uint32, tab *Table, p []byte, checkInitIEEE bool) uint32 {
switch {
case haveCastagnoli.Load() && tab == castagnoliTable:

View File

@ -133,6 +133,11 @@ func (d *digest) UnmarshalBinary(b []byte) error {
return nil
}
func (d *digest) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func update(crc uint64, tab *Table, p []byte) uint64 {
buildSlicing8TablesOnce()
crc = ^crc

View File

@ -348,3 +348,33 @@ func (s *sum128a) UnmarshalBinary(b []byte) error {
s[1] = byteorder.BEUint64(b[12:])
return nil
}
func (d *sum32) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *sum32a) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *sum64) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *sum64a) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *sum128) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}
func (d *sum128a) Clone() (hash.Cloner, error) {
r := *d
return &r, nil
}

View File

@ -57,6 +57,18 @@ type Hash64 interface {
Sum64() uint64
}
// A Cloner is a hash function whose state can be cloned.
//
// All [Hash] implementations in the standard library implement this interface,
// unless GOFIPS140=v1.0.0 is set.
//
// If a hash can only determine at runtime if it can be cloned,
// (e.g., if it wraps another hash), it may return [errors.ErrUnsupported].
type Cloner interface {
Hash
Clone() (Cloner, error)
}
// XOF (extendable output function) is a hash function with arbitrary or unlimited output length.
type XOF interface {
// Write absorbs more data into the XOF's state. It panics if called

View File

@ -13,6 +13,7 @@
package maphash
import (
"hash"
"internal/byteorder"
"math"
)
@ -80,7 +81,7 @@ func String(seed Seed, s string) uint64 {
//
// The zero Hash is a valid Hash ready to use.
// A zero Hash chooses a random seed for itself during
// the first call to a Reset, Write, Seed, or Sum64 method.
// the first call to a Reset, Write, Seed, Clone, or Sum64 method.
// For control over the seed, use SetSeed.
//
// The computed hash values depend only on the initial seed and
@ -281,6 +282,13 @@ func (h *Hash) Size() int { return 8 }
// BlockSize returns h's block size.
func (h *Hash) BlockSize() int { return len(h.buf) }
// Clone implements [hash.Cloner].
func (h *Hash) Clone() (hash.Cloner, error) {
h.initSeed()
r := *h
return &r, nil
}
// Comparable returns the hash of comparable value v with the given seed
// such that Comparable(s, v1) == Comparable(s, v2) if v1 == v2.
// If v != v, then the resulting hash is randomly distributed.

View File

@ -18,7 +18,49 @@ type MakeHash func() hash.Hash
// TestHash performs a set of tests on hash.Hash implementations, checking the
// documented requirements of Write, Sum, Reset, Size, and BlockSize.
func TestHash(t *testing.T, mh MakeHash) {
TestHashWithoutClone(t, mh)
// Test whether the results after cloning are consistent.
t.Run("Clone", func(t *testing.T) {
h, ok := mh().(hash.Cloner)
if !ok {
t.Fatalf("%T does not implement hash.Cloner", mh)
}
h3, err := h.Clone()
if err != nil {
t.Fatalf("Clone failed: %v", err)
}
prefix := []byte("tmp")
writeToHash(t, h, prefix)
h2, err := h.Clone()
if err != nil {
t.Fatalf("Clone failed: %v", err)
}
prefixSum := h.Sum(nil)
if !bytes.Equal(prefixSum, h2.Sum(nil)) {
t.Fatalf("%T Clone results are inconsistent", h)
}
suffix := []byte("tmp2")
writeToHash(t, h, suffix)
writeToHash(t, h3, append(prefix, suffix...))
compositeSum := h3.Sum(nil)
if !bytes.Equal(h.Sum(nil), compositeSum) {
t.Fatalf("%T Clone results are inconsistent", h)
}
if !bytes.Equal(h2.Sum(nil), prefixSum) {
t.Fatalf("%T Clone results are inconsistent", h)
}
writeToHash(t, h2, suffix)
if !bytes.Equal(h.Sum(nil), compositeSum) {
t.Fatalf("%T Clone results are inconsistent", h)
}
if !bytes.Equal(h2.Sum(nil), compositeSum) {
t.Fatalf("%T Clone results are inconsistent", h)
}
})
}
func TestHashWithoutClone(t *testing.T, mh MakeHash) {
// Test that Sum returns an appended digest matching output of Size
t.Run("SumAppend", func(t *testing.T) {
h := mh()