From 27a8cff3c63444a72d03a9a38b4e4ff15147f9bb Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 11 Nov 2015 01:01:15 +0100 Subject: [PATCH] Initial support for SAN certificates --- acme/client.go | 133 ++++++++++++++++++++++++++++++++------------ acme/crypto.go | 6 +- acme/crypto_test.go | 2 +- 3 files changed, 104 insertions(+), 37 deletions(-) diff --git a/acme/client.go b/acme/client.go index fd1c9b92..5c7207da 100644 --- a/acme/client.go +++ b/acme/client.go @@ -208,9 +208,17 @@ func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]Certificat return nil, failures } + // remove failed challenges from slice + var succeededChallenges []authorizationResource + for _, chln := range challenges { + if failures[chln.Domain] == nil { + succeededChallenges = append(succeededChallenges, chln) + } + } + logf("[INFO] acme: Validations succeeded; requesting certificates") - certs, err := c.requestCertificates(challenges, bundle) + certs, err := c.requestCertificates(succeededChallenges, bundle) for k, v := range err { failures[k] = v } @@ -218,6 +226,47 @@ func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]Certificat return certs, failures } +func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (CertificateResource, map[string]error) { + if bundle { + logf("[INFO] acme: Obtaining bundled SAN certificate for %v", strings.Join(domains, ", ")) + } else { + logf("[INFO] acme: Obtaining SAN certificate for %v", strings.Join(domains, ", ")) + } + + challenges, failures := c.getChallenges(domains) + if len(challenges) == 0 { + return CertificateResource{}, failures + } + + errs := c.solveChallenges(challenges) + for k, v := range errs { + failures[k] = v + } + + if len(failures) == len(domains) { + return CertificateResource{}, failures + } + + // remove failed challenges from slice + var succeededChallenges []authorizationResource + for _, chln := range challenges { + if failures[chln.Domain] == nil { + succeededChallenges = append(succeededChallenges, chln) + } + } + + logf("[INFO] acme: Validations succeeded; requesting certificates") + + cert, err := c.requestCertificate(succeededChallenges, bundle) + if err != nil { + for _, chln := range succeededChallenges { + failures[chln.Domain] = err + } + } + + return cert, failures +} + // RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. func (c *Client) RevokeCertificate(certificate []byte) error { certificates, err := parsePEMBundle(certificate) @@ -335,7 +384,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund // Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. -func (c *Client) solveChallenges(challenges []*authorizationResource) map[string]error { +func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error { // loop through the resources, basically through the domains. failures := make(map[string]error) for _, authz := range challenges { @@ -378,8 +427,8 @@ func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver } // Get the challenges needed to proof our identifier to the ACME server. -func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[string]error) { - resc, errc := make(chan *authorizationResource), make(chan domainError) +func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) { + resc, errc := make(chan authorizationResource), make(chan domainError) for _, domain := range domains { go func(domain string) { @@ -413,11 +462,11 @@ func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[ } resp.Body.Close() - resc <- &authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: resp.Header.Get("Location"), Domain: domain} + resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: resp.Header.Get("Location"), Domain: domain} }(domain) } - var responses []*authorizationResource + var responses []authorizationResource failures := make(map[string]error) for i := 0; i < len(domains); i++ { select { @@ -437,10 +486,17 @@ func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[ // requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs. // It then uses these to request a certificate from the CA and returns the list of successfully // granted certificates. -func (c *Client) requestCertificates(challenges []*authorizationResource, bundle bool) ([]CertificateResource, map[string]error) { +func (c *Client) requestCertificates(challenges []authorizationResource, bundle bool) ([]CertificateResource, map[string]error) { resc, errc := make(chan CertificateResource), make(chan domainError) for _, authz := range challenges { - go c.requestCertificate(authz, resc, errc, bundle) + go func(authz authorizationResource, resc chan CertificateResource, errc chan domainError) { + certRes, err := c.requestCertificate([]authorizationResource{authz}, bundle) + if err != nil { + errc <- domainError{Domain: authz.Domain, Error: err} + } else { + resc <- certRes + } + }(authz, resc, errc) } var certs []CertificateResource @@ -457,39 +513,51 @@ func (c *Client) requestCertificates(challenges []*authorizationResource, bundle close(resc) close(errc) - return certs, nil + return certs, failures } -func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan domainError, bundle bool) { +func (c *Client) requestSANCertificate() { + +} + +func (c *Client) requestCertificate(authz []authorizationResource, bundle bool) (CertificateResource, error) { + if len(authz) == 0 { + return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!") + } + + commonName := authz[0] privKey, err := generatePrivateKey(rsakey, c.keyBits) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err + } + + 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? - csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain) + csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } csrString := base64.URLEncoding.EncodeToString(csr) - jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: []string{authz.AuthURL}}) + jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs}) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } - resp, err := c.jws.post(authz.NewCertURL, jsonBytes) + resp, err := c.jws.post(commonName.NewCertURL, jsonBytes) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } privateKeyPem := pemEncode(privKey) cerRes := CertificateResource{ - Domain: authz.Domain, + Domain: commonName.Domain, CertURL: resp.Header.Get("Location"), PrivateKey: privateKeyPem} @@ -502,8 +570,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce cert, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } // The server returns a body with a length of zero if the @@ -523,7 +590,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { // If we fail to aquire the issuer cert, return the issued certificate - do not fail. - logf("[WARNING] acme: [%s] Could not bundle issuer certificate: %v", authz.Domain, err) + logf("[WARNING] acme: [%s] Could not bundle issuer certificate: %v", commonName.Domain, err) } else { // Success - append the issuer cert to the issued cert. issuerCert = pemEncode(derCertificateBytes(issuerCert)) @@ -532,9 +599,8 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce } cerRes.Certificate = issuedCert - logf("[%s] Server responded with a certificate.", authz.Domain) - result <- cerRes - return + logf("[%s] Server responded with a certificate.", commonName.Domain) + return cerRes, nil } // The certificate was granted but is not yet issued. @@ -542,23 +608,20 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce ra := resp.Header.Get("Retry-After") retryAfter, err := strconv.Atoi(ra) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } - logf("[INFO] acme: [%s] Server responded with status 202; retrying after %ds", authz.Domain, retryAfter) + logf("[INFO] acme: [%s] Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter) time.Sleep(time.Duration(retryAfter) * time.Second) break default: - errc <- domainError{Domain: authz.Domain, Error: handleHTTPError(resp)} - return + return CertificateResource{}, handleHTTPError(resp) } resp, err = http.Get(cerRes.CertURL) if err != nil { - errc <- domainError{Domain: authz.Domain, Error: err} - return + return CertificateResource{}, err } } } diff --git a/acme/crypto.go b/acme/crypto.go index 608ec7d4..1eb675f3 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -188,13 +188,17 @@ func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) { return nil, fmt.Errorf("Invalid keytype: %d", t) } -func generateCsr(privateKey *rsa.PrivateKey, domain string) ([]byte, error) { +func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, }, } + if len(san) > 0 { + template.DNSNames = san + } + return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) } diff --git a/acme/crypto_test.go b/acme/crypto_test.go index 81de504e..81ab287e 100644 --- a/acme/crypto_test.go +++ b/acme/crypto_test.go @@ -23,7 +23,7 @@ func TestGenerateCSR(t *testing.T) { t.Fatal("Error generating private key:", err) } - csr, err := generateCsr(key.(*rsa.PrivateKey), "fizz.buzz") + csr, err := generateCsr(key.(*rsa.PrivateKey), "fizz.buzz", nil) if err != nil { t.Error("Error generating CSR:", err) }