diff --git a/acme/acme.go b/acme/acme.go index 7df6476412..c6fd998968 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -77,6 +77,10 @@ const ( type Client struct { // Key is the account key used to register with a CA and sign requests. // Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey. + // + // The following algorithms are supported: + // RS256, ES256, ES384 and ES512. + // See RFC7518 for more details about the algorithms. Key crypto.Signer // HTTPClient optionally specifies an HTTP client to use diff --git a/acme/jws.go b/acme/jws.go index 6cbca25de9..1093b50390 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -25,7 +25,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byt if err != nil { return nil, err } - alg, sha := jwsHasher(key) + alg, sha := jwsHasher(key.Public()) if alg == "" || !sha.Available() { return nil, ErrUnsupportedKey } @@ -97,13 +97,16 @@ func jwkEncode(pub crypto.PublicKey) (string, error) { } // jwsSign signs the digest using the given key. -// It returns ErrUnsupportedKey if the key type is unknown. -// The hash is used only for RSA keys. +// The hash is unused for ECDSA keys. +// +// Note: non-stdlib crypto.Signer implementations are expected to return +// the signature in the format as specified in RFC7518. +// See https://tools.ietf.org/html/rfc7518 for more details. func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { - switch key := key.(type) { - case *rsa.PrivateKey: - return key.Sign(rand.Reader, digest, hash) - case *ecdsa.PrivateKey: + if key, ok := key.(*ecdsa.PrivateKey); ok { + // The key.Sign method of ecdsa returns ASN1-encoded signature. + // So, we use the package Sign function instead + // to get R and S values directly and format the result accordingly. r, s, err := ecdsa.Sign(rand.Reader, key, digest) if err != nil { return nil, err @@ -118,18 +121,18 @@ func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) copy(sig[size*2-len(sb):], sb) return sig, nil } - return nil, ErrUnsupportedKey + return key.Sign(rand.Reader, digest, hash) } // jwsHasher indicates suitable JWS algorithm name and a hash function // to use for signing a digest with the provided key. // It returns ("", 0) if the key is not supported. -func jwsHasher(key crypto.Signer) (string, crypto.Hash) { - switch key := key.(type) { - case *rsa.PrivateKey: +func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) { + switch pub := pub.(type) { + case *rsa.PublicKey: return "RS256", crypto.SHA256 - case *ecdsa.PrivateKey: - switch key.Params().Name { + case *ecdsa.PublicKey: + switch pub.Params().Name { case "P-256": return "ES256", crypto.SHA256 case "P-384": diff --git a/acme/jws_test.go b/acme/jws_test.go index 0ff0fb5a31..ee30b1e1e4 100644 --- a/acme/jws_test.go +++ b/acme/jws_test.go @@ -5,6 +5,7 @@ package acme import ( + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" @@ -13,6 +14,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "io" "math/big" "testing" ) @@ -241,6 +243,79 @@ func TestJWSEncodeJSONEC(t *testing.T) { } } +type customTestSigner struct { + sig []byte + pub crypto.PublicKey +} + +func (s *customTestSigner) Public() crypto.PublicKey { return s.pub } +func (s *customTestSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) { + return s.sig, nil +} + +func TestJWSEncodeJSONCustom(t *testing.T) { + claims := struct{ Msg string }{"hello"} + const ( + // printf '{"Msg":"hello"}' | base64 | tr -d '=' | tr '/+' '_-' + payload = "eyJNc2ciOiJoZWxsbyJ9" + // printf 'testsig' | base64 | tr -d '=' + testsig = "dGVzdHNpZw" + + // printf '{"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":,"y":,"nonce":"nonce"}' | \ + // base64 | tr -d '=' | tr '/+' '_-' + es256phead = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IjVsaEV1" + + "ZzV4SzR4QkRaMm5BYmF4THRhTGl2ODVieEo3ZVBkMWRrTzIzSFEiLCJ5IjoiNGFpSzcyc0JlVUFH" + + "a3YwVGFMc213b2tZVVl5TnhHc1M1RU1JS3dzTklLayJ9LCJub25jZSI6Im5vbmNlIn0" + + // {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce"} + rs256phead = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" + + "IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" + + "SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" + + "QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" + + "VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" + + "NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" + + "QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" + + "bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" + + "ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" + + "b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" + + "UVEifSwibm9uY2UiOiJub25jZSJ9" + ) + + tt := []struct { + alg, phead string + pub crypto.PublicKey + }{ + {"RS256", rs256phead, testKey.Public()}, + {"ES256", es256phead, testKeyEC.Public()}, + } + for _, tc := range tt { + tc := tc + t.Run(tc.alg, func(t *testing.T) { + signer := &customTestSigner{ + sig: []byte("testsig"), + pub: tc.pub, + } + b, err := jwsEncodeJSON(claims, signer, "nonce") + if err != nil { + t.Fatal(err) + } + var j struct{ Protected, Payload, Signature string } + if err := json.Unmarshal(b, &j); err != nil { + t.Fatal(err) + } + if j.Protected != tc.phead { + t.Errorf("j.Protected = %q\nwant %q", j.Protected, tc.phead) + } + if j.Payload != payload { + t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload) + } + if j.Signature != testsig { + t.Errorf("j.Signature = %q\nwant %q", j.Signature, testsig) + } + }) + } +} + func TestJWKThumbprintRSA(t *testing.T) { // Key example from RFC 7638 const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +