Skip to content

Commit 27a35f3

Browse files
authored
Merge pull request #13 from lestrrat-go/feat-ecdsa-der
add SignECDSADER and VerifyECDSADER helpers
2 parents 5d95498 + 947c1e5 commit 27a35f3

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

Changes

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

4+
v1.2.2 UNRELEASED
5+
* Add `SignECDSADER()` and `VerifyECDSADER()` primitive helpers for ECDSA
6+
signatures in ASN.1 DER-encoded `Ecdsa-Sig-Value` form (RFC 3279 §2.2.3),
7+
as used by X.509/PKIX and composite signature schemes. The existing
8+
`SignECDSA()`/`VerifyECDSA()` functions remain the canonical entry points
9+
for the JWS-native fixed-length r||s format (RFC 7515 §3.4).
10+
411
v1.2.1 7 Apr 2026
512
* Add `SignDigest()` for signing pre-computed digests. Supported for HMAC,
613
RSA (PKCS1v15 and PSS), and ECDSA families. EdDSA and Custom return an error.

ecdsa.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,47 @@ func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reade
124124
return PackECDSASignature(r, s, key.Curve.Params().BitSize)
125125
}
126126

127+
// SignECDSADER generates an ECDSA signature in ASN.1 DER-encoded Ecdsa-Sig-Value
128+
// format (RFC 3279 §2.2.3), as required by X.509/PKIX and composite signature
129+
// schemes such as draft-ietf-lamps-pq-composite-sigs. For the fixed-length
130+
// JWS r||s format (RFC 7515 §3.4), use SignECDSA instead.
131+
//
132+
// The payload is hashed with h before signing. rr provides randomness; if nil,
133+
// rand.Reader is used.
134+
func SignECDSADER(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
135+
if !isValidECDSAKey(key) {
136+
return nil, fmt.Errorf(`invalid key type %T for ECDSA algorithm`, key)
137+
}
138+
hh := h.New()
139+
if _, err := hh.Write(payload); err != nil {
140+
return nil, fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
141+
}
142+
digest := hh.Sum(nil)
143+
144+
if rr == nil {
145+
rr = rand.Reader
146+
}
147+
148+
sig, err := ecdsa.SignASN1(rr, key, digest)
149+
if err != nil {
150+
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
151+
}
152+
return sig, nil
153+
}
154+
155+
// VerifyECDSADER verifies an ECDSA signature in ASN.1 DER-encoded
156+
// Ecdsa-Sig-Value format. See SignECDSADER for the format distinction. The
157+
// payload is hashed with h before verification.
158+
func VerifyECDSADER(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
159+
hh := h.New()
160+
hh.Write(payload)
161+
digest := hh.Sum(nil)
162+
if !ecdsa.VerifyASN1(key, digest, signature) {
163+
return NewVerificationError("invalid ECDSA signature")
164+
}
165+
return nil
166+
}
167+
127168
// SignECDSACryptoSigner generates an ECDSA signature using a crypto.Signer interface.
128169
// This function works with hardware security modules and other crypto.Signer implementations.
129170
// The signature is converted from ASN.1 format to JWS format (r||s).

ecdsa_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dsig_test
2+
3+
import (
4+
"crypto"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/sha256"
9+
"crypto/sha512"
10+
"hash"
11+
"testing"
12+
13+
"github.com/lestrrat-go/dsig"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestECDSADER(t *testing.T) {
18+
t.Parallel()
19+
20+
tests := []struct {
21+
name string
22+
curve elliptic.Curve
23+
h crypto.Hash
24+
hfunc func() hash.Hash
25+
}{
26+
{"P256/SHA256", elliptic.P256(), crypto.SHA256, sha256.New},
27+
{"P384/SHA384", elliptic.P384(), crypto.SHA384, sha512.New384},
28+
{"P521/SHA512", elliptic.P521(), crypto.SHA512, sha512.New},
29+
}
30+
31+
payload := []byte("the quick brown fox jumps over the lazy dog")
32+
33+
for _, tc := range tests {
34+
t.Run(tc.name, func(t *testing.T) {
35+
t.Parallel()
36+
37+
key, err := ecdsa.GenerateKey(tc.curve, rand.Reader)
38+
require.NoError(t, err, "generate key")
39+
40+
digest := func() []byte {
41+
h := tc.hfunc()
42+
h.Write(payload)
43+
return h.Sum(nil)
44+
}
45+
46+
t.Run("round trip", func(t *testing.T) {
47+
sig, err := dsig.SignECDSADER(key, payload, tc.h, nil)
48+
require.NoError(t, err, "SignECDSADER")
49+
require.NoError(t, dsig.VerifyECDSADER(&key.PublicKey, payload, sig, tc.h), "VerifyECDSADER")
50+
})
51+
52+
t.Run("dsig sign, stdlib verify", func(t *testing.T) {
53+
sig, err := dsig.SignECDSADER(key, payload, tc.h, nil)
54+
require.NoError(t, err, "SignECDSADER")
55+
require.True(t, ecdsa.VerifyASN1(&key.PublicKey, digest(), sig), "stdlib VerifyASN1 must accept dsig DER output")
56+
})
57+
58+
t.Run("stdlib sign, dsig verify", func(t *testing.T) {
59+
sig, err := ecdsa.SignASN1(rand.Reader, key, digest())
60+
require.NoError(t, err, "ecdsa.SignASN1")
61+
require.NoError(t, dsig.VerifyECDSADER(&key.PublicKey, payload, sig, tc.h), "VerifyECDSADER must accept stdlib DER output")
62+
})
63+
64+
t.Run("format isolation", func(t *testing.T) {
65+
derSig, err := dsig.SignECDSADER(key, payload, tc.h, nil)
66+
require.NoError(t, err, "SignECDSADER")
67+
jwsSig, err := dsig.SignECDSA(key, payload, tc.h, nil)
68+
require.NoError(t, err, "SignECDSA")
69+
70+
require.Error(t, dsig.VerifyECDSA(&key.PublicKey, payload, derSig, tc.h), "JWS verifier must reject DER signature")
71+
require.Error(t, dsig.VerifyECDSADER(&key.PublicKey, payload, jwsSig, tc.h), "DER verifier must reject JWS signature")
72+
})
73+
74+
t.Run("tamper detection", func(t *testing.T) {
75+
sig, err := dsig.SignECDSADER(key, payload, tc.h, nil)
76+
require.NoError(t, err, "SignECDSADER")
77+
tampered := make([]byte, len(sig))
78+
copy(tampered, sig)
79+
tampered[len(tampered)/2] ^= 0xFF
80+
require.Error(t, dsig.VerifyECDSADER(&key.PublicKey, payload, tampered, tc.h), "tampered signature must fail")
81+
})
82+
83+
t.Run("wrong key", func(t *testing.T) {
84+
sig, err := dsig.SignECDSADER(key, payload, tc.h, nil)
85+
require.NoError(t, err, "SignECDSADER")
86+
other, err := ecdsa.GenerateKey(tc.curve, rand.Reader)
87+
require.NoError(t, err, "generate other key")
88+
require.Error(t, dsig.VerifyECDSADER(&other.PublicKey, payload, sig, tc.h), "wrong key must fail")
89+
})
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)