diff --git a/acme/client.go b/acme/client.go index 5dec496c..d4e4f702 100644 --- a/acme/client.go +++ b/acme/client.go @@ -278,6 +278,69 @@ 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 []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} + } + + // figure out what domains it concerns + // start with the common name + domains := []string{parsedCsr.Subject.CommonName} + + // loop over the SubjectAltName DNS names +DNSNames: + for _, sanName := range parsedCsr.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, nil) + if err != nil { + for _, chln := range challenges { + failures[chln.Domain] = err + } + } + + 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 @@ -528,7 +591,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 +599,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 +612,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 +634,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"),