Skip to content

Commit 1700a13

Browse files
committed
crypto: support pre-hashed signing/verify
1 parent cb8e31b commit 1700a13

12 files changed

Lines changed: 108 additions & 2 deletions

File tree

crypto/_testsuite/testsuite.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct {
3131

3232
MultibaseCode uint64
3333

34-
DefaultHash crypto.Hash
35-
OtherHashes []crypto.Hash
34+
DefaultHash crypto.Hash
35+
OtherHashes []crypto.Hash
36+
SupportsPreHashed bool
3637

3738
PublicKeyBytesSize int
3839
PrivateKeyBytesSize int
@@ -330,6 +331,37 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
330331
require.False(t, valid)
331332
})
332333
}
334+
335+
t.Run(tc.name+"-Prehashed", func(t *testing.T) {
336+
msg := []byte("message")
337+
338+
if harness.SupportsPreHashed {
339+
// Pre-hash the message with the default hash, then sign and verify the digest directly.
340+
h := tc.defaultHash.New()
341+
h.Write(msg)
342+
digest := h.Sum(nil)
343+
344+
sig, err := tc.signer(digest, crypto.WithSigningPreHashed())
345+
require.NoError(t, err)
346+
require.NotEmpty(t, sig)
347+
348+
valid := tc.verifier(digest, sig, crypto.WithSigningPreHashed())
349+
require.True(t, valid)
350+
351+
// Wrong digest must not verify.
352+
wrong := make([]byte, len(digest))
353+
valid = tc.verifier(wrong, sig, crypto.WithSigningPreHashed())
354+
require.False(t, valid)
355+
} else {
356+
// Key type does not support PREHASHED: sign must return an error,
357+
// verify must return false.
358+
_, err := tc.signer(msg, crypto.WithSigningPreHashed())
359+
require.Error(t, err)
360+
361+
valid := tc.verifier(msg, []byte("fake"), crypto.WithSigningPreHashed())
362+
require.False(t, valid)
363+
}
364+
})
333365
}
334366
})
335367

crypto/ed25519/key_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var harness = testsuite.TestHarness[PublicKey, PrivateKey]{
2525
MultibaseCode: MultibaseCode,
2626
DefaultHash: crypto.SHA512,
2727
OtherHashes: nil,
28+
SupportsPreHashed: false,
2829
PublicKeyBytesSize: PublicKeyBytesSize,
2930
PrivateKeyBytesSize: PrivateKeyBytesSize,
3031
SignatureBytesSize: SignatureBytesSize,

crypto/hash.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"github.com/ucan-wg/go-varsig"
99
"golang.org/x/crypto/sha3"
10+
11+
helpers "github.com/MetaMask/go-did-it/crypto/internal"
1012
)
1113

