From e0d166c33c321d0ff863f459a5882096e334f508 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 4 Aug 2016 00:47:19 -0400 Subject: [PATCH] acme: support for ECDSA keys Change-Id: I4956b8277969718e70bb4f5f9893213b58e159fc Reviewed-on: https://go-review.googlesource.com/25462 Reviewed-by: Alex Vaghin --- acme/internal/acme/acme.go | 36 ++++++++++++++---- acme/internal/acme/jws.go | 67 ++++++++++++++++++++++++++-------- acme/internal/acme/jws_test.go | 63 ++++++++++++++++++++++++++++---- acme/internal/acme/types.go | 40 +++++++++++--------- 4 files changed, 158 insertions(+), 48 deletions(-) diff --git a/acme/internal/acme/acme.go b/acme/internal/acme/acme.go index 9b1a4ea..5e44dfb 100644 --- a/acme/internal/acme/acme.go +++ b/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 diff --git a/acme/internal/acme/jws.go b/acme/internal/acme/jws.go index c277529..d8bb769 100644 --- a/acme/internal/acme/jws.go +++ b/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 } diff --git a/acme/internal/acme/jws_test.go b/acme/internal/acme/jws_test.go index 7afd950..4f7e6ec 100644 --- a/acme/internal/acme/jws_test.go +++ b/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("th = %q; want %q", 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":"","y":""}' | \ + // 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("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) } } diff --git a/acme/internal/acme/types.go b/acme/internal/acme/types.go index 702be79..fb17ed5 100644 --- a/acme/internal/acme/types.go +++ b/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