Merge pull request #30 from xenolf/add-san-cert

Add SAN certificates - fix #20
This commit is contained in:
xenolf 2015-11-18 22:07:54 +01:00
commit 7662cbcec5
4 changed files with 106 additions and 43 deletions

View file

@ -211,9 +211,17 @@ func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]Certificat
return nil, failures 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") logf("[INFO] acme: Validations succeeded; requesting certificates")
certs, err := c.requestCertificates(challenges, bundle) certs, err := c.requestCertificates(succeededChallenges, bundle)
for k, v := range err { for k, v := range err {
failures[k] = v failures[k] = v
} }
@ -221,6 +229,42 @@ func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]Certificat
return certs, failures 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, ", "))
} else {
logf("[INFO] acme: Obtaining SAN certificate for %v", 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] acme: Validations succeeded; requesting certificates")
cert, err := c.requestCertificate(challenges, bundle)
if err != nil {
for _, chln := range challenges {
failures[chln.Domain] = err
}
}
return cert, failures
}
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. // RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Client) RevokeCertificate(certificate []byte) error { func (c *Client) RevokeCertificate(certificate []byte) error {
certificates, err := parsePEMBundle(certificate) certificates, err := parsePEMBundle(certificate)
@ -338,7 +382,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund
// Looks through the challenge combinations to find a solvable match. // Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns. // 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. // loop through the resources, basically through the domains.
failures := make(map[string]error) failures := make(map[string]error)
for _, authz := range challenges { for _, authz := range challenges {
@ -381,8 +425,8 @@ func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver
} }
// Get the challenges needed to proof our identifier to the ACME server. // Get the challenges needed to proof our identifier to the ACME server.
func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[string]error) { func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
resc, errc := make(chan *authorizationResource), make(chan domainError) resc, errc := make(chan authorizationResource), make(chan domainError)
for _, domain := range domains { for _, domain := range domains {
go func(domain string) { go func(domain string) {
@ -416,34 +460,48 @@ func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[
} }
resp.Body.Close() 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) }(domain)
} }
var responses []*authorizationResource responses := make(map[string]authorizationResource)
failures := make(map[string]error) failures := make(map[string]error)
for i := 0; i < len(domains); i++ { for i := 0; i < len(domains); i++ {
select { select {
case res := <-resc: case res := <-resc:
responses = append(responses, res) responses[res.Domain] = res
case err := <-errc: case err := <-errc:
failures[err.Domain] = err.Error 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(resc)
close(errc) close(errc)
return responses, failures return challenges, failures
} }
// requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs. // 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 // It then uses these to request a certificate from the CA and returns the list of successfully
// granted certificates. // 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) resc, errc := make(chan CertificateResource), make(chan domainError)
for _, authz := range challenges { 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 var certs []CertificateResource
@ -460,39 +518,47 @@ func (c *Client) requestCertificates(challenges []*authorizationResource, bundle
close(resc) close(resc)
close(errc) 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) 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) privKey, err := generatePrivateKey(rsakey, c.keyBits)
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return }
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? // 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 { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
csrString := base64.URLEncoding.EncodeToString(csr) 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 { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
resp, err := c.jws.post(authz.NewCertURL, jsonBytes) resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
privateKeyPem := pemEncode(privKey) privateKeyPem := pemEncode(privKey)
cerRes := CertificateResource{ cerRes := CertificateResource{
Domain: authz.Domain, Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"), CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem} PrivateKey: privateKeyPem}
@ -505,8 +571,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
cert, err := ioutil.ReadAll(resp.Body) cert, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
// The server returns a body with a length of zero if the // The server returns a body with a length of zero if the
@ -526,7 +591,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
issuerCert, err := c.getIssuerCertificate(links["up"]) issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil { if err != nil {
// If we fail to aquire the issuer cert, return the issued certificate - do not fail. // 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 { } else {
// Success - append the issuer cert to the issued cert. // Success - append the issuer cert to the issued cert.
issuerCert = pemEncode(derCertificateBytes(issuerCert)) issuerCert = pemEncode(derCertificateBytes(issuerCert))
@ -535,9 +600,8 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
} }
cerRes.Certificate = issuedCert cerRes.Certificate = issuedCert
logf("[%s] Server responded with a certificate.", authz.Domain) logf("[%s] Server responded with a certificate.", commonName.Domain)
result <- cerRes return cerRes, nil
return
} }
// The certificate was granted but is not yet issued. // The certificate was granted but is not yet issued.
@ -545,23 +609,20 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
ra := resp.Header.Get("Retry-After") ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra) retryAfter, err := strconv.Atoi(ra)
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
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) time.Sleep(time.Duration(retryAfter) * time.Second)
break break
default: default:
errc <- domainError{Domain: authz.Domain, Error: handleHTTPError(resp)} return CertificateResource{}, handleHTTPError(resp)
return
} }
resp, err = http.Get(cerRes.CertURL) resp, err = http.Get(cerRes.CertURL)
if err != nil { if err != nil {
errc <- domainError{Domain: authz.Domain, Error: err} return CertificateResource{}, err
return
} }
} }
} }

View file

@ -211,13 +211,17 @@ func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) {
return nil, fmt.Errorf("Invalid keytype: %d", t) 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{ template := x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: domain, CommonName: domain,
}, },
} }
if len(san) > 0 {
template.DNSNames = san
}
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
} }

View file

@ -23,7 +23,7 @@ func TestGenerateCSR(t *testing.T) {
t.Fatal("Error generating private key:", err) 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 { if err != nil {
t.Error("Error generating CSR:", err) t.Error("Error generating CSR:", err)
} }

View file

@ -125,7 +125,7 @@ func run(c *cli.Context) {
logger().Fatal("Please specify --domains") 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 { if len(failures) > 0 {
for k, v := range failures { for k, v := range failures {
logger().Printf("[%s] Could not obtain certificates\n\t%v", k, v) logger().Printf("[%s] Could not obtain certificates\n\t%v", k, v)
@ -137,9 +137,7 @@ func run(c *cli.Context) {
logger().Fatalf("Cound not check/create path: %v", err) logger().Fatalf("Cound not check/create path: %v", err)
} }
for _, certRes := range certs { saveCertRes(cert, conf)
saveCertRes(certRes, conf)
}
} }
func revoke(c *cli.Context) { func revoke(c *cli.Context) {