Add support for EC certificates / account keys

This commit is contained in:
xenolf 2016-01-27 02:01:39 +01:00
parent f203a8e336
commit 0e26bb45ca
12 changed files with 130 additions and 63 deletions

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"crypto/rsa" "crypto"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"os" "os"
@ -13,7 +13,7 @@ import (
// Account represents a users local saved credentials // Account represents a users local saved credentials
type Account struct { type Account struct {
Email string `json:"email"` Email string `json:"email"`
key *rsa.PrivateKey key crypto.PrivateKey
Registration *acme.RegistrationResource `json:"registration"` Registration *acme.RegistrationResource `json:"registration"`
conf *Configuration 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) 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) { 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 { if err != nil {
logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err) logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
} }
logger().Printf("Saved key to %s", accKeyPath) logger().Printf("Saved key to %s", accKeyPath)
} else { } else {
privKey, err = loadRsaKey(accKeyPath) privKey, err = loadPrivateKey(accKeyPath)
if err != nil { if err != nil {
logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) 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. // GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() *rsa.PrivateKey { func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.key return a.key
} }

View file

@ -3,7 +3,6 @@ package acme
import ( import (
"crypto" "crypto"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@ -38,7 +37,7 @@ func logf(format string, args ...interface{}) {
type User interface { type User interface {
GetEmail() string GetEmail() string
GetRegistration() *RegistrationResource GetRegistration() *RegistrationResource
GetPrivateKey() *rsa.PrivateKey GetPrivateKey() crypto.PrivateKey
} }
// Interface for all challenge solvers to implement. // Interface for all challenge solvers to implement.
@ -53,7 +52,7 @@ type Client struct {
directory directory directory directory
user User user User
jws *jws jws *jws
keyBits int keyType KeyType
issuerCert []byte issuerCert []byte
solvers map[Challenge]solver 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 // 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 // the ACME directory located at caDirURL for the rest of its actions. It will
// generate private keys for certificates of size keyBits. // 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() privKey := user.GetPrivateKey()
if privKey == nil { if privKey == nil {
return nil, errors.New("private key was 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 var dir directory
if _, err := getJSON(caDirURL, &dir); err != nil { if _, err := getJSON(caDirURL, &dir); err != nil {
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) 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[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} 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 // 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 // AgreeToTOS updates the Client registration and sends the agreement to
// the server. // the server.
func (c *Client) AgreeToTOS() error { func (c *Client) AgreeToTOS() error {
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL reg := c.user.GetRegistration()
c.user.GetRegistration().Body.Resource = "reg"
reg.Body.Agreement = c.user.GetRegistration().TosURL
reg.Body.Resource = "reg"
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
return err return err
} }
@ -457,7 +454,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
commonName := authz[0] commonName := authz[0]
var err error var err error
if privKey == nil { if privKey == nil {
privKey, err = generatePrivateKey(rsakey, c.keyBits) privKey, err = generatePrivateKey(c.keyType)
if err != nil { if err != nil {
return CertificateResource{}, err return CertificateResource{}, err
} }
@ -471,7 +468,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
} }
// TODO: should the CSR be customizable? // 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 { if err != nil {
return CertificateResource{}, err return CertificateResource{}, err
} }

View file

@ -25,12 +25,16 @@ import (
"golang.org/x/crypto/sha3" "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 type derCertificateBytes []byte
const ( const (
eckey keyType = iota EC256 = KeyType("P256")
rsakey EC384 = KeyType("P348")
RSA2048 = KeyType("2048")
RSA4096 = KeyType("4096")
RSA8192 = KeyType("8192")
) )
const ( const (
@ -123,8 +127,16 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
} }
func getKeyAuthorization(token string, key interface{}) (string, 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 // Generate the Key Authorization for the challenge
jwk := keyAsJWK(key) jwk := keyAsJWK(publicKey)
if jwk == nil { if jwk == nil {
return "", errors.New("Could not generate JWK from key.") 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) { func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
switch t {
case eckey: switch keyType {
case EC256:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case EC384:
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case rsakey: case RSA2048:
return rsa.GenerateKey(rand.Reader, keyLength) 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{ template := x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: domain, CommonName: domain,
@ -245,6 +264,9 @@ func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byt
func pemEncode(data interface{}) []byte { func pemEncode(data interface{}) []byte {
var pemBlock *pem.Block var pemBlock *pem.Block
switch key := data.(type) { switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey: case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
break break

View file

@ -45,7 +45,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
} }
// Generate the Key Authorization for the challenge // 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 { if err != nil {
return err return err
} }

View file

@ -21,7 +21,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
// Generate the Key Authorization for the challenge // 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 { if err != nil {
return err return err
} }

View file

@ -2,7 +2,9 @@ package acme
import ( import (
"bytes" "bytes"
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa" "crypto/rsa"
"fmt" "fmt"
"net/http" "net/http"
@ -12,7 +14,7 @@ import (
type jws struct { type jws struct {
directoryURL string directoryURL string
privKey *rsa.PrivateKey privKey crypto.PrivateKey
nonces []string 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -28,17 +28,13 @@ type registrationMessage struct {
// Registration is returned by the ACME server after the registration // Registration is returned by the ACME server after the registration
// The client implementation should save this registration somewhere. // The client implementation should save this registration somewhere.
type Registration struct { type Registration struct {
Resource string `json:"resource,omitempty"` Resource string `json:"resource,omitempty"`
ID int `json:"id"` ID int `json:"id"`
Key struct { Key jose.JsonWebKey `json:"key"`
Kty string `json:"kty"` Contact []string `json:"contact"`
N string `json:"n"` Agreement string `json:"agreement,omitempty"`
E string `json:"e"` Authorizations string `json:"authorizations,omitempty"`
} `json:"key"` Certificates string `json:"certificates,omitempty"`
Contact []string `json:"contact"`
Agreement string `json:"agreement,omitempty"`
Authorizations string `json:"authorizations,omitempty"`
Certificates string `json:"certificates,omitempty"`
// RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"` // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
} }

View file

@ -22,7 +22,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
// Generate the Key Authorization for the challenge // 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 { if err != nil {
return err 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 // TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) { func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) {
// generate a new RSA key for the certificates // generate a new RSA key for the certificates
tempPrivKey, err := generatePrivateKey(rsakey, 2048) tempPrivKey, err := generatePrivateKey(RSA2048)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }

8
cli.go
View file

@ -92,10 +92,10 @@ func main() {
Name: "accept-tos, a", Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
}, },
cli.IntFlag{ cli.StringFlag{
Name: "rsa-key-size, B", Name: "key-type, k",
Value: 2048, Value: "rsa2048",
Usage: "Size of the RSA key.", Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "path", Name: "path",

View file

@ -34,7 +34,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
//TODO: move to account struct? Currently MUST pass email. //TODO: move to account struct? Currently MUST pass email.
acc := NewAccount(c.GlobalString("email"), conf) 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 { if err != nil {
logger().Fatalf("Could not create client: %s", err.Error()) logger().Fatalf("Could not create client: %s", err.Error())
} }

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"net/url" "net/url"
"os" "os"
"path" "path"
@ -20,9 +21,22 @@ func NewConfiguration(c *cli.Context) *Configuration {
return &Configuration{context: c} return &Configuration{context: c}
} }
// RsaBits returns the current set RSA bit length for private keys // KeyType the type from which private keys should be generated
func (c *Configuration) RsaBits() int { func (c *Configuration) KeyType() (acme.KeyType, error) {
return c.context.GlobalInt("rsa-key-size") 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. // ExcludedSolvers is a list of solvers that are to be excluded.

View file

@ -1,21 +1,30 @@
package main package main
import ( import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"io/ioutil" "io/ioutil"
"os" "os"
) )
func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { func generatePrivateKey(file string) (crypto.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, length)
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil { if err != nil {
return nil, err 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) certOut, err := os.Create(file)
if err != nil { if err != nil {
@ -28,12 +37,20 @@ func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) {
return privateKey, nil return privateKey, nil
} }
func loadRsaKey(file string) (*rsa.PrivateKey, error) { func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file) keyBytes, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
keyBlock, _ := pem.Decode(keyBytes) 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.")
} }