1214
// As the standard crypto library prohibits from registering additional hash algorithm (like keccak),
@@ -40,6 +42,11 @@ const (
4042
maxStdHash
4143

4244
// Extensions
45+
46+
// PREHASHED signals that the message is already a digest — no hashing is applied.
47+
// The caller is responsible for passing a correctly sized digest for the chosen key type.
48+
// Note: PREHASHED has no varsig representation and is not supported by some key types.
49+
PREHASHED
4350
KECCAK_256
4451
KECCAK_512
4552

@@ -82,6 +89,9 @@ func (h Hash) ToVarsigHash() varsig.Hash {
8289
if h == MD5SHA1 {
8390
panic("no multihash/multicodec value exists for MD5+SHA1")
8491
}
92+
if h == PREHASHED {
93+
panic("PREHASHED has no varsig hash representation")
94+
}
8595
if h < maxHash {
8696
return hashVarsigs[h]
8797
}
@@ -137,10 +147,12 @@ func FromVarsigHash(h varsig.Hash) Hash {
137147
}
138148

139149
var hashNames = []string{
150+
"Prehashed",
140151
"Keccak-256",
141152
"Keccak-512",
142153
}
143154
var hashFns = []func() hash.Hash{
155+
helpers.NewPreHashedHasher,
144156
sha3.NewLegacyKeccak256,
145157
sha3.NewLegacyKeccak512,
146158
}
@@ -166,6 +178,7 @@ var hashVarsigs = []varsig.Hash{
166178
varsig.HashBlake2b_384,
167179
varsig.HashBlake2b_512,
168180
0, // maxStdHash
181+
0, // PREHASHED - no varsig representation; ToVarsigHash panics before reaching here
169182
varsig.HashKeccak_256,
170183
varsig.HashKeccak_512,
171184
}

crypto/internal/hash.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package helpers
2+
3+
import "hash"
4+
5+
var _ hash.Hash = &preHashedHasher{}
6+
7+
// preHashedHasher is an identity hash.Hash: Write accumulates bytes, Sum returns them as-is.
8+
// Used with PREHASHED to pass a pre-computed digest through the standard hashing pattern
9+
// without any additional transformation.
10+
type preHashedHasher struct {
11+
buf []byte
12+
}
13+
14+
func NewPreHashedHasher() hash.Hash {
15+
return &preHashedHasher{}
16+
}
17+
18+
func (h *preHashedHasher) Write(p []byte) (int, error) {
19+
h.buf = append(h.buf, p...)
20+
return len(p), nil
21+
}
22+
23+
func (h *preHashedHasher) Sum(b []byte) []byte {
24+
return append(b, h.buf...)
25+
}
26+
27+
func (h *preHashedHasher) Reset() {
28+
h.buf = h.buf[:0]
29+
}
30+
31+
func (h *preHashedHasher) Size() int {
32+
return -1
33+
}
34+
35+
func (h *preHashedHasher) BlockSize() int {
36+
return 1
37+
}

crypto/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ func WithSigningHash(hash Hash) SigningOption {
6767
}
6868
}
6969

70+
// WithSigningPreHashed specify that the signing message was pre-hashed.
71+
// When using this option, the caller is responsible for providing a pre-computed digest
72+
// suitable for the key type.
73+
// This is the same as WithSigningHash(PREHASHED).
74+
func WithSigningPreHashed() SigningOption {
75+
return func(opts *SigningOpts) {
76+
opts.hash = PREHASHED
77+
}
78+
}
79+
7080
// WithPayloadEncoding specify the encoding that was used on the message before signing it.
7181
// This will be included in the resulting varsig.
7282
func WithPayloadEncoding(encoding varsig.PayloadEncoding) SigningOption {

crypto/p256/key_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
2323
MultibaseCode: MultibaseCode,
2424
DefaultHash: crypto.SHA256,
2525
OtherHashes: []crypto.Hash{crypto.SHA224, crypto.SHA384, crypto.SHA512},
26+
SupportsPreHashed: true,
2627
PublicKeyBytesSize: PublicKeyBytesSize,
2728
PrivateKeyBytesSize: PrivateKeyBytesSize,
2829
SignatureBytesSize: SignatureBytesSize,

crypto/p384/key_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
2020
MultibaseCode: MultibaseCode,
2121
DefaultHash: crypto.SHA384,
2222
OtherHashes: []crypto.Hash{crypto.SHA256, crypto.SHA512},
23+
SupportsPreHashed: true,
2324
PublicKeyBytesSize: PublicKeyBytesSize,
2425
PrivateKeyBytesSize: PrivateKeyBytesSize,
2526
SignatureBytesSize: SignatureBytesSize,

crypto/p521/key_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
2020
MultibaseCode: MultibaseCode,
2121
DefaultHash: crypto.SHA512,
2222
OtherHashes: []crypto.Hash{crypto.SHA384},
23+
SupportsPreHashed: true,
2324
PublicKeyBytesSize: PublicKeyBytesSize,
2425
PrivateKeyBytesSize: PrivateKeyBytesSize,
2526
SignatureBytesSize: SignatureBytesSize,

crypto/rsa/key_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var harness2048 = testsuite.TestHarness[*PublicKey, *PrivateKey]{
2121
MultibaseCode: MultibaseCode,
2222
DefaultHash: crypto.SHA256,
2323
OtherHashes: []crypto.Hash{crypto.SHA384, crypto.SHA512},
24+
SupportsPreHashed: false,
2425
}
2526

2627
var harness3072 = testsuite.TestHarness[*PublicKey, *PrivateKey]{
@@ -34,6 +35,7 @@ var harness3072 = testsuite.TestHarness[*PublicKey, *PrivateKey]{
3435
MultibaseCode: MultibaseCode,
3536
DefaultHash: crypto.SHA384,
3637
OtherHashes: []crypto.Hash{crypto.SHA512},
38+
SupportsPreHashed: false,
3739
}
3840

3941
var harness4096 = testsuite.TestHarness[*PublicKey, *PrivateKey]{
@@ -47,6 +49,7 @@ var harness4096 = testsuite.TestHarness[*PublicKey, *PrivateKey]{
4749
MultibaseCode: MultibaseCode,
4850
DefaultHash: crypto.SHA512,
4951
OtherHashes: []crypto.Hash{},
52+
SupportsPreHashed: false,
5053
}
5154

5255
func TestSuite2048(t *testing.T) {

crypto/rsa/private.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([
170170
params := crypto.CollectSigningOptions(opts)
171171

172172
hashCode := params.HashOrDefault(defaultSigHash(p.k.N.BitLen()))
173+
if hashCode == crypto.PREHASHED {
174+
return nil, fmt.Errorf("rsa: PREHASHED is not supported")
175+
}
173176
hasher := hashCode.New()
174177
hasher.Write(message)
175178
hash := hasher.Sum(nil)

0 commit comments

Comments
 (0)