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
|
||||
|
||||
- Register with CA
|
||||
- Obtain certificates
|
||||
- Obtain certificates, both from scratch or with an existing CSR
|
||||
- Renew certificates
|
||||
- Revoke certificates
|
||||
- Robust implementation of all ACME challenges
|
||||
|
@ -89,6 +89,7 @@ COMMANDS:
|
|||
|
||||
GLOBAL OPTIONS:
|
||||
--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.
|
||||
--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.
|
||||
|
@ -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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
|
|
|
@ -278,6 +278,65 @@ func (c *Client) AgreeToTOS() error {
|
|||
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.
|
||||
// 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
|
||||
|
@ -404,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)
|
||||
|
@ -528,7 +599,6 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
||||
}
|
||||
|
||||
commonName := authz[0]
|
||||
var err error
|
||||
if privKey == nil {
|
||||
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 authURLs []string
|
||||
for _, auth := range authz[1:] {
|
||||
san = append(san, auth.Domain)
|
||||
authURLs = append(authURLs, auth.AuthURL)
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
|
@ -550,6 +620,17 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
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)
|
||||
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
||||
if err != nil {
|
||||
|
@ -561,7 +642,6 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
privateKeyPem := pemEncode(privKey)
|
||||
cerRes := CertificateResource{
|
||||
Domain: commonName.Domain,
|
||||
CertURL: resp.Header.Get("Location"),
|
||||
|
|
|
@ -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:"-"`
|
||||
}
|
||||
|
|
4
cli.go
4
cli.go
|
@ -103,6 +103,10 @@ func main() {
|
|||
Name: "domains, d",
|
||||
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{
|
||||
Name: "server, s",
|
||||
Value: "https://acme-v01.api.letsencrypt.org/directory",
|
||||
|
|
|
@ -2,7 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"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())
|
||||
}
|
||||
|
||||
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())
|
||||
if certRes.PrivateKey != nil {
|
||||
// if we were given a CSR, we don't know the private key
|
||||
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")
|
||||
|
@ -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 {
|
||||
conf, acc, client := setup(c)
|
||||
if acc.Registration == nil {
|
||||
|
@ -232,11 +268,34 @@ func run(c *cli.Context) error {
|
|||
handleTOS(c, client, acc)
|
||||
}
|
||||
|
||||
if len(c.GlobalStringSlice("domains")) == 0 {
|
||||
logger().Fatal("Please specify --domains or -d")
|
||||
// we require either domains or csr, but not both
|
||||
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 {
|
||||
for k, v := range failures {
|
||||
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
|
||||
|
|
Loading…
Reference in a new issue