cert: Extend acme.CertificateResource, support CSRs on renew

client.RenewCertificate now supports CSRs, and in fact prefers them,
when renewing certificates. In other words, if the certificate was
created via a CSR then using that will be attempted before re-generating
off a new private key.

Also adjusted the API of ObtainCertificateForCSR to be a little
more in line with the original ObtainCertificate function.
This commit is contained in:
Chris Marchesi 2016-06-07 19:50:52 -07:00
parent 01e2a30802
commit 575370e196
4 changed files with 43 additions and 17 deletions

View file

@ -285,21 +285,14 @@ func (c *Client) AgreeToTOS() error {
// your issued certificate as a bundle. // your issued certificate as a bundle.
// This function will never return a partial certificate. If one domain in the list fails, // This function will never return a partial certificate. If one domain in the list fails,
// the whole certificate will fail. // the whole certificate will fail.
func (c *Client) ObtainCertificateForCSR(csr []byte, bundle bool) (CertificateResource, map[string]error) { func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
// parse the CSR
parsedCsr, err := x509.ParseCertificateRequest(csr)
if err != nil {
return CertificateResource{}, map[string]error{"csr": err}
}
// figure out what domains it concerns // figure out what domains it concerns
// start with the common name // start with the common name
domains := []string{parsedCsr.Subject.CommonName} domains := []string{csr.Subject.CommonName}
// loop over the SubjectAltName DNS names // loop over the SubjectAltName DNS names
DNSNames: DNSNames:
for _, sanName := range parsedCsr.DNSNames { for _, sanName := range csr.DNSNames {
// /
for _, existingName := range domains { for _, existingName := range domains {
if existingName == sanName { if existingName == sanName {
// duplicate; skip this name // duplicate; skip this name
@ -331,13 +324,16 @@ DNSNames:
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
cert, err := c.requestCertificateForCsr(challenges, bundle, csr, nil) cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
if err != nil { if err != nil {
for _, chln := range challenges { for _, chln := range challenges {
failures[chln.Domain] = err failures[chln.Domain] = err
} }
} }
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
return cert, failures return cert, failures
} }
@ -467,6 +463,18 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif
return cert, nil return cert, nil
} }
// If the certificate is the same, then we need to request a new certificate.
// Start by checking to see if the certificate was based off a CSR, and
// use that if it's defined.
if len(cert.CSR) > 0 {
csr, err := pemDecodeTox509CSR(cert.CSR)
if err != nil {
return CertificateResource{}, err
}
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
return newCert, failures[cert.Domain]
}
var privKey crypto.PrivateKey var privKey crypto.PrivateKey
if cert.PrivateKey != nil { if cert.PrivateKey != nil {
privKey, err = parsePEMPrivateKey(cert.PrivateKey) privKey, err = parsePEMPrivateKey(cert.PrivateKey)

View file

@ -236,6 +236,9 @@ func pemEncode(data interface{}) []byte {
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
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
break
case derCertificateBytes: case derCertificateBytes:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
} }
@ -261,6 +264,19 @@ func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
return x509.ParseCertificate(pemBlock.Bytes) return x509.ParseCertificate(pemBlock.Bytes)
} }
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
if pemBlock.Type != "CERTIFICATE REQUEST" {
return nil, fmt.Errorf("PEM block is not a certificate request")
}
return x509.ParseCertificateRequest(pemBlock.Bytes)
}
// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate. // GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate.
// The certificate has to be PEM encoded. Any other encodings like DER will fail. // The certificate has to be PEM encoded. Any other encodings like DER will fail.
func GetPEMCertExpiration(cert []byte) (time.Time, error) { func GetPEMCertExpiration(cert []byte) (time.Time, error) {

View file

@ -113,4 +113,5 @@ type CertificateResource struct {
AccountRef string `json:"accountRef,omitempty"` AccountRef string `json:"accountRef,omitempty"`
PrivateKey []byte `json:"-"` PrivateKey []byte `json:"-"`
Certificate []byte `json:"-"` Certificate []byte `json:"-"`
CSR []byte `json:"-"`
} }

View file

@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"io/ioutil" "io/ioutil"
@ -209,11 +210,12 @@ func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
} }
} }
func readCSRFile(filename string) ([]byte, error) { func readCSRFile(filename string) (*x509.CertificateRequest, error) {
bytes, err := ioutil.ReadFile(filename) bytes, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
raw := bytes
// see if we can find a PEM-encoded CSR // see if we can find a PEM-encoded CSR
var p *pem.Block var p *pem.Block
@ -229,14 +231,14 @@ func readCSRFile(filename string) ([]byte, error) {
// did we get a CSR? // did we get a CSR?
if p.Type == "CERTIFICATE REQUEST" { if p.Type == "CERTIFICATE REQUEST" {
return p.Bytes, nil raw = p.Bytes
} }
} }
// no PEM-encoded CSR // no PEM-encoded CSR
// assume we were given a DER-encoded ASN.1 CSR // assume we were given a DER-encoded ASN.1 CSR
// (if this assumption is wrong, parsing these bytes will fail) // (if this assumption is wrong, parsing these bytes will fail)
return bytes, nil return x509.ParseCertificateRequest(raw)
} }
func run(c *cli.Context) error { func run(c *cli.Context) error {
@ -281,7 +283,7 @@ func run(c *cli.Context) error {
if hasDomains { if hasDomains {
// obtain a certificate, generating a new private key // obtain a certificate, generating a new private key
cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), true, nil) cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
} else { } else {
// read the CSR // read the CSR
csr, err := readCSRFile(c.GlobalString("csr")) csr, err := readCSRFile(c.GlobalString("csr"))
@ -290,11 +292,10 @@ func run(c *cli.Context) error {
failures = map[string]error{"csr": err} failures = map[string]error{"csr": err}
} else { } else {
// obtain a certificate for this CSR // obtain a certificate for this CSR
cert, failures = client.ObtainCertificateForCSR(csr, true) cert, failures = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle"))
} }
} }
cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
if len(failures) > 0 { if len(failures) > 0 {
for k, v := range failures { for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error()) logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())