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) }