From 69bbae6026deba869378558a7c71270e1f62c192 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 11 Nov 2015 00:10:08 +0100 Subject: [PATCH 01/14] Do not exit on domain failure, only log it. --- cli_handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli_handlers.go b/cli_handlers.go index b938f9d1..e0e9e891 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -128,7 +128,7 @@ func run(c *cli.Context) { certs, failures := client.ObtainCertificates(c.GlobalStringSlice("domains"), true) if len(failures) > 0 { for k, v := range failures { - logger().Fatalf("[%s] Could not obtain certificates\n\t%v", k, v) + logger().Printf("[%s] Could not obtain certificates\n\t%v", k, v) } } From f6576e88158bfd463fcdee226138c4e67bb1f640 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 11 Nov 2015 01:00:20 +0100 Subject: [PATCH 02/14] Add locking to JWS nonce store. --- acme/jws.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/acme/jws.go b/acme/jws.go index 96f7ae9e..74a41867 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -6,13 +6,15 @@ import ( "crypto/rsa" "fmt" "net/http" + "sync" "github.com/letsencrypt/go-jose" ) type jws struct { - privKey *rsa.PrivateKey - nonces []string + privKey *rsa.PrivateKey + nonces []string + nonceMutex sync.Mutex } func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { @@ -24,11 +26,9 @@ func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { // Posts a JWS signed message to the specified URL func (j *jws) post(url string, content []byte) (*http.Response, error) { - if len(j.nonces) == 0 { - err := j.getNonce(url) - if err != nil { - return nil, fmt.Errorf("Could not get a nonce for request: %s\n\t\tError: %v", url, err) - } + err := j.getNonce(url) + if err != nil { + return nil, fmt.Errorf("Could not get a nonce for request: %s\n\t\tError: %v", url, err) } signedContent, err := j.signContent(content) @@ -66,11 +66,20 @@ func (j *jws) getNonceFromResponse(resp *http.Response) error { return fmt.Errorf("Server did not respond with a proper nonce header.") } + j.nonceMutex.Lock() j.nonces = append(j.nonces, nonce) + j.nonceMutex.Unlock() return nil } func (j *jws) getNonce(url string) error { + j.nonceMutex.Lock() + if len(j.nonces) > 0 { + j.nonceMutex.Unlock() + return nil + } + j.nonceMutex.Unlock() + resp, err := http.Head(url) if err != nil { return err @@ -80,6 +89,9 @@ func (j *jws) getNonce(url string) error { } func (j *jws) consumeNonce() string { + j.nonceMutex.Lock() + defer j.nonceMutex.Unlock() + nonce := "" if len(j.nonces) == 0 { return nonce From 27a8cff3c63444a72d03a9a38b4e4ff15147f9bb Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 11 Nov 2015 01:01:15 +0100 Subject: [PATCH 03/14] 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) } From 4801a4779e4abfc6063f1bf651bc740f850f9771 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 11 Nov 2015 18:05:09 +0100 Subject: [PATCH 04/14] Make the CLI use SAN certs --- cli_handlers.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cli_handlers.go b/cli_handlers.go index e0e9e891..0e6c343e 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -125,7 +125,7 @@ func run(c *cli.Context) { logger().Fatal("Please specify --domains") } - certs, failures := client.ObtainCertificates(c.GlobalStringSlice("domains"), true) + cert, failures := client.ObtainSANCertificate(c.GlobalStringSlice("domains"), true) if len(failures) > 0 { for k, v := range failures { logger().Printf("[%s] Could not obtain certificates\n\t%v", k, v) @@ -137,9 +137,10 @@ func run(c *cli.Context) { logger().Fatalf("Cound not check/create path: %v", err) } - for _, certRes := range certs { - saveCertRes(certRes, conf) - } + //for _, certRes := range certs { + // saveCertRes(certRes, conf) + //} + saveCertRes(cert, conf) } func revoke(c *cli.Context) { From c849ca1b90d59f94df4ab4ad5984097bf26198a6 Mon Sep 17 00:00:00 2001 From: xenolf Date: Tue, 17 Nov 2015 19:45:15 +0100 Subject: [PATCH 05/14] If any challenge fails - return an error --- acme/client.go | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/acme/client.go b/acme/client.go index 5c7207da..afa7f667 100644 --- a/acme/client.go +++ b/acme/client.go @@ -239,22 +239,11 @@ func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (Certificat } errs := c.solveChallenges(challenges) - for k, v := range errs { - failures[k] = v - } - - if len(failures) == len(domains) { + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(errs) > 0 { 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) From 6671fd137ca05b10275679b5ca54d07a6fb9a557 Mon Sep 17 00:00:00 2001 From: xenolf Date: Tue, 17 Nov 2015 22:22:25 +0100 Subject: [PATCH 06/14] Make sure the challenges do not get re-ordered for SAN certs --- acme/client.go | 26 +++++++++++++++++++++++--- acme/client_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/acme/client.go b/acme/client.go index afa7f667..d36ae1f8 100644 --- a/acme/client.go +++ b/acme/client.go @@ -234,10 +234,13 @@ func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (Certificat } challenges, failures := c.getChallenges(domains) - if len(challenges) == 0 { + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(failures) > 0 { return CertificateResource{}, failures } + challenges = reorderAuthorizations(domains, challenges) + errs := c.solveChallenges(challenges) // If any challenge fails - return. Do not generate partial SAN certificates. if len(errs) > 0 { @@ -246,9 +249,9 @@ func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (Certificat logf("[INFO] acme: Validations succeeded; requesting certificates") - cert, err := c.requestCertificate(succeededChallenges, bundle) + cert, err := c.requestCertificate(challenges, bundle) if err != nil { - for _, chln := range succeededChallenges { + for _, chln := range challenges { failures[chln.Domain] = err } } @@ -660,3 +663,20 @@ func parseLinks(links []string) map[string]string { return linkMap } + +func reorderAuthorizations(domains []string, challenges []authorizationResource) []authorizationResource { + // restore order of challenges + for i, domain := range domains { + if domain == challenges[i].Domain { + continue + } + + for j, chlng := range challenges { + if chlng.Domain == domain { + challenges[i], challenges[j] = challenges[j], challenges[i] + } + } + } + + return challenges +} diff --git a/acme/client_test.go b/acme/client_test.go index 1a29bf45..a7cd8ad8 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" + "fmt" + mrand "math/rand" "net/http" "net/http/httptest" "testing" @@ -68,3 +70,33 @@ type mockUser struct { func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } func (u mockUser) GetPrivateKey() *rsa.PrivateKey { return u.privatekey } + +func TestReorderAuthorizations(t *testing.T) { + // generate fake domains + var domains []string + for i := 0; i < 30; i++ { + domains = append(domains, fmt.Sprintf("example%d.com", i)) + } + + // generate authorizationResources from the domains + var challenges []authorizationResource + for _, domain := range domains { + challenges = append(challenges, authorizationResource{Domain: domain}) + } + + // shuffle the challenges slice + for i := len(challenges) - 1; i > 0; i-- { + j := mrand.Intn(i + 1) + challenges[i], challenges[j] = challenges[j], challenges[i] + } + + // reorder the challenges + reordered := reorderAuthorizations(domains, challenges) + + // test if reordering was successfull + for i, domain := range domains { + if domain != reordered[i].Domain { + t.Errorf("Expected reordered[%d] to equal %s but was %s", i, domain, reordered[i].Domain) + } + } +} From 487c8763d5545bae074dcf310ca9bf950c0649bd Mon Sep 17 00:00:00 2001 From: xenolf Date: Tue, 17 Nov 2015 22:36:25 +0100 Subject: [PATCH 07/14] Revert adding locks to jws - not in scope of this branch --- acme/jws.go | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/acme/jws.go b/acme/jws.go index 74a41867..96f7ae9e 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -6,15 +6,13 @@ import ( "crypto/rsa" "fmt" "net/http" - "sync" "github.com/letsencrypt/go-jose" ) type jws struct { - privKey *rsa.PrivateKey - nonces []string - nonceMutex sync.Mutex + privKey *rsa.PrivateKey + nonces []string } func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { @@ -26,9 +24,11 @@ func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { // Posts a JWS signed message to the specified URL func (j *jws) post(url string, content []byte) (*http.Response, error) { - err := j.getNonce(url) - if err != nil { - return nil, fmt.Errorf("Could not get a nonce for request: %s\n\t\tError: %v", url, err) + if len(j.nonces) == 0 { + err := j.getNonce(url) + if err != nil { + return nil, fmt.Errorf("Could not get a nonce for request: %s\n\t\tError: %v", url, err) + } } signedContent, err := j.signContent(content) @@ -66,20 +66,11 @@ func (j *jws) getNonceFromResponse(resp *http.Response) error { return fmt.Errorf("Server did not respond with a proper nonce header.") } - j.nonceMutex.Lock() j.nonces = append(j.nonces, nonce) - j.nonceMutex.Unlock() return nil } func (j *jws) getNonce(url string) error { - j.nonceMutex.Lock() - if len(j.nonces) > 0 { - j.nonceMutex.Unlock() - return nil - } - j.nonceMutex.Unlock() - resp, err := http.Head(url) if err != nil { return err @@ -89,9 +80,6 @@ func (j *jws) getNonce(url string) error { } func (j *jws) consumeNonce() string { - j.nonceMutex.Lock() - defer j.nonceMutex.Unlock() - nonce := "" if len(j.nonces) == 0 { return nonce From b9ba9e58b386847292646ff202e5fbc5d70e46c6 Mon Sep 17 00:00:00 2001 From: xenolf Date: Tue, 17 Nov 2015 23:07:13 +0100 Subject: [PATCH 08/14] Return the right error --- acme/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.go b/acme/client.go index d36ae1f8..e06917a0 100644 --- a/acme/client.go +++ b/acme/client.go @@ -244,7 +244,7 @@ func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (Certificat errs := c.solveChallenges(challenges) // If any challenge fails - return. Do not generate partial SAN certificates. if len(errs) > 0 { - return CertificateResource{}, failures + return CertificateResource{}, errs } logf("[INFO] acme: Validations succeeded; requesting certificates") From ca5c3a4315f03cd45d806213624d04aff1ea798d Mon Sep 17 00:00:00 2001 From: xenolf Date: Tue, 17 Nov 2015 23:31:37 +0100 Subject: [PATCH 09/14] Remove commented code --- cli_handlers.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cli_handlers.go b/cli_handlers.go index 0e6c343e..dafa25c9 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -137,9 +137,6 @@ func run(c *cli.Context) { logger().Fatalf("Cound not check/create path: %v", err) } - //for _, certRes := range certs { - // saveCertRes(certRes, conf) - //} saveCertRes(cert, conf) } From 3be490f6cb520fae7655c69662e45aa5aa80051e Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 18 Nov 2015 19:44:47 +0100 Subject: [PATCH 10/14] Change how challenge order is preserved as suggested by @zakjan --- acme/client.go | 32 ++++++++++---------------------- acme/client_test.go | 32 -------------------------------- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/acme/client.go b/acme/client.go index e06917a0..15e1d4c7 100644 --- a/acme/client.go +++ b/acme/client.go @@ -239,8 +239,6 @@ func (c *Client) ObtainSANCertificate(domains []string, bundle bool) (Certificat return CertificateResource{}, failures } - challenges = reorderAuthorizations(domains, challenges) - errs := c.solveChallenges(challenges) // If any challenge fails - return. Do not generate partial SAN certificates. if len(errs) > 0 { @@ -458,21 +456,28 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s }(domain) } - var responses []authorizationResource + responses := make(map[string]authorizationResource) failures := make(map[string]error) for i := 0; i < len(domains); i++ { select { case res := <-resc: - responses = append(responses, res) + responses[res.Domain] = res case err := <-errc: failures[err.Domain] = err.Error } } + challenges := make([]authorizationResource, 0, len(responses)) + for _, domain := range domains { + if challenge, ok := responses[domain]; ok { + challenges = append(challenges, challenge) + } + } + close(resc) close(errc) - return responses, failures + return challenges, failures } // requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs. @@ -663,20 +668,3 @@ func parseLinks(links []string) map[string]string { return linkMap } - -func reorderAuthorizations(domains []string, challenges []authorizationResource) []authorizationResource { - // restore order of challenges - for i, domain := range domains { - if domain == challenges[i].Domain { - continue - } - - for j, chlng := range challenges { - if chlng.Domain == domain { - challenges[i], challenges[j] = challenges[j], challenges[i] - } - } - } - - return challenges -} diff --git a/acme/client_test.go b/acme/client_test.go index a7cd8ad8..1a29bf45 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -4,8 +4,6 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" - "fmt" - mrand "math/rand" "net/http" "net/http/httptest" "testing" @@ -70,33 +68,3 @@ type mockUser struct { func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } func (u mockUser) GetPrivateKey() *rsa.PrivateKey { return u.privatekey } - -func TestReorderAuthorizations(t *testing.T) { - // generate fake domains - var domains []string - for i := 0; i < 30; i++ { - domains = append(domains, fmt.Sprintf("example%d.com", i)) - } - - // generate authorizationResources from the domains - var challenges []authorizationResource - for _, domain := range domains { - challenges = append(challenges, authorizationResource{Domain: domain}) - } - - // shuffle the challenges slice - for i := len(challenges) - 1; i > 0; i-- { - j := mrand.Intn(i + 1) - challenges[i], challenges[j] = challenges[j], challenges[i] - } - - // reorder the challenges - reordered := reorderAuthorizations(domains, challenges) - - // test if reordering was successfull - for i, domain := range domains { - if domain != reordered[i].Domain { - t.Errorf("Expected reordered[%d] to equal %s but was %s", i, domain, reordered[i].Domain) - } - } -} From caba7ddee796de3f2dfe6aab113ab5f118a7131e Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 18 Nov 2015 19:53:42 +0100 Subject: [PATCH 11/14] Add comment to ObtainSANCertificate --- acme/client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/acme/client.go b/acme/client.go index 15e1d4c7..23e189f4 100644 --- a/acme/client.go +++ b/acme/client.go @@ -226,6 +226,11 @@ func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]Certificat return certs, failures } +// ObtainSANCertificate 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. +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. 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, ", ")) From cae6d59e193312c3817068553be88aa5db5f346a Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 18 Nov 2015 21:06:45 +0100 Subject: [PATCH 12/14] Move back to square/go-jose --- acme/jws.go | 25 ++++++++++++++++--------- acme/messages.go | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/acme/jws.go b/acme/jws.go index 96f7ae9e..c5c496a1 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -4,10 +4,11 @@ import ( "bytes" "crypto/ecdsa" "crypto/rsa" + "errors" "fmt" "net/http" - "github.com/letsencrypt/go-jose" + "github.com/square/go-jose" ) type jws struct { @@ -15,10 +16,15 @@ type jws struct { nonces []string } -func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { - return jose.JsonWebKey{ - Key: key, - Algorithm: "EC", +func keyAsJWK(key interface{}) *jose.JsonWebKey { + switch k := key.(type) { + case *ecdsa.PublicKey: + return &jose.JsonWebKey{Key: k, Algorithm: "EC"} + case *rsa.PublicKey: + return &jose.JsonWebKey{Key: k, Algorithm: "RSA"} + + default: + return nil } } @@ -52,8 +58,9 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { if err != nil { return nil, err } + signer.SetNonceSource(j) - signed, err := signer.Sign(content, j.consumeNonce()) + signed, err := signer.Sign(content) if err != nil { return nil, err } @@ -79,12 +86,12 @@ func (j *jws) getNonce(url string) error { return j.getNonceFromResponse(resp) } -func (j *jws) consumeNonce() string { +func (j *jws) Nonce() (string, error) { nonce := "" if len(j.nonces) == 0 { - return nonce + return nonce, errors.New("No nonce available.") } nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1] - return nonce + return nonce, nil } diff --git a/acme/messages.go b/acme/messages.go index 948a86cc..857ec32a 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -3,7 +3,7 @@ package acme import ( "time" - "github.com/letsencrypt/go-jose" + "github.com/square/go-jose" ) type directory struct { From a8c2a1287140477bd40aa6bdbdbd9dc3bb69bace Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 18 Nov 2015 21:15:49 +0100 Subject: [PATCH 13/14] Move back to square/go-jose (reverted from commit cae6d59e193312c3817068553be88aa5db5f346a) --- acme/jws.go | 25 +++++++++---------------- acme/messages.go | 2 +- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/acme/jws.go b/acme/jws.go index c5c496a1..96f7ae9e 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -4,11 +4,10 @@ import ( "bytes" "crypto/ecdsa" "crypto/rsa" - "errors" "fmt" "net/http" - "github.com/square/go-jose" + "github.com/letsencrypt/go-jose" ) type jws struct { @@ -16,15 +15,10 @@ type jws struct { nonces []string } -func keyAsJWK(key interface{}) *jose.JsonWebKey { - switch k := key.(type) { - case *ecdsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "EC"} - case *rsa.PublicKey: - return &jose.JsonWebKey{Key: k, Algorithm: "RSA"} - - default: - return nil +func keyAsJWK(key *ecdsa.PublicKey) jose.JsonWebKey { + return jose.JsonWebKey{ + Key: key, + Algorithm: "EC", } } @@ -58,9 +52,8 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { if err != nil { return nil, err } - signer.SetNonceSource(j) - signed, err := signer.Sign(content) + signed, err := signer.Sign(content, j.consumeNonce()) if err != nil { return nil, err } @@ -86,12 +79,12 @@ func (j *jws) getNonce(url string) error { return j.getNonceFromResponse(resp) } -func (j *jws) Nonce() (string, error) { +func (j *jws) consumeNonce() string { nonce := "" if len(j.nonces) == 0 { - return nonce, errors.New("No nonce available.") + return nonce } nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1] - return nonce, nil + return nonce } diff --git a/acme/messages.go b/acme/messages.go index 857ec32a..948a86cc 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -3,7 +3,7 @@ package acme import ( "time" - "github.com/square/go-jose" + "github.com/letsencrypt/go-jose" ) type directory struct { From f41ed4f9de04f44c8cd677bd357d6cf4a326f252 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 18 Nov 2015 21:41:27 +0100 Subject: [PATCH 14/14] Remove unneeded function --- acme/client.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/acme/client.go b/acme/client.go index 23e189f4..520a73ce 100644 --- a/acme/client.go +++ b/acme/client.go @@ -518,10 +518,6 @@ func (c *Client) requestCertificates(challenges []authorizationResource, bundle return certs, failures } -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!")