forked from TrueCloudLab/lego
Merge pull request #224 from paybyphone/support_existing_csr
Support existing CSRs (update to #122)
This commit is contained in:
commit
b2fad61981
6 changed files with 180 additions and 11 deletions
11
README.md
11
README.md
|
@ -27,7 +27,7 @@ docker build -t lego .
|
||||||
#### Features
|
#### Features
|
||||||
|
|
||||||
- Register with CA
|
- Register with CA
|
||||||
- Obtain certificates
|
- Obtain certificates, both from scratch or with an existing CSR
|
||||||
- Renew certificates
|
- Renew certificates
|
||||||
- Revoke certificates
|
- Revoke certificates
|
||||||
- Robust implementation of all ACME challenges
|
- Robust implementation of all ACME challenges
|
||||||
|
@ -89,6 +89,7 @@ COMMANDS:
|
||||||
|
|
||||||
GLOBAL OPTIONS:
|
GLOBAL OPTIONS:
|
||||||
--domains, -d [--domains option --domains option] Add domains to the process
|
--domains, -d [--domains option --domains option] Add domains to the process
|
||||||
|
--csr, -c Certificate signing request filename, if an external CSR is to be used
|
||||||
--server, -s "https://acme-v01.api.letsencrypt.org/directory" CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.
|
--server, -s "https://acme-v01.api.letsencrypt.org/directory" CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.
|
||||||
--email, -m Email used for registration and recovery contact.
|
--email, -m Email used for registration and recovery contact.
|
||||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
||||||
|
@ -130,6 +131,14 @@ $ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego
|
||||||
|
|
||||||
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
|
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
|
||||||
|
|
||||||
|
Obtain a certificate given a certificate signing request (CSR) generated by something else:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ lego --email="foo@bar.com" --csr=/path/to/csr.pem run
|
||||||
|
```
|
||||||
|
|
||||||
|
(lego will infer the domains to be validated based on the contents of the CSR, so make sure the CSR's Common Name and optional SubjectAltNames are set correctly.)
|
||||||
|
|
||||||
lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead:
|
lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
@ -278,6 +278,65 @@ func (c *Client) AgreeToTOS() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
|
||||||
|
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
|
||||||
|
// for this CSR is not required.
|
||||||
|
// If bundle is true, the []byte contains both the issuer certificate and
|
||||||
|
// 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 x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
|
||||||
|
// figure out what domains it concerns
|
||||||
|
// start with the common name
|
||||||
|
domains := []string{csr.Subject.CommonName}
|
||||||
|
|
||||||
|
// loop over the SubjectAltName DNS names
|
||||||
|
DNSNames:
|
||||||
|
for _, sanName := range csr.DNSNames {
|
||||||
|
for _, existingName := range domains {
|
||||||
|
if existingName == sanName {
|
||||||
|
// duplicate; skip this name
|
||||||
|
continue DNSNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// name is unique
|
||||||
|
domains = append(domains, sanName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bundle {
|
||||||
|
logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||||
|
} else {
|
||||||
|
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
challenges, failures := c.getChallenges(domains)
|
||||||
|
// If any challenge fails - return. Do not generate partial SAN certificates.
|
||||||
|
if len(failures) > 0 {
|
||||||
|
return CertificateResource{}, failures
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := c.solveChallenges(challenges)
|
||||||
|
// If any challenge fails - return. Do not generate partial SAN certificates.
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return CertificateResource{}, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
|
||||||
// The first domain in domains is used for the CommonName field of the certificate, all other
|
// The first domain in domains is used for the CommonName field of the certificate, all other
|
||||||
// domains are added using the Subject Alternate Names extension. A new private key is generated
|
// domains are added using the Subject Alternate Names extension. A new private key is generated
|
||||||
|
@ -404,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)
|
||||||
|
@ -528,7 +599,6 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
||||||
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
||||||
}
|
}
|
||||||
|
|
||||||
commonName := authz[0]
|
|
||||||
var err error
|
var err error
|
||||||
if privKey == nil {
|
if privKey == nil {
|
||||||
privKey, err = generatePrivateKey(c.keyType)
|
privKey, err = generatePrivateKey(c.keyType)
|
||||||
|
@ -537,11 +607,11 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine certificate name(s) based on the authorization resources
|
||||||
|
commonName := authz[0]
|
||||||
var san []string
|
var san []string
|
||||||
var authURLs []string
|
|
||||||
for _, auth := range authz[1:] {
|
for _, auth := range authz[1:] {
|
||||||
san = append(san, auth.Domain)
|
san = append(san, auth.Domain)
|
||||||
authURLs = append(authURLs, auth.AuthURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: should the CSR be customizable?
|
// TODO: should the CSR be customizable?
|
||||||
|
@ -550,6 +620,17 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
||||||
return CertificateResource{}, err
|
return CertificateResource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
|
||||||
|
commonName := authz[0]
|
||||||
|
|
||||||
|
var authURLs []string
|
||||||
|
for _, auth := range authz[1:] {
|
||||||
|
authURLs = append(authURLs, auth.AuthURL)
|
||||||
|
}
|
||||||
|
|
||||||
csrString := base64.URLEncoding.EncodeToString(csr)
|
csrString := base64.URLEncoding.EncodeToString(csr)
|
||||||
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -561,7 +642,6 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
||||||
return CertificateResource{}, err
|
return CertificateResource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKeyPem := pemEncode(privKey)
|
|
||||||
cerRes := CertificateResource{
|
cerRes := CertificateResource{
|
||||||
Domain: commonName.Domain,
|
Domain: commonName.Domain,
|
||||||
CertURL: resp.Header.Get("Location"),
|
CertURL: resp.Header.Get("Location"),
|
||||||
|
|
|
@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|
4
cli.go
4
cli.go
|
@ -103,6 +103,10 @@ func main() {
|
||||||
Name: "domains, d",
|
Name: "domains, d",
|
||||||
Usage: "Add domains to the process",
|
Usage: "Add domains to the process",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "csr, c",
|
||||||
|
Usage: "Certificate signing request filename, if an external CSR is to be used",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "server, s",
|
Name: "server, s",
|
||||||
Value: "https://acme-v01.api.letsencrypt.org/directory",
|
Value: "https://acme-v01.api.letsencrypt.org/directory",
|
||||||
|
|
|
@ -2,7 +2,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -148,10 +150,13 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
|
||||||
logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
|
logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if certRes.PrivateKey != nil {
|
||||||
|
// if we were given a CSR, we don't know the private key
|
||||||
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
|
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
|
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -205,6 +210,37 @@ func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
rest := bytes
|
||||||
|
for {
|
||||||
|
// decode a PEM block
|
||||||
|
p, rest = pem.Decode(rest)
|
||||||
|
|
||||||
|
// did we fail?
|
||||||
|
if p == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// did we get a CSR?
|
||||||
|
if p.Type == "CERTIFICATE REQUEST" {
|
||||||
|
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 x509.ParseCertificateRequest(raw)
|
||||||
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error {
|
func run(c *cli.Context) error {
|
||||||
conf, acc, client := setup(c)
|
conf, acc, client := setup(c)
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil {
|
||||||
|
@ -232,11 +268,34 @@ func run(c *cli.Context) error {
|
||||||
handleTOS(c, client, acc)
|
handleTOS(c, client, acc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.GlobalStringSlice("domains")) == 0 {
|
// we require either domains or csr, but not both
|
||||||
logger().Fatal("Please specify --domains or -d")
|
hasDomains := len(c.GlobalStringSlice("domains")) > 0
|
||||||
|
hasCsr := len(c.GlobalString("csr")) > 0
|
||||||
|
if hasDomains && hasCsr {
|
||||||
|
logger().Fatal("Please specify either --domains/-d or --csr/-c, but not both")
|
||||||
|
}
|
||||||
|
if !hasDomains && !hasCsr {
|
||||||
|
logger().Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cert acme.CertificateResource
|
||||||
|
var failures map[string]error
|
||||||
|
|
||||||
|
if hasDomains {
|
||||||
|
// obtain a certificate, generating a new private key
|
||||||
|
cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
|
||||||
|
} else {
|
||||||
|
// read the CSR
|
||||||
|
csr, err := readCSRFile(c.GlobalString("csr"))
|
||||||
|
if err != nil {
|
||||||
|
// we couldn't read the CSR
|
||||||
|
failures = map[string]error{"csr": err}
|
||||||
|
} else {
|
||||||
|
// obtain a certificate for this CSR
|
||||||
|
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