From 0e26bb45ca14fb6befc3cbe710b90c70ebd0f0c4 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 27 Jan 2016 02:01:39 +0100 Subject: [PATCH] Add support for EC certificates / account keys --- account.go | 16 +++++++------- acme/client.go | 23 +++++++++----------- acme/crypto.go | 44 +++++++++++++++++++++++++++++---------- acme/dns_challenge.go | 2 +- acme/http_challenge.go | 2 +- acme/jws.go | 20 +++++++++++++++--- acme/messages.go | 18 +++++++--------- acme/tls_sni_challenge.go | 4 ++-- cli.go | 8 +++---- cli_handlers.go | 7 ++++++- configuration.go | 20 +++++++++++++++--- crypto.go | 29 ++++++++++++++++++++------ 12 files changed, 130 insertions(+), 63 deletions(-) diff --git a/account.go b/account.go index 13325470..85ac09f1 100644 --- a/account.go +++ b/account.go @@ -1,7 +1,7 @@ package main import ( - "crypto/rsa" + "crypto" "encoding/json" "io/ioutil" "os" @@ -13,7 +13,7 @@ import ( // Account represents a users local saved credentials type Account struct { Email string `json:"email"` - key *rsa.PrivateKey + key crypto.PrivateKey Registration *acme.RegistrationResource `json:"registration"` conf *Configuration @@ -28,16 +28,18 @@ func NewAccount(email string, conf *Configuration) *Account { logger().Fatalf("Could not check/create directory for account %s: %v", email, err) } - var privKey *rsa.PrivateKey + var privKey crypto.PrivateKey if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - logger().Printf("No key found for account %s. Generating a %v bit key.", email, conf.RsaBits()) - privKey, err = generateRsaKey(conf.RsaBits(), accKeyPath) + + logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email) + privKey, err = generatePrivateKey(accKeyPath) if err != nil { logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err) } + logger().Printf("Saved key to %s", accKeyPath) } else { - privKey, err = loadRsaKey(accKeyPath) + privKey, err = loadPrivateKey(accKeyPath) if err != nil { logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) } @@ -73,7 +75,7 @@ func (a *Account) GetEmail() string { } // GetPrivateKey returns the private RSA account key. -func (a *Account) GetPrivateKey() *rsa.PrivateKey { +func (a *Account) GetPrivateKey() crypto.PrivateKey { return a.key } diff --git a/acme/client.go b/acme/client.go index be9843e2..769b17e0 100644 --- a/acme/client.go +++ b/acme/client.go @@ -3,7 +3,6 @@ package acme import ( "crypto" - "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" @@ -38,7 +37,7 @@ func logf(format string, args ...interface{}) { type User interface { GetEmail() string GetRegistration() *RegistrationResource - GetPrivateKey() *rsa.PrivateKey + GetPrivateKey() crypto.PrivateKey } // Interface for all challenge solvers to implement. @@ -53,7 +52,7 @@ type Client struct { directory directory user User jws *jws - keyBits int + keyType KeyType issuerCert []byte solvers map[Challenge]solver } @@ -61,16 +60,12 @@ type Client struct { // NewClient creates a new ACME client on behalf of the user. The client will depend on // the ACME directory located at caDirURL for the rest of its actions. It will // generate private keys for certificates of size keyBits. -func NewClient(caDirURL string, user User, keyBits int) (*Client, error) { +func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { privKey := user.GetPrivateKey() if privKey == nil { return nil, errors.New("private key was nil") } - if err := privKey.Validate(); err != nil { - return nil, fmt.Errorf("invalid private key: %v", err) - } - var dir directory if _, err := getJSON(caDirURL, &dir); err != nil { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) @@ -98,7 +93,7 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) { solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} - return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil + return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } // SetChallengeProvider specifies a custom provider that will make the solution available @@ -197,8 +192,10 @@ func (c *Client) Register() (*RegistrationResource, error) { // AgreeToTOS updates the Client registration and sends the agreement to // the server. func (c *Client) AgreeToTOS() error { - c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL - c.user.GetRegistration().Body.Resource = "reg" + reg := c.user.GetRegistration() + + reg.Body.Agreement = c.user.GetRegistration().TosURL + reg.Body.Resource = "reg" _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) return err } @@ -457,7 +454,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, commonName := authz[0] var err error if privKey == nil { - privKey, err = generatePrivateKey(rsakey, c.keyBits) + privKey, err = generatePrivateKey(c.keyType) if err != nil { return CertificateResource{}, err } @@ -471,7 +468,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san) + csr, err := generateCsr(privKey, commonName.Domain, san) if err != nil { return CertificateResource{}, err } diff --git a/acme/crypto.go b/acme/crypto.go index 5ce5ea5f..78a87847 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -25,12 +25,16 @@ import ( "golang.org/x/crypto/sha3" ) -type keyType int +// KeyType represents the key algo as well as the key size or curve to use. +type KeyType string type derCertificateBytes []byte const ( - eckey keyType = iota - rsakey + EC256 = KeyType("P256") + EC384 = KeyType("P348") + RSA2048 = KeyType("2048") + RSA4096 = KeyType("4096") + RSA8192 = KeyType("8192") ) const ( @@ -123,8 +127,16 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { } func getKeyAuthorization(token string, key interface{}) (string, error) { + var publicKey crypto.PublicKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + publicKey = k.Public() + case *rsa.PrivateKey: + publicKey = k.Public() + } + // Generate the Key Authorization for the challenge - jwk := keyAsJWK(key) + jwk := keyAsJWK(publicKey) if jwk == nil { return "", errors.New("Could not generate JWK from key.") } @@ -217,18 +229,25 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { } } -func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) { - switch t { - case eckey: +func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { + + switch keyType { + case EC256: + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case EC384: return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case rsakey: - return rsa.GenerateKey(rand.Reader, keyLength) + case RSA2048: + return rsa.GenerateKey(rand.Reader, 2048) + case RSA4096: + return rsa.GenerateKey(rand.Reader, 4096) + case RSA8192: + return rsa.GenerateKey(rand.Reader, 8192) } - return nil, fmt.Errorf("Invalid keytype: %d", t) + return nil, fmt.Errorf("Invalid KeyType: %s", keyType) } -func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byte, error) { +func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, @@ -245,6 +264,9 @@ func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byt func pemEncode(data interface{}) []byte { var pemBlock *pem.Block switch key := data.(type) { + case *ecdsa.PrivateKey: + keyBytes, _ := x509.MarshalECPrivateKey(key) + pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} case *rsa.PrivateKey: pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} break diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 4cd58f50..d187f63b 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -45,7 +45,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { } // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) if err != nil { return err } diff --git a/acme/http_challenge.go b/acme/http_challenge.go index 1cc1f6e1..95cb1fd8 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -21,7 +21,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) if err != nil { return err } diff --git a/acme/jws.go b/acme/jws.go index b676fe39..78d82724 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -2,7 +2,9 @@ package acme import ( "bytes" + "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "fmt" "net/http" @@ -12,7 +14,7 @@ import ( type jws struct { directoryURL string - privKey *rsa.PrivateKey + privKey crypto.PrivateKey nonces []string } @@ -46,8 +48,20 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) { } func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { - // TODO: support other algorithms - RS512 - signer, err := jose.NewSigner(jose.RS256, j.privKey) + + var alg jose.SignatureAlgorithm + switch k := j.privKey.(type) { + case *rsa.PrivateKey: + alg = jose.RS256 + case *ecdsa.PrivateKey: + if k.Curve == elliptic.P256() { + alg = jose.ES256 + } else if k.Curve == elliptic.P384() { + alg = jose.ES384 + } + } + + signer, err := jose.NewSigner(alg, j.privKey) if err != nil { return nil, err } diff --git a/acme/messages.go b/acme/messages.go index 55e54321..d238df81 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -28,17 +28,13 @@ type registrationMessage struct { // Registration is returned by the ACME server after the registration // The client implementation should save this registration somewhere. type Registration struct { - Resource string `json:"resource,omitempty"` - ID int `json:"id"` - Key struct { - Kty string `json:"kty"` - N string `json:"n"` - E string `json:"e"` - } `json:"key"` - Contact []string `json:"contact"` - Agreement string `json:"agreement,omitempty"` - Authorizations string `json:"authorizations,omitempty"` - Certificates string `json:"certificates,omitempty"` + Resource string `json:"resource,omitempty"` + ID int `json:"id"` + Key jose.JsonWebKey `json:"key"` + Contact []string `json:"contact"` + Agreement string `json:"agreement,omitempty"` + Authorizations string `json:"authorizations,omitempty"` + Certificates string `json:"certificates,omitempty"` // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"` } diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go index dca886bd..c36f6acc 100644 --- a/acme/tls_sni_challenge.go +++ b/acme/tls_sni_challenge.go @@ -22,7 +22,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, &t.jws.privKey.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) if err != nil { return err } @@ -43,7 +43,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { // TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) { // generate a new RSA key for the certificates - tempPrivKey, err := generatePrivateKey(rsakey, 2048) + tempPrivKey, err := generatePrivateKey(RSA2048) if err != nil { return tls.Certificate{}, err } diff --git a/cli.go b/cli.go index 97d3a816..893d1cda 100644 --- a/cli.go +++ b/cli.go @@ -92,10 +92,10 @@ func main() { Name: "accept-tos, a", Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", }, - cli.IntFlag{ - Name: "rsa-key-size, B", - Value: 2048, - Usage: "Size of the RSA key.", + cli.StringFlag{ + Name: "key-type, k", + Value: "rsa2048", + Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384", }, cli.StringFlag{ Name: "path", diff --git a/cli_handlers.go b/cli_handlers.go index e6e71cbe..cdf4dca8 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -34,7 +34,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { //TODO: move to account struct? Currently MUST pass email. acc := NewAccount(c.GlobalString("email"), conf) - client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits()) + keyType, err := conf.KeyType() + if err != nil { + logger().Fatal(err.Error()) + } + + client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) if err != nil { logger().Fatalf("Could not create client: %s", err.Error()) } diff --git a/configuration.go b/configuration.go index cbb157f3..a437fd56 100644 --- a/configuration.go +++ b/configuration.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/url" "os" "path" @@ -20,9 +21,22 @@ func NewConfiguration(c *cli.Context) *Configuration { return &Configuration{context: c} } -// RsaBits returns the current set RSA bit length for private keys -func (c *Configuration) RsaBits() int { - return c.context.GlobalInt("rsa-key-size") +// KeyType the type from which private keys should be generated +func (c *Configuration) KeyType() (acme.KeyType, error) { + switch strings.ToUpper(c.context.GlobalString("key-type")) { + case "RSA2048": + return acme.RSA2048, nil + case "RSA4096": + return acme.RSA4096, nil + case "RSA8192": + return acme.RSA8192, nil + case "EC256": + return acme.EC256, nil + case "EC384": + return acme.EC384, nil + } + + return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type")) } // ExcludedSolvers is a list of solvers that are to be excluded. diff --git a/crypto.go b/crypto.go index 3644ed99..684b1100 100644 --- a/crypto.go +++ b/crypto.go @@ -1,21 +1,30 @@ package main import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "io/ioutil" "os" ) -func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, length) +func generatePrivateKey(file string) (crypto.PrivateKey, error) { + + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return nil, err } - pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} certOut, err := os.Create(file) if err != nil { @@ -28,12 +37,20 @@ func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { return privateKey, nil } -func loadRsaKey(file string) (*rsa.PrivateKey, error) { +func loadPrivateKey(file string) (crypto.PrivateKey, error) { keyBytes, err := ioutil.ReadFile(file) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + } + + return nil, errors.New("Unknown private key type.") }