From 735bd924bf525a25571a7fdea30a9211f0b076fb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 17 Jul 2015 17:44:20 +0000 Subject: [PATCH] Update letsencrypt to DVSNI v03 (fixes #597). --- letsencrypt/achallenges.py | 42 +++++++--- letsencrypt/auth_handler.py | 16 ++-- letsencrypt/crypto_util.py | 11 ++- letsencrypt/plugins/common.py | 11 +-- letsencrypt/plugins/common_test.py | 29 +++---- .../plugins/standalone/authenticator.py | 47 ++++++----- .../standalone/tests/authenticator_test.py | 80 +++++++++---------- letsencrypt/tests/achallenges_test.py | 28 +++---- letsencrypt/tests/acme_util.py | 3 +- letsencrypt/tests/auth_handler_test.py | 6 +- letsencrypt/tests/continuity_auth_test.py | 6 +- 11 files changed, 133 insertions(+), 146 deletions(-) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index a81ae05a2..ced57395a 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -18,7 +18,7 @@ Note, that all annotated challenges act as a proxy objects:: """ from acme import challenges -from acme.jose import util as jose_util +from acme import jose from letsencrypt import crypto_util @@ -26,7 +26,7 @@ from letsencrypt import crypto_util # pylint: disable=too-few-public-methods -class AnnotatedChallenge(jose_util.ImmutableMap): +class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. Wraps around server provided challenge and annotates with data @@ -43,23 +43,41 @@ class AnnotatedChallenge(jose_util.ImmutableMap): class DVSNI(AnnotatedChallenge): - """Client annotated "dvsni" ACME challenge.""" - __slots__ = ('challb', 'domain', 'key') + """Client annotated "dvsni" ACME challenge. + + :ivar .Account account: + + """ + __slots__ = ('challb', 'domain', 'account') acme_type = challenges.DVSNI - def gen_cert_and_response(self, s=None): # pylint: disable=invalid-name + def gen_cert_and_response(self, key_pem=None, bits=2048, alg=jose.RS256): """Generate a DVSNI cert and save it to filepath. - :returns: ``(cert_pem, response)`` tuple, where ``cert_pem`` is the PEM - encoded certificate and ``response`` is an instance - :class:`acme.challenges.DVSNIResponse`. + :param bytes key_pem: Private PEM-encoded key used for + certificate generation. If none provided, a fresh key will + be generated. + :param int bits: Number of bits for fresh key generation. + :param .JWAAlgorithm alg: + + :returns: ``(response, cert_pem, key_pem)`` tuple, where + ``response`` is an instance of + `acme.challenges.DVSNIResponse`, ``cert_pem`` is the + PEM-encoded certificate and ``key_pem`` is PEM-encoded + private key. :rtype: tuple """ - response = challenges.DVSNIResponse(s=s) - cert_pem = crypto_util.make_ss_cert(self.key, [ - self.domain, self.nonce_domain, response.z_domain(self.challb)]) - return cert_pem, response + key_pem = crypto_util.make_key(bits) if key_pem is None else key_pem + response = challenges.DVSNIResponse(validation=jose.JWS.sign( + payload=self.challb.chall.json_dumps().encode('utf-8'), + alg=alg, + key=self.account.key, + include_jwk=False, + )) + cert_pem = crypto_util.make_ss_cert( + key_pem, ["some CN", response.z_domain], force_san=True) + return response, cert_pem, key_pem class SimpleHTTP(AnnotatedChallenge): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index bd6e89cc3..9c985b751 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -317,7 +317,7 @@ class AuthHandler(object): challb = self.authzr[domain].body.challenges[index] chall = challb.chall - achall = challb_to_achall(challb, self.account.key, domain) + achall = challb_to_achall(challb, self.account, domain) if isinstance(chall, challenges.ContinuityChallenge): cont_chall.append(achall) @@ -327,15 +327,11 @@ class AuthHandler(object): return cont_chall, dv_chall -def challb_to_achall(challb, key, domain): +def challb_to_achall(challb, account, domain): """Converts a ChallengeBody object to an AnnotatedChallenge. - :param challb: ChallengeBody - :type challb: :class:`acme.messages.ChallengeBody` - - :param key: Key - :type key: :class:`letsencrypt.le_util.Key` - + :param .ChallengeBody challb: ChallengeBody + :param .Account account: :param str domain: Domain of the challb :returns: Appropriate AnnotatedChallenge @@ -347,10 +343,10 @@ def challb_to_achall(challb, key, domain): if isinstance(chall, challenges.DVSNI): return achallenges.DVSNI( - challb=challb, domain=domain, key=key) + challb=challb, domain=domain, account=account) elif isinstance(chall, challenges.SimpleHTTP): return achallenges.SimpleHTTP( - challb=challb, domain=domain, key=key) + challb=challb, domain=domain, key=account.key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryToken): diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index edfd2eccf..8850fc357 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -216,10 +216,13 @@ def pyopenssl_load_certificate(data): def make_ss_cert(key, domains, not_before=None, - validity=(7 * 24 * 60 * 60)): + validity=(7 * 24 * 60 * 60), force_san=False): """Returns new self-signed cert in PEM form. - Uses key and contains all domains. + If more than one domain is provided, all of the domains are put into + ``subjectAltName`` X.509 extension and first domain is set as the + subject CN. If only one domain is provided no ``subjectAltName`` + extension is used, unless `force_san` is ``True``. """ if isinstance(key, jose.JWK): @@ -243,7 +246,7 @@ def make_ss_cert(key, domains, not_before=None, # TODO: what to put into cert.get_subject()? cert.set_issuer(cert.get_subject()) - if len(domains) > 1: + if force_san or len(domains) > 1: extensions.append(OpenSSL.crypto.X509Extension( "subjectAltName", critical=False, @@ -312,7 +315,7 @@ def _get_sans_from_cert_or_req( def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of Subject Alternative Names from a certificate. - :param str csr: Certificate (encoded). + :param str cert: Certificate (encoded). :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` :returns: A list of Subject Alternative Names. diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 104e8d9c4..c781fbe28 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -4,7 +4,6 @@ import pkg_resources import shutil import tempfile -from cryptography.hazmat.primitives import serialization import zope.interface from acme.jose import util as jose_util @@ -173,17 +172,11 @@ class Dvsni(object): self.configurator.reverter.register_file_creation(True, key_path) self.configurator.reverter.register_file_creation(True, cert_path) - cert_pem, response = achall.gen_cert_and_response(s) + response, cert_pem, key_pem = achall.gen_cert_and_response(s) - # Write out challenge cert + # Write out challenge cert and key with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) - - # Write out challenge key - key_pem = achall.key.key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption()) with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index b68ab8369..4ecf638dd 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -120,21 +120,12 @@ class DvsniTest(unittest.TestCase): achalls = [ achallenges.DVSNI( challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" - "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", - ), "pending"), - domain="encryption-example.demo", key=auth_key), + challenges.DVSNI(token=b'dvsni1'), "pending"), + domain="encryption-example.demo", account=mock.Mock(key=auth_key)), achallenges.DVSNI( challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\xba\xa9\xda?