Skip to content

Commit bfa7d42

Browse files
Alex VaghinFiloSottile
Alex Vaghin
authored andcommitted
acme: support custom crypto.Signer implementations
Currently, only rsa.PrivateKey and ecdsa.PrivateKey are supported when creating JWS signatures. However, it is unnecessarily limiting because any crypto.Signer implementation can sign a digest in the appropriate format. This change uses key.Public() instead of type-asserting the private key which allows for a custom crypto.Signer implementation. For instance, a key stored in a hardware module where the latter does the actual signing without the key ever leaving its boundaries. Change-Id: Ie7930ea2ba8c49dde7107ff074ae34abec05bdb9 Reviewed-on: https://go-review.googlesource.com/c/145137 Run-TryBot: Alex Vaghin <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent 4d3f4d9 commit bfa7d42

File tree

3 files changed

+95
-13
lines changed

3 files changed

+95
-13
lines changed

acme/acme.go

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const (
7777
type Client struct {
7878
// Key is the account key used to register with a CA and sign requests.
7979
// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
80+
//
81+
// The following algorithms are supported:
82+
// RS256, ES256, ES384 and ES512.
83+
// See RFC7518 for more details about the algorithms.
8084
Key crypto.Signer
8185

8286
// HTTPClient optionally specifies an HTTP client to use

acme/jws.go

+16-13
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byt
2525
if err != nil {
2626
return nil, err
2727
}
28-
alg, sha := jwsHasher(key)
28+
alg, sha := jwsHasher(key.Public())
2929
if alg == "" || !sha.Available() {
3030
return nil, ErrUnsupportedKey
3131
}
@@ -97,13 +97,16 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
9797
}
9898

9999
// jwsSign signs the digest using the given key.
100-
// It returns ErrUnsupportedKey if the key type is unknown.
101-
// The hash is used only for RSA keys.
100+
// The hash is unused for ECDSA keys.
101+
//
102+
// Note: non-stdlib crypto.Signer implementations are expected to return
103+
// the signature in the format as specified in RFC7518.
104+
// See https://tools.ietf.org/html/rfc7518 for more details.
102105
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
103-
switch key := key.(type) {
104-
case *rsa.PrivateKey:
105-
return key.Sign(rand.Reader, digest, hash)
106-
case *ecdsa.PrivateKey:
106+
if key, ok := key.(*ecdsa.PrivateKey); ok {
107+
// The key.Sign method of ecdsa returns ASN1-encoded signature.
108+
// So, we use the package Sign function instead
109+
// to get R and S values directly and format the result accordingly.
107110
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
108111
if err != nil {
109112
return nil, err
@@ -118,18 +121,18 @@ func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error)
118121
copy(sig[size*2-len(sb):], sb)
119122
return sig, nil
120123
}
121-
return nil, ErrUnsupportedKey
124+
return key.Sign(rand.Reader, digest, hash)
122125
}
123126

124127
// jwsHasher indicates suitable JWS algorithm name and a hash function
125128
// to use for signing a digest with the provided key.
126129
// It returns ("", 0) if the key is not supported.
127-
func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
128-
switch key := key.(type) {
129-
case *rsa.PrivateKey:
130+
func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
131+
switch pub := pub.(type) {
132+
case *rsa.PublicKey:
130133
return "RS256", crypto.SHA256
131-
case *ecdsa.PrivateKey:
132-
switch key.Params().Name {
134+
case *ecdsa.PublicKey:
135+
switch pub.Params().Name {
133136
case "P-256":
134137
return "ES256", crypto.SHA256
135138
case "P-384":

acme/jws_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package acme
66

77
import (
8+
"crypto"
89
"crypto/ecdsa"
910
"crypto/elliptic"
1011
"crypto/rsa"
@@ -13,6 +14,7 @@ import (
1314
"encoding/json"
1415
"encoding/pem"
1516
"fmt"
17+
"io"
1618
"math/big"
1719
"testing"
1820
)
@@ -241,6 +243,79 @@ func TestJWSEncodeJSONEC(t *testing.T) {
241243
}
242244
}
243245

246+
type customTestSigner struct {
247+
sig []byte
248+
pub crypto.PublicKey
249+
}
250+
251+
func (s *customTestSigner) Public() crypto.PublicKey { return s.pub }
252+
func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {
253+
return s.sig, nil
254+
}
255+
256+
func TestJWSEncodeJSONCustom(t *testing.T) {
257+
claims := struct{ Msg string }{"hello"}
258+
const (
259+
// printf '{"Msg":"hello"}' | base64 | tr -d '=' | tr '/+' '_-'
260+
payload = "eyJNc2ciOiJoZWxsbyJ9"
261+
// printf 'testsig' | base64 | tr -d '='
262+
testsig = "dGVzdHNpZw"
263+
264+
// printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":<testKeyECPubY>,"y":<testKeyECPubY>,"nonce":"nonce"}' | \
265+
// base64 | tr -d '=' | tr '/+' '_-'
266+
es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjVsaEV1" +
267+
"ZzV4SzR4QkRaMm5BYmF4THRhTGl2ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFH" +
268+
"a3YwVGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6Im5vbmNlIn0"
269+
270+
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce"}
271+
rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
272+
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
273+
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
274+
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
275+
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
276+
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
277+
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
278+
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
279+
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
280+
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
281+
"UVEifSwibm9uY2UiOiJub25jZSJ9"
282+
)
283+
284+
tt := []struct {
285+
alg, phead string
286+
pub crypto.PublicKey
287+
}{
288+
{"RS256", rs256phead, testKey.Public()},
289+
{"ES256", es256phead, testKeyEC.Public()},
290+
}
291+
for _, tc := range tt {
292+
tc := tc
293+
t.Run(tc.alg, func(t *testing.T) {
294+
signer := &customTestSigner{
295+
sig: []byte("testsig"),
296+
pub: tc.pub,
297+
}
298+
b, err := jwsEncodeJSON(claims, signer, "nonce")
299+
if err != nil {
300+
t.Fatal(err)
301+
}
302+
var j struct{ Protected, Payload, Signature string }
303+
if err := json.Unmarshal(b, &j); err != nil {
304+
t.Fatal(err)
305+
}
306+
if j.Protected != tc.phead {
307+
t.Errorf("j.Protected = %q\nwant %q", j.Protected, tc.phead)
308+
}
309+
if j.Payload != payload {
310+
t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload)
311+
}
312+
if j.Signature != testsig {
313+
t.Errorf("j.Signature = %q\nwant %q", j.Signature, testsig)
314+
}
315+
})
316+
}
317+
}
318+
244319
func TestJWKThumbprintRSA(t *testing.T) {
245320
// Key example from RFC 7638
246321
const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +

0 commit comments

Comments
 (0)