Repository: certbot/certbot

Issue:
Add --must-staple flag
We should have a flag that adds the Must Staple extension in CSRs, per https://tools.ietf.org/html/rfc7633.



Discussion:
+1

:+1: 


Resolving patch:
diff --git a/certbot/cli.py b/certbot/cli.py
--- a/certbot/cli.py
+++ b/certbot/cli.py
@@ -704,6 +704,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):
     helpful.add(
         "security", "--rsa-key-size", type=int, metavar="N",
         default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
+    helpful.add(
+        "security", "--must-staple", action="store_true",
+        help=config_help("must_staple"), dest="must_staple", default=False)
     helpful.add(
         "security", "--redirect", action="store_true",
         help="Automatically redirect all HTTP traffic to HTTPS for the newly "
diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py
--- a/certbot/crypto_util.py
+++ b/certbot/crypto_util.py
@@ -75,9 +75,11 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"):
     :rtype: :class:`certbot.le_util.CSR`
 
     """
-    csr_pem, csr_der = make_csr(privkey.pem, names)
-
     config = zope.component.getUtility(interfaces.IConfig)
+
+    csr_pem, csr_der = make_csr(privkey.pem, names,
+        must_staple=config.must_staple)
+
     # Save CSR
     le_util.make_or_verify_dir(path, 0o755, os.geteuid(),
                                config.strict_permissions)
@@ -92,7 +94,7 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"):
 
 
 # Lower level functions
-def make_csr(key_str, domains):
+def make_csr(key_str, domains, must_staple=False):
     """Generate a CSR.
 
     :param str key_str: PEM-encoded RSA key.
@@ -111,13 +113,19 @@ def make_csr(key_str, domains):
     req.get_subject().CN = domains[0]
     # TODO: what to put into req.get_subject()?
     # TODO: put SAN if len(domains) > 1
-    req.add_extensions([
+    extensions = [
         OpenSSL.crypto.X509Extension(
             "subjectAltName",
             critical=False,
             value=", ".join("DNS:%s" % d for d in domains)
-        ),
-    ])
+        )
+    ]
+    if must_staple:
+        extensions.append(OpenSSL.crypto.X509Extension(
+            "1.3.6.1.5.5.7.1.24",
+            critical=False,
+            value="DER:30:03:02:01:05"))
+    req.add_extensions(extensions)
     req.set_version(2)
     req.set_pubkey(pkey)
     req.sign(pkey, "sha256")
diff --git a/certbot/interfaces.py b/certbot/interfaces.py
--- a/certbot/interfaces.py
+++ b/certbot/interfaces.py
@@ -200,6 +200,10 @@ class IConfig(zope.interface.Interface):
     email = zope.interface.Attribute(
         "Email used for registration and recovery contact.")
     rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
+    must_staple = zope.interface.Attribute(
+        "Whether to request the OCSP Must Staple certificate extension. "
+        "Additional setup may be required after issuance. This does not "
+        "currently autoconfigure web servers for OCSP stapling. ")
 
     config_dir = zope.interface.Attribute("Configuration directory.")
     work_dir = zope.interface.Attribute("Working directory.")


Test patch:
diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py
--- a/certbot/tests/crypto_util_test.py
+++ b/certbot/tests/crypto_util_test.py
@@ -95,6 +95,25 @@ def test_san(self):
             ['example.com', 'www.example.com'], get_sans_from_csr(
                 csr_der, OpenSSL.crypto.FILETYPE_ASN1))
 
+    def test_must_staple(self):
+        # TODO: Fails for RSA256_KEY
+        csr_pem, _ = self._call(
+            RSA512_KEY, ['example.com', 'www.example.com'], must_staple=True)
+        csr = OpenSSL.crypto.load_certificate_request(
+            OpenSSL.crypto.FILETYPE_PEM, csr_pem)
+
+        # In pyopenssl 0.13 (used with TOXENV=py26-oldest and py27-oldest), csr
+        # objects don't have a get_extensions() method, so we skip this test if
+        # the method isn't available.
+        if hasattr(csr, 'get_extensions'):
+            # NOTE: Ideally we would filter by the TLS Feature OID, but
+            # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID,
+            # and the shortname field is just "UNDEF"
+            must_staple_exts = [e for e in csr.get_extensions()
+                if e.get_data() == "0\x03\x02\x01\x05"]
+            self.assertEqual(len(must_staple_exts), 1,
+                "Expected exactly one Must Staple extension")
+
 
 class ValidCSRTest(unittest.TestCase):
     """Tests for certbot.crypto_util.valid_csr."""