diff --git a/cli.go b/cli.go index 947ed345..9747ab98 100644 --- a/cli.go +++ b/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", diff --git a/cli_handlers.go b/cli_handlers.go index 06d534c4..8e06947c 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -3,6 +3,7 @@ package main import ( "bufio" "encoding/json" + "encoding/pem" "io/ioutil" "os" "path" @@ -148,9 +149,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 +209,36 @@ func handleTOS(c *cli.Context, client *acme.Client, acc *Account) { } } +func readCSRFile(filename string) ([]byte, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + // 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" { + return p.Bytes, nil + } + } + + // 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 +} + func run(c *cli.Context) error { conf, acc, client := setup(c) if acc.Registration == nil { @@ -232,11 +266,35 @@ 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)") } - cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil) + 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"), true, 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, true) + } + } + + 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())