acme: support for ECDSA keys

Change-Id: I4956b8277969718e70bb4f5f9893213b58e159fc
Reviewed-on: https://go-review.googlesource.com/25462
Reviewed-by: Alex Vaghin <ddos@google.com>
This commit is contained in:
Anmol Sethi
2016-08-05 15:12:09 +00:00
committed by Brad Fitzpatrick
parent 7a1054f3ac
commit e0d166c33c
4 changed files with 158 additions and 48 deletions
+29 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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":"<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("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
View File
@@ -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