forked from TrueCloudLab/lego
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:
parent
01e2a30802
commit
575370e196
4 changed files with 43 additions and 17 deletions
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue