Merge pull request #224 from paybyphone/support_existing_csr

Support existing CSRs (update to #122)
This commit is contained in:
xenolf 2016-06-16 01:01:12 +02:00 committed by GitHub
commit b2fad61981
6 changed files with 180 additions and 11 deletions

View file

@ -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

View file

@ -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"),

View file

@ -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) {

View file

@ -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
View file

@ -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",

View file

@ -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,9 +150,12 @@ 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())
} }
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600) if certRes.PrivateKey != nil {
if err != nil { // if we were given a CSR, we don't know the private key
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error()) err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
if err != nil {
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")
@ -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())