Ver Fonte

acme: add support for ECDSA-based signatures

A previous change, https://go-review.googlesource.com/25462, added
support only for encoding JWK with a public ECDSA key.
Client users weren't actually able to make any request
using a EC key because of invalid JWS header and signature.

This change adds EC-based header and signature, which makes the Client
to have full support for ECDSA keys now.

Change-Id: Ia88f08465ce941f72d2001019f5aa47441a17d6e
Reviewed-on: https://go-review.googlesource.com/27438
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Alex Vaghin há 9 anos atrás
pai
commit
3461a682e1
2 ficheiros alterados com 131 adições e 5 exclusões
  1. 55 4
      acme/internal/acme/jws.go
  2. 76 1
      acme/internal/acme/jws_test.go

+ 55 - 4
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) {

+ 76 - 1
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 <val> | 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" +