From 575370e196df110567eeb17a2c59815022acee28 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Tue, 7 Jun 2016 19:50:52 -0700 Subject: [PATCH] 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. --- acme/client.go | 30 +++++++++++++++++++----------- acme/crypto.go | 16 ++++++++++++++++ acme/messages.go | 1 + cli_handlers.go | 13 +++++++------ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/acme/client.go b/acme/client.go index d4e4f702..d4febfb2 100644 --- a/acme/client.go +++ b/acme/client.go @@ -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) diff --git a/acme/crypto.go b/acme/crypto.go index fc20442f..33240b61 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -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) { diff --git a/acme/messages.go b/acme/messages.go index a6539b96..0efeae67 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -113,4 +113,5 @@ type CertificateResource struct { AccountRef string `json:"accountRef,omitempty"` PrivateKey []byte `json:"-"` Certificate []byte `json:"-"` + CSR []byte `json:"-"` } diff --git a/cli_handlers.go b/cli_handlers.go index 8e06947c..99da0144 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -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())