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:
committed by
Brad Fitzpatrick
parent
7a1054f3ac
commit
e0d166c33c
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user