From 51a95ee548d13658971259b286d0b84bc885906e Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 24 Oct 2015 03:55:18 +0200 Subject: [PATCH] Add initial support for certificate bundling --- acme/client.go | 125 +++++++++++++++++++++++++++++++++++++++-------- acme/crypto.go | 3 ++ acme/messages.go | 4 +- cli_handlers.go | 4 +- 4 files changed, 113 insertions(+), 23 deletions(-) diff --git a/acme/client.go b/acme/client.go index bb7ed14e..d7ae3b82 100644 --- a/acme/client.go +++ b/acme/client.go @@ -44,12 +44,13 @@ type solver interface { // Client is the user-friendy way to ACME type Client struct { - directory directory - user User - jws *jws - keyBits int - devMode bool - solvers map[string]solver + directory directory + user User + jws *jws + keyBits int + devMode bool + issuerCert []byte + solvers map[string]solver } // NewClient creates a new client for the set user. @@ -161,7 +162,9 @@ func (c *Client) AgreeToTOS() error { // ObtainCertificates tries to obtain certificates from the CA server // using the challenges it has configured. The returned certificates are // PEM encoded byte slices. -func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, error) { +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]CertificateResource, error) { logger().Print("Obtaining certificates...") challenges := c.getChallenges(domains) err := c.solveChallenges(challenges) @@ -171,17 +174,22 @@ func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, er logger().Print("Validations succeeded. Getting certificates") - return c.requestCertificates(challenges) + return c.requestCertificates(challenges, bundle) } -// RevokeCertificate takes a PEM encoded certificate 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 { - certBlock, err := pemDecode(certificate) + certificates, err := parsePEMBundle(certificate) if err != nil { return err } - encodedCert := base64.URLEncoding.EncodeToString(certBlock.Bytes) + x509Cert := certificates[len(certificates)-1] + if x509Cert.IsCA { + return fmt.Errorf("Certificate bundle ends with a CA certificate") + } + + encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) jsonBytes, err := json.Marshal(revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}) if err != nil { @@ -207,14 +215,21 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // Please be aware that this function will return a new certificate in ANY case that is not an error. // If the server does not provide us with a new cert on a GET request to the CertURL // this function will start a new-cert flow where a new certificate gets generated. -func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool) (CertificateResource, error) { +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bundle bool) (CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded - // cert later on in the renewal process. - x509Cert, err := pemDecodeTox509(cert.Certificate) + // cert later on in the renewal process. The input may be a bundle or a single certificate. + certificates, err := parsePEMBundle(cert.Certificate) if err != nil { return CertificateResource{}, err } + x509Cert := certificates[len(certificates)-1] + if x509Cert.IsCA { + return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle ends with a CA certificate", cert.Domain) + } + // This is just meant to be informal for the user. timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) logger().Printf("[%s] Trying to renew certificate with %d hours remaining.", cert.Domain, int(timeLeft.Hours())) @@ -243,11 +258,31 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool) (Cer if revokeOld { c.RevokeCertificate(cert.Certificate) } - cert.Certificate = pemEncode(derCertificateBytes(serverCertBytes)) + issuedCert := pemEncode(derCertificateBytes(serverCertBytes)) + // If bundle is true, we want to return a certificate bundle. + // To do this, we need the issuer certificate. + if bundle { + // The issuer certificate link is always supplied via an "up" link + // in the response headers of a new certificate. + links := parseLinks(resp.Header["Link"]) + issuerCert, err := c.getIssuerCertificate(links["up"]) + if err != nil { + // If we fail to aquire the issuer cert, return the issued certificate - do not fail. + logger().Printf("[%s] Could not bundle issuer certificate.\n%v", cert.Domain, err) + cert.Certificate = issuedCert + } else { + // Success - prepend the issuer cert to the issued cert. + issuerCert = pemEncode(derCertificateBytes(issuerCert)) + issuerCert = append(issuerCert, issuedCert...) + cert.Certificate = issuerCert + } + } else { + cert.Certificate = issuedCert + } return cert, nil } - newCerts, err := c.ObtainCertificates([]string{cert.Domain}) + newCerts, err := c.ObtainCertificates([]string{cert.Domain}, bundle) if err != nil { return CertificateResource{}, err } @@ -361,10 +396,10 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource { // 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) ([]CertificateResource, error) { +func (c *Client) requestCertificates(challenges []*authorizationResource, bundle bool) ([]CertificateResource, error) { resc, errc := make(chan CertificateResource), make(chan error) for _, authz := range challenges { - go c.requestCertificate(authz, resc, errc) + go c.requestCertificate(authz, resc, errc, bundle) } var certs []CertificateResource @@ -383,13 +418,14 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer return certs, nil } -func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan error) { +func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan error, bundle bool) { privKey, err := generatePrivateKey(rsakey, c.keyBits) if err != nil { errc <- err return } + // TODO: should the CSR be customizable? csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain) if err != nil { errc <- err @@ -432,8 +468,30 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce // certificate was not ready at the time this request completed. // Otherwise the body is the certificate. if len(cert) > 0 { + cerRes.CertStableURL = resp.Header.Get("Content-Location") - cerRes.Certificate = pemEncode(derCertificateBytes(cert)) + + issuedCert := pemEncode(derCertificateBytes(cert)) + // If bundle is true, we want to return a certificate bundle. + // To do this, we need the issuer certificate. + if bundle { + // The issuer certificate link is always supplied via an "up" link + // in the response headers of a new certificate. + links := parseLinks(resp.Header["Link"]) + issuerCert, err := c.getIssuerCertificate(links["up"]) + if err != nil { + // If we fail to aquire the issuer cert, return the issued certificate - do not fail. + logger().Printf("[%s] Could not bundle issuer certificate.\n%v", authz.Domain, err) + cerRes.Certificate = issuedCert + } else { + // Success - prepend the issuer cert to the issued cert. + issuerCert = pemEncode(derCertificateBytes(issuerCert)) + issuerCert = append(issuerCert, issuedCert...) + cerRes.Certificate = issuerCert + } + } else { + cerRes.Certificate = issuedCert + } logger().Printf("[%s] Server responded with a certificate.", authz.Domain) result <- cerRes return @@ -465,6 +523,33 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce } } +// getIssuerCertificate requests the issuer certificate and caches it for +// subsequent requests. +func (c *Client) getIssuerCertificate(url string) ([]byte, error) { + logger().Printf("Requesting issuer cert from: %s", url) + if c.issuerCert != nil { + return c.issuerCert, nil + } + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + issuerBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + _, err = x509.ParseCertificate(issuerBytes) + if err != nil { + return nil, err + } + + c.issuerCert = issuerBytes + return issuerBytes, err +} + func logResponseHeaders(resp *http.Response) { logger().Println(resp.Status) for k, v := range resp.Header { diff --git a/acme/crypto.go b/acme/crypto.go index f41a6a8e..b928271c 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -61,6 +61,7 @@ func GetOCSPForCert(bundle []byte) ([]byte, error) { } // Insert it into the slice on position 0 + // We want it ordered right CA -> CRT certificates = append(certificates, nil) copy(certificates[1:], certificates[0:]) certificates[0] = issuerCert @@ -127,6 +128,8 @@ func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label return buffer } +// parsePEMBundle parses a certificate bundle from top to bottom and returns +// a slice of x509 certificates. This function will error if no certificates are found. func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { var certificates []*x509.Certificate diff --git a/acme/messages.go b/acme/messages.go index e25cd9b2..948a86cc 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -94,7 +94,9 @@ type revokeCertMessage struct { // CertificateResource represents a CA issued certificate. // PrivateKey and Certificate are both already PEM encoded -// and can be directly written to disk. +// and can be directly written to disk. Certificate may +// be a certificate bundle, depending on the options supplied +// to create it. type CertificateResource struct { Domain string `json:"domain"` CertURL string `json:"certUrl"` diff --git a/cli_handlers.go b/cli_handlers.go index 51697433..fa7cc9c8 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -120,7 +120,7 @@ func run(c *cli.Context) { logger().Fatal("Please specify --domains") } - certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains")) + certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"), true) if err != nil { logger().Fatalf("Could not obtain certificates\n\t%v", err) } @@ -198,7 +198,7 @@ func renew(c *cli.Context) { certRes.PrivateKey = keyBytes certRes.Certificate = certBytes - newCert, err := client.RenewCertificate(certRes, true) + newCert, err := client.RenewCertificate(certRes, true, true) if err != nil { logger().Printf("%v", err) return