Skip to content

Commit 45a9578

Browse files
authored
Merge pull request #11 from lestrrat-go/feat-sign-digest
add SignDigest for pre-computed digest signing
2 parents 598e8ab + ea58240 commit 45a9578

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

Changes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Changes
22
=======
33

4+
v1.2.1 UNRELEASED
5+
* Add `SignDigest()` for signing pre-computed digests. Supported for HMAC,
6+
RSA (PKCS1v15 and PSS), and ECDSA families. EdDSA and Custom return an error.
7+
48
v1.2.0 6 Apr 2026
59
* Add `VerifyDigest()` for verifying signatures against pre-computed digests.
610
Supported for HMAC, RSA, and ECDSA families. EdDSA and Custom return an error.

sign.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package dsig
22

33
import (
44
"crypto"
5+
"crypto/ecdsa"
6+
"crypto/rand"
57
"crypto/rsa"
68
"fmt"
79
"io"
@@ -105,3 +107,104 @@ func dispatchCustomSign(key any, info AlgorithmInfo, payload []byte, rr io.Reade
105107
}
106108
return signer.Sign(key, payload, rr)
107109
}
110+
111+
// SignDigest generates a digital signature from a pre-computed digest.
112+
//
113+
// For RSA/ECDSA, digest is the hash of the signing input and key is the
114+
// private key used for signing.
115+
//
116+
// For HMAC, the digest must be the pre-computed MAC (i.e. the output of
117+
// hmac.New(hashFunc, key) after writing the signing input). The digest IS
118+
// the signature, so it is returned as-is.
119+
//
120+
// EdDSA and Custom families are not supported and return an error.
121+
//
122+
// rr is an io.Reader that provides randomness for signing. If rr is nil,
123+
// it defaults to rand.Reader.
124+
func SignDigest(key any, alg string, digest []byte, rr io.Reader) ([]byte, error) {
125+
info, ok := GetAlgorithmInfo(alg)
126+
if !ok {
127+
return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature algorithm %q`, alg)
128+
}
129+
130+
switch info.Family {
131+
case HMAC:
132+
// The caller already computed the HMAC (which incorporates the key)
133+
// and passed it as digest. The digest IS the signature.
134+
return digest, nil
135+
case RSA:
136+
return dispatchRSASignDigest(key, info, digest, rr)
137+
case ECDSA:
138+
return dispatchECDSASignDigest(key, info, digest, rr)
139+
case EdDSAFamily:
140+
return nil, fmt.Errorf(`dsig.SignDigest: EdDSA does not support digest-based signing`)
141+
case Custom:
142+
return nil, fmt.Errorf(`dsig.SignDigest: custom algorithms do not support digest-based signing`)
143+
default:
144+
return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature family %q`, info.Family)
145+
}
146+
}
147+
148+
func dispatchRSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) {
149+
meta, ok := info.Meta.(RSAFamilyMeta)
150+
if !ok {
151+
return nil, fmt.Errorf(`dsig.SignDigest: invalid RSA metadata`)
152+
}
153+
154+
if rr == nil {
155+
rr = rand.Reader
156+
}
157+
158+
cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key)
159+
if err != nil {
160+
return nil, fmt.Errorf(`dsig.SignDigest: %w`, err)
161+
}
162+
if isCryptoSigner {
163+
var opts crypto.SignerOpts = meta.Hash
164+
if meta.PSS {
165+
rsaopts := rsaPSSOptions(meta.Hash)
166+
opts = &rsaopts
167+
}
168+
return cs.Sign(rr, digest, opts)
169+
}
170+
171+
privkey, ok := key.(*rsa.PrivateKey)
172+
if !ok {
173+
return nil, fmt.Errorf(`dsig.SignDigest: invalid key type %T. *rsa.PrivateKey is required`, key)
174+
}
175+
if meta.PSS {
176+
rsaopts := rsaPSSOptions(meta.Hash)
177+
return rsa.SignPSS(rr, privkey, meta.Hash, digest, &rsaopts)
178+
}
179+
return rsa.SignPKCS1v15(rr, privkey, meta.Hash, digest)
180+
}
181+
182+
func dispatchECDSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) {
183+
meta, ok := info.Meta.(ECDSAFamilyMeta)
184+
if !ok {
185+
return nil, fmt.Errorf(`dsig.SignDigest: invalid ECDSA metadata`)
186+
}
187+
188+
if rr == nil {
189+
rr = rand.Reader
190+
}
191+
192+
privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key)
193+
if err != nil {
194+
return nil, fmt.Errorf(`dsig.SignDigest: %w`, err)
195+
}
196+
if isCryptoSigner {
197+
signed, err := cs.Sign(rr, digest, meta.Hash)
198+
if err != nil {
199+
return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using crypto.Signer: %w`, err)
200+
}
201+
return signECDSACryptoSigner(cs, signed)
202+
}
203+
204+
r, s, err := ecdsa.Sign(rr, privkey, digest)
205+
if err != nil {
206+
return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using ecdsa: %w`, err)
207+
}
208+
return PackECDSASignature(r, s, privkey.Curve.Params().BitSize)
209+
}
210+

