diff --git a/acme/internal/acme/jws.go b/acme/internal/acme/jws.go index d8bb769..49ba313 100644 --- a/acme/internal/acme/jws.go +++ b/acme/internal/acme/jws.go @@ -10,6 +10,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + _ "crypto/sha512" // need for EC keys "encoding/base64" "encoding/json" "fmt" @@ -24,19 +25,24 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byt if err != nil { return nil, err } - phead := fmt.Sprintf(`{"alg":"RS256","jwk":%s,"nonce":%q}`, jwk, nonce) + alg, sha := jwsHasher(key) + if alg == "" || !sha.Available() { + return nil, ErrUnsupportedKey + } + phead := fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q}`, alg, jwk, nonce) phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) cs, err := json.Marshal(claimset) if err != nil { return nil, err } payload := base64.RawURLEncoding.EncodeToString(cs) - h := sha256.New() - h.Write([]byte(phead + "." + payload)) - sig, err := key.Sign(rand.Reader, h.Sum(nil), crypto.SHA256) + hash := sha.New() + hash.Write([]byte(phead + "." + payload)) + sig, err := jwsSign(key, sha, hash.Sum(nil)) if err != nil { return nil, err } + enc := struct { Protected string `json:"protected"` Payload string `json:"payload"` @@ -90,6 +96,51 @@ func jwkEncode(pub crypto.PublicKey) (string, error) { return "", ErrUnsupportedKey } +// 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. +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: + r, s, err := ecdsa.Sign(rand.Reader, key, digest) + if err != nil { + return nil, err + } + rb, sb := r.Bytes(), s.Bytes() + size := key.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return nil, ErrUnsupportedKey +} + +// 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: + return "RS256", crypto.SHA256 + case *ecdsa.PrivateKey: + switch key.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-512": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + // JWKThumbprint creates a JWK thumbprint out of pub // as specified in https://tools.ietf.org/html/rfc7638. func JWKThumbprint(pub crypto.PublicKey) (string, error) { diff --git a/acme/internal/acme/jws_test.go b/acme/internal/acme/jws_test.go index 4f7e6ec..ed63858 100644 --- a/acme/internal/acme/jws_test.go +++ b/acme/internal/acme/jws_test.go @@ -49,7 +49,26 @@ EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO // This thumbprint is for the testKey defined above. const testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ" -var testKey *rsa.PrivateKey +const ( + // openssl ecparam -name secp256k1 -genkey -noout + testKeyECPEM = ` +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49 +AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5 +QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ== +-----END EC PRIVATE KEY----- +` + // 1. opnessl ec -in key.pem -noout -text + // 2. remove first byte, 04 (the header); the rest is X and Y + // 3. covert each with: echo | xxd -r -p | base64 | tr -d '=' | tr '/+' '_-' + testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ" + testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk" +) + +var ( + testKey *rsa.PrivateKey + testKeyEC *ecdsa.PrivateKey +) func init() { d, _ := pem.Decode([]byte(testKeyPEM)) @@ -61,6 +80,14 @@ func init() { if err != nil { panic(err.Error()) } + + if d, _ = pem.Decode([]byte(testKeyECPEM)); d == nil { + panic("no block found in testKeyECPEM") + } + testKeyEC, err = x509.ParseECPrivateKey(d.Bytes) + if err != nil { + panic(err.Error()) + } } func TestJWSEncodeJSON(t *testing.T) { @@ -110,6 +137,54 @@ func TestJWSEncodeJSON(t *testing.T) { } } +func TestJWSEncodeJSONEC(t *testing.T) { + claims := struct{ Msg string }{"Hello JWS"} + + b, err := jwsEncodeJSON(claims, testKeyEC, "nonce") + if err != nil { + t.Fatal(err) + } + var jws struct{ Protected, Payload, Signature string } + if err := json.Unmarshal(b, &jws); err != nil { + t.Fatal(err) + } + + if b, err = base64.RawURLEncoding.DecodeString(jws.Protected); err != nil { + t.Fatalf("jws.Protected: %v", err) + } + var head struct { + Alg string + Nonce string + JWK struct { + Crv string + Kty string + X string + Y string + } `json:"jwk"` + } + if err := json.Unmarshal(b, &head); err != nil { + t.Fatalf("jws.Protected: %v", err) + } + if head.Alg != "ES256" { + t.Errorf("head.Alg = %q; want ES256", head.Alg) + } + if head.Nonce != "nonce" { + t.Errorf("head.Nonce = %q; want nonce", head.Nonce) + } + if head.JWK.Crv != "P-256" { + t.Errorf("head.JWK.Crv = %q; want P-256", head.JWK.Crv) + } + if head.JWK.Kty != "EC" { + t.Errorf("head.JWK.Kty = %q; want EC", head.JWK.Kty) + } + if head.JWK.X != testKeyECPubX { + t.Errorf("head.JWK.X = %q; want %q", head.JWK.X, testKeyECPubX) + } + if head.JWK.Y != testKeyECPubY { + t.Errorf("head.JWK.Y = %q; want %q", head.JWK.Y, testKeyECPubY) + } +} + func TestJWKThumbprintRSA(t *testing.T) { // Key example from RFC 7638 const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +