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.
|
||||
// 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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -113,4 +113,5 @@ type CertificateResource struct {
|
|||
AccountRef string `json:"accountRef,omitempty"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
Certificate []byte `json:"-"`
|
||||
CSR []byte `json:"-"`
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue