Quellcode durchsuchen

acme: support for ECDSA keys

Change-Id: I4956b8277969718e70bb4f5f9893213b58e159fc
Reviewed-on: https://go-review.googlesource.com/25462
Reviewed-by: Alex Vaghin <ddos@google.com>
Anmol Sethi vor 9 Jahren
Ursprung
Commit
e0d166c33c
4 geänderte Dateien mit 158 neuen und 48 gelöschten Zeilen
  1. 29 7
      acme/internal/acme/acme.go
  2. 51 16
      acme/internal/acme/jws.go
  3. 56 7
      acme/internal/acme/jws_test.go
  4. 22 18
      acme/internal/acme/types.go

+ 29 - 7
acme/internal/acme/acme.go

@@ -11,6 +11,7 @@ package acme
 
 import (
 	"bytes"
+	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha256"
@@ -52,7 +53,8 @@ type Client struct {
 	HTTPClient *http.Client
 
 	// Key is the account key used to register with a CA and sign requests.
-	Key *rsa.PrivateKey
+	// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
+	Key crypto.Signer
 
 	// DirectoryURL points to the CA directory endpoint.
 	// If empty, LetsEncryptURL is used.
@@ -318,6 +320,11 @@ func (c *Client) GetChallenge(url string) (*Challenge, error) {
 //
 // The server will then perform the validation asynchronously.
 func (c *Client) Accept(chal *Challenge) (*Challenge, error) {
+	auth, err := keyAuth(c.Key.Public(), chal.Token)
+	if err != nil {
+		return nil, err
+	}
+
 	req := struct {
 		Resource string `json:"resource"`
 		Type     string `json:"type"`
@@ -325,7 +332,7 @@ func (c *Client) Accept(chal *Challenge) (*Challenge, error) {
 	}{
 		Resource: "challenge",
 		Type:     chal.Type,
-		Auth:     keyAuth(&c.Key.PublicKey, chal.Token),
+		Auth:     auth,
 	}
 	res, err := c.postJWS(chal.URI, req)
 	if err != nil {
@@ -354,7 +361,12 @@ func (c *Client) HTTP01Handler(token string) http.Handler {
 			return
 		}
 		w.Header().Set("content-type", "text/plain")
-		w.Write([]byte(keyAuth(&c.Key.PublicKey, token)))
+		auth, err := keyAuth(c.Key.Public(), token)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Write([]byte(auth))
 	})
 }
 
@@ -370,7 +382,10 @@ func (c *Client) HTTP01Handler(token string) http.Handler {
 // The token argument is a Challenge.Token value.
 // The returned certificate is valid for the next 24 hours.
 func (c *Client) TLSSNI01ChallengeCert(token string) (tls.Certificate, error) {
-	ka := keyAuth(&c.Key.PublicKey, token)
+	ka, err := keyAuth(c.Key.Public(), token)
+	if err != nil {
+		return tls.Certificate{}, nil
+	}
 	b := sha256.Sum256([]byte(ka))
 	h := hex.EncodeToString(b[:])
 	name := fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
@@ -389,7 +404,10 @@ func (c *Client) TLSSNI02ChallengeCert(token string) (tls.Certificate, error) {
 	h := hex.EncodeToString(b[:])
 	sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
 
-	ka := keyAuth(&c.Key.PublicKey, token)
+	ka, err := keyAuth(c.Key.Public(), token)
+	if err != nil {
+		return tls.Certificate{}, nil
+	}
 	b = sha256.Sum256([]byte(ka))
 	h = hex.EncodeToString(b[:])
 	sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
@@ -573,8 +591,12 @@ func retryAfter(v string) (time.Duration, error) {
 }
 
 // keyAuth generates a key authorization string for a given token.
-func keyAuth(pub *rsa.PublicKey, token string) string {
-	return fmt.Sprintf("%s.%s", token, JWKThumbprint(pub))
+func keyAuth(pub crypto.PublicKey, token string) (string, error) {
+	th, err := JWKThumbprint(pub)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%s.%s", token, th), nil
 }
 
 // tlsChallengeCert creates a temporary certificate for TLS-SNI challenges

+ 51 - 16
acme/internal/acme/jws.go

@@ -6,6 +6,7 @@ package acme
 
 import (
 	"crypto"
+	"crypto/ecdsa"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/sha256"
@@ -18,8 +19,11 @@ import (
 // jwsEncodeJSON signs claimset using provided key and a nonce.
 // The result is serialized in JSON format.
 // See https://tools.ietf.org/html/rfc7515#section-7.
-func jwsEncodeJSON(claimset interface{}, key *rsa.PrivateKey, nonce string) ([]byte, error) {
-	jwk := jwkEncode(&key.PublicKey)
+func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byte, error) {
+	jwk, err := jwkEncode(key.Public())
+	if err != nil {
+		return nil, err
+	}
 	phead := fmt.Sprintf(`{"alg":"RS256","jwk":%s,"nonce":%q}`, jwk, nonce)
 	phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
 	cs, err := json.Marshal(claimset)
@@ -29,7 +33,7 @@ func jwsEncodeJSON(claimset interface{}, key *rsa.PrivateKey, nonce string) ([]b
 	payload := base64.RawURLEncoding.EncodeToString(cs)
 	h := sha256.New()
 	h.Write([]byte(phead + "." + payload))
-	sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
+	sig, err := key.Sign(rand.Reader, h.Sum(nil), crypto.SHA256)
 	if err != nil {
 		return nil, err
 	}
@@ -45,23 +49,54 @@ func jwsEncodeJSON(claimset interface{}, key *rsa.PrivateKey, nonce string) ([]b
 	return json.Marshal(&enc)
 }
 
-// jwkEncode encodes public part of an RSA key into a JWK.
+// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
 // The result is also suitable for creating a JWK thumbprint.
-func jwkEncode(pub *rsa.PublicKey) string {
-	n := pub.N
-	e := big.NewInt(int64(pub.E))
-	// fields order is important
-	// see https://tools.ietf.org/html/rfc7638#section-3.3 for details
-	return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
-		base64.RawURLEncoding.EncodeToString(e.Bytes()),
-		base64.RawURLEncoding.EncodeToString(n.Bytes()),
-	)
+// https://tools.ietf.org/html/rfc7517
+func jwkEncode(pub crypto.PublicKey) (string, error) {
+	switch pub := pub.(type) {
+	case *rsa.PublicKey:
+		// https://tools.ietf.org/html/rfc7518#section-6.3.1
+		n := pub.N
+		e := big.NewInt(int64(pub.E))
+		// Field order is important.
+		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
+		return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
+			base64.RawURLEncoding.EncodeToString(e.Bytes()),
+			base64.RawURLEncoding.EncodeToString(n.Bytes()),
+		), nil
+	case *ecdsa.PublicKey:
+		// https://tools.ietf.org/html/rfc7518#section-6.2.1
+		p := pub.Curve.Params()
+		n := p.BitSize / 8
+		if p.BitSize%8 != 0 {
+			n++
+		}
+		x := pub.X.Bytes()
+		if n > len(x) {
+			x = append(make([]byte, n-len(x)), x...)
+		}
+		y := pub.Y.Bytes()
+		if n > len(y) {
+			y = append(make([]byte, n-len(y)), y...)
+		}
+		// Field order is important.
+		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
+		return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
+			p.Name,
+			base64.RawURLEncoding.EncodeToString(x),
+			base64.RawURLEncoding.EncodeToString(y),
+		), nil
+	}
+	return "", ErrUnsupportedKey
 }
 
 // JWKThumbprint creates a JWK thumbprint out of pub
 // as specified in https://tools.ietf.org/html/rfc7638.
-func JWKThumbprint(pub *rsa.PublicKey) string {
-	jwk := jwkEncode(pub)
+func JWKThumbprint(pub crypto.PublicKey) (string, error) {
+	jwk, err := jwkEncode(pub)
+	if err != nil {
+		return "", err
+	}
 	b := sha256.Sum256([]byte(jwk))
-	return base64.RawURLEncoding.EncodeToString(b[:])
+	return base64.RawURLEncoding.EncodeToString(b[:]), nil
 }

+ 56 - 7
acme/internal/acme/jws_test.go

@@ -5,6 +5,8 @@
 package acme
 
 import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
 	"crypto/rsa"
 	"crypto/x509"
 	"encoding/base64"
@@ -108,7 +110,7 @@ func TestJWSEncodeJSON(t *testing.T) {
 	}
 }
 
-func TestJWKThumbprint(t *testing.T) {
+func TestJWKThumbprintRSA(t *testing.T) {
 	// Key example from RFC 7638
 	const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +
 		"VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" +
@@ -119,21 +121,68 @@ func TestJWKThumbprint(t *testing.T) {
 	const base64E = "AQAB"
 	const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
 
-	bytes, err := base64.RawURLEncoding.DecodeString(base64N)
+	b, err := base64.RawURLEncoding.DecodeString(base64N)
 	if err != nil {
 		t.Fatalf("Error parsing example key N: %v", err)
 	}
-	n := new(big.Int).SetBytes(bytes)
+	n := new(big.Int).SetBytes(b)
 
-	bytes, err = base64.RawURLEncoding.DecodeString(base64E)
+	b, err = base64.RawURLEncoding.DecodeString(base64E)
 	if err != nil {
 		t.Fatalf("Error parsing example key E: %v", err)
 	}
-	e := new(big.Int).SetBytes(bytes)
+	e := new(big.Int).SetBytes(b)
 
 	pub := &rsa.PublicKey{N: n, E: int(e.Uint64())}
-	th := JWKThumbprint(pub)
+	th, err := JWKThumbprint(pub)
+	if err != nil {
+		t.Error(err)
+	}
+	if th != expected {
+		t.Errorf("thumbprint = %q; want %q", th, expected)
+	}
+}
+
+func TestJWKThumbprintEC(t *testing.T) {
+	// Key example from RFC 7520
+	// expected was computed with
+	// echo -n '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
+	// openssl dgst -binary -sha256 | \
+	// base64 | \
+	// tr -d '=' | tr '/+' '_-'
+	const (
+		base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
+			"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"
+		base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" +
+			"QkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
+		expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M"
+	)
+
+	b, err := base64.RawURLEncoding.DecodeString(base64X)
+	if err != nil {
+		t.Fatalf("Error parsing example key X: %v", err)
+	}
+	x := new(big.Int).SetBytes(b)
+
+	b, err = base64.RawURLEncoding.DecodeString(base64Y)
+	if err != nil {
+		t.Fatalf("Error parsing example key Y: %v", err)
+	}
+	y := new(big.Int).SetBytes(b)
+
+	pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}
+	th, err := JWKThumbprint(pub)
+	if err != nil {
+		t.Error(err)
+	}
 	if th != expected {
-		t.Errorf("th = %q; want %q", th, expected)
+		t.Errorf("thumbprint = %q; want %q", th, expected)
+	}
+}
+
+func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
+	_, err := JWKThumbprint(struct{}{})
+	if err != ErrUnsupportedKey {
+		t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
 	}
 }

+ 22 - 18
acme/internal/acme/types.go

@@ -1,6 +1,7 @@
 package acme
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 )
@@ -15,6 +16,27 @@ const (
 	StatusRevoked    = "revoked"
 )
 
+// ErrUnsupportedKey is returned when an unsupported key type is encountered.
+var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
+
+// Error is an ACME error, defined in Problem Details for HTTP APIs doc
+// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
+type Error struct {
+	// StatusCode is The HTTP status code generated by the origin server.
+	StatusCode int
+	// ProblemType is a URI reference that identifies the problem type,
+	// typically in a "urn:acme:error:xxx" form.
+	ProblemType string
+	// Detail is a human-readable explanation specific to this occurrence of the problem.
+	Detail string
+	// Header is the original server error response headers.
+	Header http.Header
+}
+
+func (e *Error) Error() string {
+	return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
+}
+
 // Account is a user account. It is associated with a private key.
 type Account struct {
 	// URI is the account unique ID, which is also a URL used to retrieve
@@ -117,24 +139,6 @@ type AuthzID struct {
 	Value string // The identifier itself, e.g. "example.org".
 }
 
-// Error is an ACME error, defined in Problem Details for HTTP APIs doc
-// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
-type Error struct {
-	// StatusCode is The HTTP status code generated by the origin server.
-	StatusCode int
-	// ProblemType is a URI reference that identifies the problem type,
-	// typically in a "urn:acme:error:xxx" form.
-	ProblemType string
-	// Detail is a human-readable explanation specific to this occurrence of the problem.
-	Detail string
-	// Header is the original server error response headers.
-	Header http.Header
-}
-
-func (e *Error) Error() string {
-	return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
-}
-
 // wireAuthz is ACME JSON representation of Authorization objects.
 type wireAuthz struct {
 	Status       string