]> Cypherpunks repositories - gostls13.git/commitdiff
crypto,hash: add and implement hash.Cloner
authorFilippo Valsorda <filippo@golang.org>
Wed, 21 May 2025 21:55:43 +0000 (23:55 +0200)
committerGopher Robot <gobot@golang.org>
Wed, 21 May 2025 23:39:55 +0000 (16:39 -0700)
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>

24 files changed:
api/next/69521.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/sha3/69521.md [new file with mode: 0644]
doc/next/6-stdlib/99-minor/hash/69521.md [new file with mode: 0644]
doc/next/6-stdlib/99-minor/hash/maphash/69521.md [new file with mode: 0644]
src/crypto/hmac/hmac_test.go
src/crypto/internal/cryptotest/hash.go
src/crypto/internal/fips140/hmac/hmac.go
src/crypto/internal/fips140/sha256/sha256.go
src/crypto/internal/fips140/sha512/sha512.go
src/crypto/md5/md5.go
src/crypto/md5/md5_test.go
src/crypto/sha1/sha1.go
src/crypto/sha1/sha1_test.go
src/crypto/sha256/sha256_test.go
src/crypto/sha3/sha3.go
src/crypto/sha3/sha3_test.go
src/crypto/sha512/sha512_test.go
src/hash/adler32/adler32.go
src/hash/crc32/crc32.go
src/hash/crc64/crc64.go
src/hash/fnv/fnv.go
src/hash/hash.go
src/hash/maphash/maphash.go
src/internal/testhash/hash.go

diff --git a/api/next/69521.txt b/api/next/69521.txt
new file mode 100644 (file)
index 0000000..6974226
--- /dev/null
@@ -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
diff --git a/doc/next/6-stdlib/99-minor/crypto/sha3/69521.md b/doc/next/6-stdlib/99-minor/crypto/sha3/69521.md
new file mode 100644 (file)
index 0000000..2af674d
--- /dev/null
@@ -0,0 +1 @@
+The new [SHA3.Clone] method implements [hash.Cloner](/pkg/hash#Cloner).
diff --git a/doc/next/6-stdlib/99-minor/hash/69521.md b/doc/next/6-stdlib/99-minor/hash/69521.md
new file mode 100644 (file)
index 0000000..a8d58e3
--- /dev/null
@@ -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].
diff --git a/doc/next/6-stdlib/99-minor/hash/maphash/69521.md b/doc/next/6-stdlib/99-minor/hash/maphash/69521.md
new file mode 100644 (file)
index 0000000..497df8b
--- /dev/null
@@ -0,0 +1 @@
+The new [Hash.Clone] method implements [hash.Cloner](/pkg/hash#Cloner).
index 7accad763244a1f1d73f32cdcf5dda51eaf1501d..9b7eee7bf7873e70e88bfa2e88fc2ba1e0ad2cd5 100644 (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)
index a2916e9c87ab752b5b860b4b6869017a39361e29..f00e9c80d3c86af03757960e37749d6d458dbbd8 100644 (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))
 }
 
index 3d193d55923ee692e8faa7a9c0d836bf93645367..9b28017662564b126f31c2028510151341b3a7a5 100644 (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)}
index bc157f9adbfc346101b155e97691e37fc3c68751..a51ad2be24d3a96d776068fb139040aa68c05bfe 100644 (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
index 55c90a8cd68cdf4ff3be91efacd2055de27c23f8..3e7a5e11f151986324d8c6fb84f8b0553c23a03d 100644 (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}
index dc586fb21787c93ce07a5341c4bbf61962a540c4..9274f89d3e2c1559b5c282c4881b3d2947f50b9e 100644 (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
index c0bb15f05bac7c8e26f21e6793f2c76b108fe9a2..403ff2881f4b685b966f7660dc5ccfc9fb4580e4 100644 (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())
index d2ffaac0aeb674cf4065934b5763cb5ab0c63277..3acc5b11fb789e32756b6aaad48431450560cecf 100644 (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
index 0a0596e56c545d0614292730ac31f78ce1107852..ef6e5ddcbb2d97f4791a1bced926b93d0f04e243 100644 (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)
 
index 38a7f25afb3fd1c4c3064e5939dbc681771aa239..11b24db7d6b0a0357edfae9ee340d6eabbef782a 100644 (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) {
index a6c5ae55f1ff206d2cf745e8963cbe29df96577d..2a1b3ca7008f5e9de7694cf6f906cb334de48afe 100644 (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
index 6757d6efa1fcc9b1390416b995044c462d99c271..15ee877236e195ad3b6a8c8832b7d29afd3ec803 100644 (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
index 7e80f49dea4315f701f56fb795dab387ea392c46..080bf694f03652867c9989b8886f1d38de17620b 100644 (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)
 
index e2551e09522651372490c6e789352c5bc5d0bdf8..c6179789eafbd900c7b459839c1c957f135f8172 100644 (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)
index d40bb1b7acdb392745231b5b85fdd27f51879add..e58f1123198a726a63e708d038937b6cfa250be3 100644 (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:
index c40c7024b6aaba50ca374736a8dcd29a6c16d20c..1e551ff454bd1528a3891519b097baa23d88ba01 100644 (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
index 5c4b9b5da8c895dc2ad07602b6322444fbd109e7..dd4a77ce1a9420942f697718847c743939d1d407 100644 (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
+}
index c72c4af710d29a70d31bcffe3bbf00aa75de062f..6483bc1086e744e64b550bb38eeec8076c5e665f 100644 (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
index a8872d72a5aa77204e2f3000170eb5713ce92e33..5004539f070d28170a609eb3efcb46b03df67450 100644 (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.
index d863408f55062d96c7e23bf94fbb6f0d1292f4be..3413d5c20de29b6a0f5be558486eee3f2ece1959 100644 (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()