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.
// This function will never return a partial certificate. If one domain in the list fails,
// the whole certificate will fail.
func (c *Client) ObtainCertificateForCSR(csr []byte, bundle bool) (CertificateResource, map[string]error) {
// parse the CSR
parsedCsr, err := x509.ParseCertificateRequest(csr)
if err != nil {
return CertificateResource{}, map[string]error{"csr": err}
}
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
// figure out what domains it concerns
// start with the common name
domains := []string{parsedCsr.Subject.CommonName}
domains := []string{csr.Subject.CommonName}
// loop over the SubjectAltName DNS names
DNSNames:
for _, sanName := range parsedCsr.DNSNames {
// /
for _, sanName := range csr.DNSNames {
for _, existingName := range domains {
if existingName == sanName {
// duplicate; skip this name
@ -331,13 +324,16 @@ DNSNames:
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 {
for _, chln := range challenges {
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
}
@ -467,6 +463,18 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif
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
if cert.PrivateKey != nil {
privKey, err = parsePEMPrivateKey(cert.PrivateKey)

View file

@ -236,6 +236,9 @@ func pemEncode(data interface{}) []byte {
case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
break
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
break
case 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)
}
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.
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
func GetPEMCertExpiration(cert []byte) (time.Time, error) {

View file

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

View file

@ -2,6 +2,7 @@ package main
import (
"bufio"
"crypto/x509"
"encoding/json"
"encoding/pem"
"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)
if err != nil {
return nil, err
}
raw := bytes
// see if we can find a PEM-encoded CSR
var p *pem.Block
@ -229,14 +231,14 @@ func readCSRFile(filename string) ([]byte, error) {
// did we get a CSR?
if p.Type == "CERTIFICATE REQUEST" {
return p.Bytes, nil
raw = p.Bytes
}
}
// no PEM-encoded CSR
// assume we were given a DER-encoded ASN.1 CSR
// (if this assumption is wrong, parsing these bytes will fail)
return bytes, nil
return x509.ParseCertificateRequest(raw)
}
func run(c *cli.Context) error {
@ -281,7 +283,7 @@ func run(c *cli.Context) error {
if hasDomains {
// 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 {
// read the CSR
csr, err := readCSRFile(c.GlobalString("csr"))
@ -290,11 +292,10 @@ func run(c *cli.Context) error {
failures = map[string]error{"csr": err}
} else {
// 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 {
for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())