From 72914df00f027074ea70c878dde401f02fa22f09 Mon Sep 17 00:00:00 2001 From: xenolf Date: Thu, 27 Oct 2016 11:22:10 +0200 Subject: [PATCH] Add OCSP must staple support Introduces a new command line switch `--must-staple` to `run` and `renew`. Using this switch will add the must staple TLS extension to the CSR generated by lego and thus also to the generated certificate. This does not work with user specified CSRs! Fixes #270 --- acme/client.go | 12 ++++++------ acme/crypto.go | 17 ++++++++++++++++- acme/crypto_test.go | 2 +- cli.go | 8 ++++++++ cli_handlers.go | 4 ++-- providers/dns/gandi/gandi_test.go | 2 +- 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/acme/client.go b/acme/client.go index 9fe9a9d9..9f837af3 100644 --- a/acme/client.go +++ b/acme/client.go @@ -353,7 +353,7 @@ DNSNames: // 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) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey) (CertificateResource, map[string]error) { +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) { if bundle { logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { @@ -374,7 +374,7 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - cert, err := c.requestCertificate(challenges, bundle, privKey) + cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple) if err != nil { for _, chln := range challenges { failures[chln.Domain] = err @@ -410,7 +410,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error { // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. -func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) { +func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. certificates, err := parsePEMBundle(cert.Certificate) @@ -462,7 +462,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif domains = append(domains, x509Cert.Subject.CommonName) } - newCert, failures := c.ObtainCertificate(domains, bundle, privKey) + newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple) return newCert, failures[cert.Domain] } @@ -563,7 +563,7 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s return challenges, failures } -func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) { +func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) { if len(authz) == 0 { return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!") } @@ -584,7 +584,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName.Domain, san) + csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple) if err != nil { return CertificateResource{}, err } diff --git a/acme/crypto.go b/acme/crypto.go index af97f5d1..c63b23b9 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -20,6 +20,8 @@ import ( "strings" "time" + "encoding/asn1" + "golang.org/x/crypto/ocsp" ) @@ -47,6 +49,12 @@ const ( OCSPServerFailed = ocsp.ServerFailed ) +// Constants for OCSP must staple +var ( + tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} + ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} +) + // GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, // the parsed response, and an error, if any. The returned []byte can be passed directly // into the OCSPStaple property of a tls.Certificate. If the bundle only contains the @@ -206,7 +214,7 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { return nil, fmt.Errorf("Invalid KeyType: %s", keyType) } -func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) { +func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, @@ -217,6 +225,13 @@ func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]b template.DNSNames = san } + if mustStaple { + template.Extensions = append(template.Extensions, pkix.Extension{ + Id: tlsFeatureExtensionOID, + Value: ocspMustStapleFeature, + }) + } + return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) } diff --git a/acme/crypto_test.go b/acme/crypto_test.go index d2fc5088..6f43835f 100644 --- a/acme/crypto_test.go +++ b/acme/crypto_test.go @@ -24,7 +24,7 @@ func TestGenerateCSR(t *testing.T) { t.Fatal("Error generating private key:", err) } - csr, err := generateCsr(key, "fizz.buzz", nil) + csr, err := generateCsr(key, "fizz.buzz", nil, true) if err != nil { t.Error("Error generating CSR:", err) } diff --git a/cli.go b/cli.go index 64221fdf..ed874531 100644 --- a/cli.go +++ b/cli.go @@ -64,6 +64,10 @@ func main() { Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, + cli.BoolFlag{ + Name: "must-staple", + Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", + }, }, }, { @@ -89,6 +93,10 @@ func main() { Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, + cli.BoolFlag{ + Name: "must-staple", + Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", + }, }, }, { diff --git a/cli_handlers.go b/cli_handlers.go index 6647c2ac..6c2e61b1 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -339,7 +339,7 @@ func run(c *cli.Context) error { if hasDomains { // obtain a certificate, generating a new private key - cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil) + cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple")) } else { // read the CSR csr, err := readCSRFile(c.GlobalString("csr")) @@ -452,7 +452,7 @@ func renew(c *cli.Context) error { certRes.Certificate = certBytes - newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle")) + newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"), c.Bool("must-staple")) if err != nil { logger().Fatalf("%s", err.Error()) } diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go index db4175f8..451333ca 100644 --- a/providers/dns/gandi/gandi_test.go +++ b/providers/dns/gandi/gandi_test.go @@ -141,7 +141,7 @@ func TestDNSProviderLive(t *testing.T) { } // complete the challenge bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil) + _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) if len(failures) > 0 { t.Fatal(failures) }