sign_digest_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package dsig_test
2+
3+
import (
4+
"crypto"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/hmac"
8+
"crypto/rand"
9+
"crypto/rsa"
10+
"crypto/sha256"
11+
"testing"
12+
13+
"github.com/lestrrat-go/dsig"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestSignDigest(t *testing.T) {
18+
t.Parallel()
19+
payload := []byte("hello world")
20+
21+
t.Run("HMAC returns digest unchanged", func(t *testing.T) {
22+
t.Parallel()
23+
key := []byte("secretkey")
24+
25+
mac := hmac.New(sha256.New, key)
26+
mac.Write(payload)
27+
digest := mac.Sum(nil)
28+
29+
sig, err := dsig.SignDigest(key, dsig.HMACWithSHA256, digest, nil)
30+
require.NoError(t, err)
31+
require.Equal(t, digest, sig, "HMAC SignDigest should return digest as-is")
32+
33+
require.NoError(t, dsig.VerifyDigest(key, dsig.HMACWithSHA256, digest, sig))
34+
})
35+
36+
t.Run("RSA PKCS1v15", func(t *testing.T) {
37+
t.Parallel()
38+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
39+
require.NoError(t, err)
40+
41+
hasher := crypto.SHA256.New()
42+
hasher.Write(payload)
43+
digest := hasher.Sum(nil)
44+
45+
sig, err := dsig.SignDigest(priv, dsig.RSAPKCS1v15WithSHA256, digest, nil)
46+
require.NoError(t, err)
47+
48+
require.NoError(t, dsig.VerifyDigest(&priv.PublicKey, dsig.RSAPKCS1v15WithSHA256, digest, sig))
49+
})
50+
51+
t.Run("RSA PSS", func(t *testing.T) {
52+
t.Parallel()
53+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
54+
require.NoError(t, err)
55+
56+
hasher := crypto.SHA256.New()
57+
hasher.Write(payload)
58+
digest := hasher.Sum(nil)
59+
60+
sig, err := dsig.SignDigest(priv, dsig.RSAPSSWithSHA256, digest, nil)
61+
require.NoError(t, err)
62+
63+
require.NoError(t, dsig.VerifyDigest(&priv.PublicKey, dsig.RSAPSSWithSHA256, digest, sig))
64+
})
65+
66+
t.Run("RSA via crypto.Signer", func(t *testing.T) {
67+
t.Parallel()
68+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
69+
require.NoError(t, err)
70+
71+
hasher := crypto.SHA256.New()
72+
hasher.Write(payload)
73+
digest := hasher.Sum(nil)
74+
75+
// *rsa.PrivateKey implements crypto.Signer — this exercises the crypto.Signer path
76+
sig, err := dsig.SignDigest(priv, dsig.RSAPKCS1v15WithSHA256, digest, nil)
77+
require.NoError(t, err)
78+
79+
require.NoError(t, dsig.VerifyDigest(&priv.PublicKey, dsig.RSAPKCS1v15WithSHA256, digest, sig))
80+
})
81+
82+
t.Run("ECDSA", func(t *testing.T) {
83+
t.Parallel()
84+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
85+
require.NoError(t, err)
86+
87+
hasher := crypto.SHA256.New()
88+
hasher.Write(payload)
89+
digest := hasher.Sum(nil)
90+
91+
sig, err := dsig.SignDigest(priv, dsig.ECDSAWithP256AndSHA256, digest, nil)
92+
require.NoError(t, err)
93+
94+
require.NoError(t, dsig.VerifyDigest(&priv.PublicKey, dsig.ECDSAWithP256AndSHA256, digest, sig))
95+
})
96+
97+
t.Run("ECDSA via crypto.Signer", func(t *testing.T) {
98+
t.Parallel()
99+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
100+
require.NoError(t, err)
101+
102+
hasher := crypto.SHA256.New()
103+
hasher.Write(payload)
104+
digest := hasher.Sum(nil)
105+
106+
// *ecdsa.PrivateKey implements crypto.Signer but ecdsaGetSignerKey
107+
// optimizes to the non-crypto.Signer path for *ecdsa.PrivateKey.
108+
// This test confirms the end result is the same.
109+
sig, err := dsig.SignDigest(priv, dsig.ECDSAWithP256AndSHA256, digest, nil)
110+
require.NoError(t, err)
111+
112+
require.NoError(t, dsig.VerifyDigest(&priv.PublicKey, dsig.ECDSAWithP256AndSHA256, digest, sig))
113+
})
114+
115+
t.Run("EdDSA returns error", func(t *testing.T) {
116+
t.Parallel()
117+
_, err := dsig.SignDigest(nil, dsig.EdDSA, []byte("digest"), nil)
118+
require.Error(t, err)
119+
})
120+
121+
t.Run("Unknown algorithm returns error", func(t *testing.T) {
122+
t.Parallel()
123+
_, err := dsig.SignDigest(nil, "UNKNOWN_ALG", []byte("digest"), nil)
124+
require.Error(t, err)
125+
})
126+
}

0 commit comments

Comments
 (0)