diff --git a/acme/client.go b/acme/client.go index e45e60b30..64b55b9ff 100644 --- a/acme/client.go +++ b/acme/client.go @@ -1,6 +1,7 @@ package acme import ( + "crypto" "crypto/rsa" "crypto/x509" "encoding/base64" @@ -10,7 +11,6 @@ import ( "io/ioutil" "log" "net" - "net/http" "regexp" "strconv" "strings" @@ -194,12 +194,14 @@ func (c *Client) AgreeToTOS() error { // ObtainCertificate tries to obtain a single certificate using all domains passed into it. // The first domain in domains is used for the CommonName field of the certificate, all other -// domains are added using the Subject Alternate Names extension. +// domains are added using the Subject Alternate Names extension. A new private key is generated +// for every invocation of this function. If you do not want that you can supply your own private key +// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one. // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. -func (c *Client) ObtainCertificate(domains []string, bundle bool) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey) (CertificateResource, map[string]error) { if bundle { logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { @@ -220,7 +222,7 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool) (CertificateRe logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - cert, err := c.requestCertificate(challenges, bundle) + cert, err := c.requestCertificate(challenges, bundle, privKey) if err != nil { for _, chln := range challenges { failures[chln.Domain] = err @@ -255,6 +257,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // this function will start a new-cert flow where a new certificate gets generated. // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. +// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. @@ -316,7 +319,15 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif return cert, nil } - newCert, failures := c.ObtainCertificate([]string{cert.Domain}, bundle) + var privKey crypto.PrivateKey + if cert.PrivateKey != nil { + privKey, err = parsePEMPrivateKey(cert.PrivateKey) + if err != nil { + return CertificateResource{}, err + } + } + + newCert, failures := c.ObtainCertificate([]string{cert.Domain}, bundle, privKey) return newCert, failures[cert.Domain] } @@ -412,15 +423,18 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s return challenges, failures } -func (c *Client) requestCertificate(authz []authorizationResource, bundle bool) (CertificateResource, error) { +func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) { if len(authz) == 0 { return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!") } commonName := authz[0] - privKey, err := generatePrivateKey(rsakey, c.keyBits) - if err != nil { - return CertificateResource{}, err + var err error + if privKey == nil { + privKey, err = generatePrivateKey(rsakey, c.keyBits) + if err != nil { + return CertificateResource{}, err + } } var san []string diff --git a/acme/crypto.go b/acme/crypto.go index 6e9c721a6..9bd199ef7 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -202,6 +202,19 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { return certificates, nil } +func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { + keyBlock, _ := pem.Decode(key) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + default: + return nil, errors.New("Unknown PEM header value") + } +} + func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) { switch t { case eckey: diff --git a/cli.go b/cli.go index 83173e08a..6f186c033 100644 --- a/cli.go +++ b/cli.go @@ -55,6 +55,10 @@ func main() { Value: 0, Usage: "The number of days left on a certificate to renew it.", }, + cli.BoolFlag{ + Name: "reuse-key", + Usage: "Used to indicate you want to reuse your current private key for the new certificate.", + }, }, }, } diff --git a/cli_handlers.go b/cli_handlers.go index e622316dd..613cd6e83 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -138,7 +138,7 @@ func run(c *cli.Context) { logger().Fatal("Please specify --domains or -d") } - cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), true) + cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), true, nil) if len(failures) > 0 { for k, v := range failures { logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error()) @@ -214,11 +214,6 @@ func renew(c *cli.Context) { } } - keyBytes, err := ioutil.ReadFile(privPath) - if err != nil { - logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error()) - } - metaBytes, err := ioutil.ReadFile(metaPath) if err != nil { logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error()) @@ -230,7 +225,14 @@ func renew(c *cli.Context) { logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error()) } - certRes.PrivateKey = keyBytes + if c.Bool("reuse-key") { + keyBytes, err := ioutil.ReadFile(privPath) + if err != nil { + logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error()) + } + certRes.PrivateKey = keyBytes + } + certRes.Certificate = certBytes newCert, err := client.RenewCertificate(certRes, true)