From 675e6e212a23d37cfd00e507c8e43c158721e2c4 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 15 Mar 2016 00:58:47 -0700 Subject: [PATCH 1/2] Add --must-staple flag. --- letsencrypt/cli.py | 3 +++ letsencrypt/crypto_util.py | 20 ++++++++++++++------ letsencrypt/interfaces.py | 2 ++ letsencrypt/tests/crypto_util_test.py | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8e545a5de..cbd3b0704 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1589,6 +1589,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/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 5fdcba843..7856c13cc 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -75,9 +75,11 @@ def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"): :rtype: :class:`letsencrypt.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-letsencrypt.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/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1921b1e54..be82787b5 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -200,6 +200,8 @@ 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 extension.") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 1a9f39572..ec35fc688 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -95,6 +95,25 @@ class MakeCSRTest(unittest.TestCase): ['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 letsencrypt.crypto_util.valid_csr.""" From 82beb6f705093f5e82b2c9bdb8013276dcb92939 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Wed, 16 Mar 2016 16:25:41 -0700 Subject: [PATCH 2/2] Update must-staple help. --- letsencrypt/interfaces.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index be82787b5..a2e8506e6 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -201,7 +201,9 @@ class IConfig(zope.interface.Interface): "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 extension.") + "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.")