def test_http_multiva_threshold_fail(): # Only config-next has remote VAs configured and is appropriate for this # integration test. if not CONFIG_NEXT: return client = chisel2.make_client() # Configure a guestlist that will fail the multiVA threshold test by # only allowing the primary VA. guestlist = {"boulder": 1} hostname, cleanup = multiva_setup(client, guestlist) try: chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") except acme_errors.ValidationError as e: # NOTE(@cpu): Chisel2's expect_problem doesn't work in this case so this # test needs to unpack an `acme_errors.ValidationError` on its own. It # might be possible to clean this up in the future. if len(e.failed_authzrs) != 1: raise Exception("expected one failed authz, found {0}".format( len(e.failed_authzrs))) challs = e.failed_authzrs[0].body.challenges httpChall = None for chall_body in challs: if isinstance(chall_body.chall, challenges.HTTP01): httpChall = chall_body if httpChall is None: raise Exception("no HTTP-01 challenge in failed authz") if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized": raise Exception("expected unauthorized prob, found {0}".format( httpChall.error.typ)) finally: cleanup()
def test_http_multiva_threshold_fail(): # Only config-next has remote VAs configured and is appropriate for this # integration test. if not CONFIG_NEXT: return client = chisel2.make_client() # Configure a guestlist that will fail the multiVA threshold test by # only allowing the primary VA. guestlist = {"boulder": 1} hostname, cleanup = multiva_setup(client, guestlist) try: chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") except acme_errors.ValidationError as e: # NOTE(@cpu): Chisel2's expect_problem doesn't work in this case so this # test needs to unpack an `acme_errors.ValidationError` on its own. It # might be possible to clean this up in the future. if len(e.failed_authzrs) != 1: raise Exception("expected one failed authz, found {0}".format(len(e.failed_authzrs))) challs = e.failed_authzrs[0].body.challenges httpChall = None for chall_body in challs: if isinstance(chall_body.chall, challenges.HTTP01): httpChall = chall_body if httpChall is None: raise Exception("no HTTP-01 challenge in failed authz") if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized": raise Exception("expected unauthorized prob, found {0}".format(httpChall.error.typ)) finally: cleanup()
def test_http_challenge_http_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Calculate its keyauth so we can add it in a special non-standard location # for the redirect result resp = chall.response(client.net.key) keyauth = resp.key_authorization challSrv.add_http01_response("http-redirect", keyauth) # Create a HTTP redirect from the challenge's validation path to some other # token path where we have registered the key authorization. challengePath = "/.well-known/acme-challenge/{0}".format(token) redirectPath = "/.well-known/acme-challenge/http-redirect?params=are&important=to¬=lose" challSrv.add_http_redirect( challengePath, "http://{0}{1}".format(d, redirectPath)) chisel2.auth_and_issue([d], client=client, chall_type="http-01") challSrv.remove_http_redirect(challengePath) challSrv.remove_http01_response("http-redirect") history = challSrv.http_request_history(d) challSrv.clear_http_request_history(d) # There should have been at least two GET requests made to the # challtestsrv. There may have been more if remote VAs were configured. if len(history) < 2: raise Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))) initialRequests = [] redirectedRequests = [] for request in history: # All requests should have been over HTTP if request['HTTPS'] is True: raise Exception("Expected all requests to be HTTP") # Initial requests should have the expected initial HTTP-01 URL for the challenge if request['URL'] == challengePath: initialRequests.append(request) # Redirected requests should have the expected redirect path URL with all # its parameters elif request['URL'] == redirectPath: redirectedRequests.append(request) else: raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) # There should have been at least 1 initial HTTP-01 validation request. if len(initialRequests) < 1: raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) # There should have been at least 1 redirected HTTP request for each VA if len(redirectedRequests) < 1: raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests)))
def test_http_challenge_http_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Calculate its keyauth so we can add it in a special non-standard location # for the redirect result resp = chall.response(client.net.key) keyauth = resp.key_authorization challSrv.add_http01_response("http-redirect", keyauth) # Create a HTTP redirect from the challenge's validation path to some other # token path where we have registered the key authorization. challengePath = "/.well-known/acme-challenge/{0}".format(token) redirectPath = "/.well-known/acme-challenge/http-redirect?params=are&important=to¬=lose" challSrv.add_http_redirect( challengePath, "http://{0}{1}".format(d, redirectPath)) chisel2.auth_and_issue([d], client=client, chall_type="http-01") challSrv.remove_http_redirect(challengePath) challSrv.remove_http01_response("http-redirect") history = challSrv.http_request_history(d) challSrv.clear_http_request_history(d) # There should have been at least two GET requests made to the # challtestsrv. There may have been more if remote VAs were configured. if len(history) < 2: raise Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))) initialRequests = [] redirectedRequests = [] for request in history: # All requests should have been over HTTP if request['HTTPS'] is True: raise Exception("Expected all requests to be HTTP") # Initial requests should have the expected initial HTTP-01 URL for the challenge if request['URL'] == challengePath: initialRequests.append(request) # Redirected requests should have the expected redirect path URL with all # its parameters elif request['URL'] == redirectPath: redirectedRequests.append(request) else: raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) # There should have been at least 1 initial HTTP-01 validation request. if len(initialRequests) < 1: raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) # There should have been at least 1 redirected HTTP request for each VA if len(redirectedRequests) < 1: raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests)))
def test_duplicate_orders(): """ Test that the same client issuing for the same domain names twice in a row works without error. """ client = chisel2.make_client(None) domains = [random_domain()] chisel2.auth_and_issue(domains, client=client) chisel2.auth_and_issue(domains, client=client)
def test_duplicate_orders(): """ Test that the same client issuing for the same domain names twice in a row works without error. """ client = chisel2.make_client(None) domains = [ random_domain() ] chisel2.auth_and_issue(domains, client=client) chisel2.auth_and_issue(domains, client=client)
def check_challenge_dns_err(chalType): """ check_challenge_dns_err tests that performing an ACME challenge of the specified type to a hostname that is configured to return SERVFAIL for all queries produces the correct problem type and detail message. """ client = chisel2.make_client() # Create a random domains. d = random_domain() # Configure the chall srv to SERVFAIL all queries for that domain. challSrv.add_servfail_response(d) # Expect a DNS problem with a detail that matches a regex expectedProbType = "dns" expectedProbRegex = re.compile( r"DNS problem: SERVFAIL looking up (A|AAAA|TXT|CAA) for {0}".format(d)) # Try and issue for the domain with the given challenge type. failed = False try: chisel2.auth_and_issue([d], client=client, chall_type=chalType) except acme_errors.ValidationError as e: # Mark that the auth_and_issue failed failed = True # Extract the failed challenge from each failed authorization for authzr in e.failed_authzrs: c = None if chalType == "http-01": c = chisel2.get_chall(authzr, challenges.HTTP01) elif chalType == "dns-01": c = chisel2.get_chall(authzr, challenges.DNS01) elif chalType == "tls-alpn-01": c = chisel2.get_chall(authzr, challenges.TLSALPN01) else: raise Exception( "Invalid challenge type requested: {0}".format(challType)) # The failed challenge's error should match expected error = c.error if error is None or error.typ != "urn:ietf:params:acme:error:{0}".format( expectedProbType): raise Exception("Expected {0} prob, got {1}".format( expectedProbType, error.typ)) if not expectedProbRegex.match(error.detail): raise Exception( "Prob detail did not match expectedProbRegex, got \"{0}\"". format(error.detail)) finally: challSrv.remove_servfail_response(d) # If there was no exception that means something went wrong. The test should fail. if failed is False: raise Exception( "No problem generated issuing for broken DNS identifier")
def test_long_san_no_cn(): try: chisel2.auth_and_issue([''.join(random.choice(string.ascii_uppercase) for x in range(61)) + ".com"]) # if we get to this raise the auth_and_issue call didn't fail, so fail the test raise Exception("Issuance didn't fail when the only SAN in a certificate was longer than the max CN length") except messages.Error as e: if e.typ != "urn:ietf:params:acme:error:malformed": raise Exception('Expected malformed type problem, got {0}'.format(e.typ)) if e.detail != 'Error finalizing order :: issuing precertificate: CSR doesn\'t contain a SAN short enough to fit in CN': raise Exception('Problem detail did not match expected')
def test_revoke_by_authz(): domains = [random_domain()] order = chisel2.auth_and_issue(domains) # create a new client and re-authz client = chisel2.make_client(None) chisel2.auth_and_issue(domains, client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) client.revoke(josepy.ComparableX509(cert), 0)
def test_revoke_by_authz(): domains = [random_domain()] order = chisel2.auth_and_issue(domains) # create a new client and re-authz client = chisel2.make_client(None) chisel2.auth_and_issue(domains, client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) client.revoke(josepy.ComparableX509(cert), 0)
def test_tls_alpn_challenge(): # Pick two random domains domains = [random_domain(), random_domain()] # Add A records for these domains to ensure the VA's requests are directed # to the interface that the challtestsrv has bound for TLS-ALPN-01 challenge # responses for host in domains: challSrv.add_a_record(host, ["10.88.88.88"]) chisel2.auth_and_issue(domains, chall_type="tls-alpn-01") for host in domains: challSrv.remove_a_record(host)
def test_tls_alpn_challenge(): # Pick two random domains domains = [random_domain(),random_domain()] # Add A records for these domains to ensure the VA's requests are directed # to the interface that the challtestsrv has bound for TLS-ALPN-01 challenge # responses for host in domains: challSrv.add_a_record(host, ["10.88.88.88"]) chisel2.auth_and_issue(domains, chall_type="tls-alpn-01") for host in domains: challSrv.remove_a_record(host)
def test_revoke_by_issuer(): client = make_client(None) order = auth_and_issue([random_domain()], client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) client.revoke(jose.ComparableX509(cert), 0)
def test_delete_unused_challenges(): order = chisel2.auth_and_issue([random_domain()], chall_type="dns-01") a = order.authorizations[0] if len(a.body.challenges) != 1: raise Exception("too many challenges (%d) left after validation" % len(a.body.challenges)) if not isinstance(a.body.challenges[0].chall, challenges.DNS01): raise Exception("wrong challenge type left after validation") # intentionally fail a challenge client = chisel2.make_client() csr_pem = chisel2.make_csr([random_domain()]) order = client.new_order(csr_pem) c = chisel2.get_chall(order.authorizations[0], challenges.DNS01) client.answer_challenge(c, c.response(client.net.key)) for _ in range(5): a, _ = client.poll(order.authorizations[0]) if a.body.status == Status("invalid"): break time.sleep(1) if len(a.body.challenges) != 1: raise Exception( "too many challenges (%d) left after failed validation" % len(a.body.challenges)) if not isinstance(a.body.challenges[0].chall, challenges.DNS01): raise Exception("wrong challenge type left after validation")
def test_sct_embedding(): if not os.environ.get('BOULDER_CONFIG_DIR', '').startswith("test/config-next"): return order = chisel2.auth_and_issue([random_domain()]) cert = x509.load_pem_x509_certificate(str(order.fullchain_pem), default_backend()) # make sure there is no poison extension try: cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")) raise Exception("certificate contains CT poison extension") except x509.ExtensionNotFound: # do nothing pass # make sure there is a SCT list extension try: sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")) except x509.ExtensionNotFound: raise Exception("certificate doesn't contain SCT list extension") if len(sctList.value) != 2: raise Exception("SCT list contains wrong number of SCTs") for sct in sctList.value: if sct.version != x509.certificate_transparency.Version.v1: raise Exception("SCT contains wrong version") if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE: raise Exception("SCT contains wrong entry type")
def test_sct_embedding(): order = chisel2.auth_and_issue([random_domain()]) cert = x509.load_pem_x509_certificate(str(order.fullchain_pem), default_backend()) # make sure there is no poison extension try: cert.extensions.get_extension_for_oid( x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")) raise Exception("certificate contains CT poison extension") except x509.ExtensionNotFound: # do nothing pass # make sure there is a SCT list extension try: sctList = cert.extensions.get_extension_for_oid( x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")) except x509.ExtensionNotFound: raise Exception("certificate doesn't contain SCT list extension") if len(sctList.value) != 2: raise Exception("SCT list contains wrong number of SCTs") for sct in sctList.value: if sct.version != x509.certificate_transparency.Version.v1: raise Exception("SCT contains wrong version") if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE: raise Exception("SCT contains wrong entry type")
def z2_disable_setup(): global z2_disable_client global z2_disable_authz global z2_disable_order z2_disable_client = chisel2.make_client() z2_disable_order = chisel2.auth_and_issue([random_domain()]) z2_disable_authz = z2_disable_order.authorizations[0]
def test_bad_overlap_wildcard(): if not os.environ.get('BOULDER_CONFIG_DIR', '').startswith("test/config-next"): return chisel2.expect_problem( "urn:ietf:params:acme:error:malformed", lambda: chisel2.auth_and_issue(["*.example.com", "www.example.com"]))
def test_failed_validation_limit(): """ Fail a challenge repeatedly for the same domain, with the same account. Once we reach the rate limit we should get a rateLimitedError. Note that this depends on the specific threshold configured in rate-limit-policies.yml. This also incidentally tests a fix for https://github.com/letsencrypt/boulder/issues/4329. We expect to get ValidationErrors, eventually followed by a rate limit error. """ domain = "fail." + random_domain() csr_pem = chisel2.make_csr([domain]) client = chisel2.make_client() threshold = 3 for _ in range(threshold): order = client.new_order(csr_pem) chall = order.authorizations[0].body.challenges[0] client.answer_challenge(chall, chall.response(client.net.key)) try: client.poll_and_finalize(order) except errors.ValidationError as e: pass chisel2.expect_problem( "urn:ietf:params:acme:error:rateLimited", lambda: chisel2.auth_and_issue([domain], client=client))
def z1_reuse_setup(): """Runs during "setup_twenty_days_ago" phase.""" global z1_reuse_client global z1_reuse_authzs z1_reuse_client = chisel2.make_client() order = chisel2.auth_and_issue([random_domain(), random_domain()], client=z1_reuse_client) for a in order.authorizations: z1_reuse_authzs.append(a)
def test_http2_http01_challenge(): """ test_http2_http01_challenge tests that an HTTP-01 challenge made to a HTTP/2 server fails with a specific error message for this case. """ client = chisel2.make_client() hostname = "fake.h2.example.com" # Add an A record for the test server to ensure the VA's requests are directed # to the interface that we bind the FakeH2ServerHandler to. challSrv.add_a_record(hostname, ["10.88.88.88"]) # Allow socket address reuse on the base TCPServer class. Failing to do this # causes subsequent integration tests to fail with "Address in use" errors even # though this test _does_ call shutdown() and server_close(). Even though the # server was shut-down Python's socket will be in TIME_WAIT because of prev. client # connections. Having the TCPServer set SO_REUSEADDR on the socket solves # the problem. socketserver.TCPServer.allow_reuse_address = True # Create, start, and wait for a fake HTTP/2 server. server = socketserver.TCPServer(('10.88.88.88', 5002), FakeH2ServerHandler) thread = threading.Thread(target=server.serve_forever) thread.daemon = False thread.start() wait_for_tcp_server('10.88.88.88', 5002) # Issuing an HTTP-01 challenge for this hostname should produce a connection # problem with an error specific to the HTTP/2 misconfiguration. expectedError = "Server is speaking HTTP/2 over HTTP" try: chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") except acme_errors.ValidationError as e: for authzr in e.failed_authzrs: c = chisel2.get_chall(authzr, challenges.HTTP01) error = c.error if error is None or error.typ != "urn:ietf:params:acme:error:connection": raise Exception("Expected connection prob, got %s" % (error.__str__())) if not error.detail.endswith(expectedError): raise Exception("Expected prob detail ending in %s, got %s" % (expectedError, error.detail)) finally: server.shutdown() server.server_close() thread.join()
def test_http2_http01_challenge(): """ test_http2_http01_challenge tests that an HTTP-01 challenge made to a HTTP/2 server fails with a specific error message for this case. """ client = chisel2.make_client() hostname = "fake.h2.example.com" # Add an A record for the test server to ensure the VA's requests are directed # to the interface that we bind the FakeH2ServerHandler to. challSrv.add_a_record(hostname, ["10.88.88.88"]) # Allow socket address reuse on the base TCPServer class. Failing to do this # causes subsequent integration tests to fail with "Address in use" errors even # though this test _does_ call shutdown() and server_close(). Even though the # server was shut-down Python's socket will be in TIME_WAIT because of prev. client # connections. Having the TCPServer set SO_REUSEADDR on the socket solves # the problem. socketserver.TCPServer.allow_reuse_address = True # Create, start, and wait for a fake HTTP/2 server. server = socketserver.TCPServer(('10.88.88.88', 5002), FakeH2ServerHandler) thread = threading.Thread(target = server.serve_forever) thread.daemon = False thread.start() wait_for_tcp_server('10.88.88.88', 5002) # Issuing an HTTP-01 challenge for this hostname should produce a connection # problem with an error specific to the HTTP/2 misconfiguration. expectedError = "Server is speaking HTTP/2 over HTTP" try: chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") except acme_errors.ValidationError as e: for authzr in e.failed_authzrs: c = chisel2.get_chall(authzr, challenges.HTTP01) error = c.error if error is None or error.typ != "urn:ietf:params:acme:error:connection": raise Exception("Expected connection prob, got %s" % (error.__str__())) if not error.detail.endswith(expectedError): raise Exception("Expected prob detail ending in %s, got %s" % (expectedError, error.detail)) finally: server.shutdown() server.server_close() thread.join()
def test_revoke_by_authz(): domains = [random_domain()] order = chisel2.auth_and_issue(domains) # create a new client and re-authz client = chisel2.make_client(None) chisel2.auth_and_issue(domains, client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) reset_akamai_purges() client.revoke(josepy.ComparableX509(cert), 0) cert_file_pem = os.path.join(tempdir, "revokeme.pem") with open(cert_file_pem, "w") as f: f.write(OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert).decode()) ee_ocsp_url = "http://localhost:4002" verify_revocation(cert_file_pem, "test/test-ca2.pem", ee_ocsp_url) verify_akamai_purge()
def test_revoke_by_authz(): domains = [random_domain()] order = chisel2.auth_and_issue(domains) # create a new client and re-authz client = chisel2.make_client(None) chisel2.auth_and_issue(domains, client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) reset_akamai_purges() client.revoke(josepy.ComparableX509(cert), 0) cert_file_pem = os.path.join(tempdir, "revokeme.pem") with open(cert_file_pem, "w") as f: f.write(OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert).decode()) ee_ocsp_url = "http://localhost:4002" verify_revocation(cert_file_pem, "test/test-ca2.pem", ee_ocsp_url) verify_akamai_purge()
def test_http_multiva_threshold_pass(): # Only config-next has remote VAs configured and is appropriate for this # integration test. if not CONFIG_NEXT: return client = chisel2.make_client() # Configure a guestlist that will pass the multiVA threshold test by # allowing the primary VA and one remote. guestlist = {"boulder": 1, "boulder-remote-b": 1} hostname, cleanup = multiva_setup(client, guestlist) try: # With the maximum number of allowed remote VA failures the overall # challenge should still succeed. chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") finally: cleanup()
def test_http_multiva_threshold_pass(): # Only config-next has remote VAs configured and is appropriate for this # integration test. if not CONFIG_NEXT: return client = chisel2.make_client() # Configure a guestlist that will pass the multiVA threshold test by # allowing the primary VA and one remote. guestlist = {"boulder": 1, "boulder-remote-b": 1} hostname, cleanup = multiva_setup(client, guestlist) try: # With the maximum number of allowed remote VA failures the overall # challenge should still succeed. chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") finally: cleanup()
def ocsp_exp_unauth_setup(): client = chisel2.make_client(None) order = chisel2.auth_and_issue([random_domain()], client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) cert_file_pem = os.path.join(tempdir, "to-expire.pem") with open(cert_file_pem, "w") as f: f.write(OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert).decode()) verify_ocsp(cert_file_pem, "test/test-ca2.pem", "http://localhost:4002", "good") global expired_cert_name expired_cert_name = cert_file_pem
def test_wildcard_exactblacklist(): """ Test issuance for a wildcard that would cover an exact blacklist entry. It should fail with a policy error. """ # We include "highrisk.le-test.hoffman-andrews.com" in `test/hostname-policy.json` # Issuing for "*.le-test.hoffman-andrews.com" should be blocked domain = "*.le-test.hoffman-andrews.com" # We expect this to produce a policy problem chisel2.expect_problem("urn:ietf:params:acme:error:rejectedIdentifier", lambda: chisel2.auth_and_issue([domain], chall_type="dns-01"))
def test_auth_deactivation_v2(): client = chisel2.make_client(None) csr_pem = chisel2.make_csr([random_domain()]) order = client.new_order(csr_pem) resp = client.deactivate_authorization(order.authorizations[0]) if resp.body.status is not messages.STATUS_DEACTIVATED: raise Exception("unexpected authorization status") order = chisel2.auth_and_issue([random_domain()], client=client) resp = client.deactivate_authorization(order.authorizations[0]) if resp.body.status is not messages.STATUS_DEACTIVATED: raise Exception("unexpected authorization status")
def test_wildcard_exactblacklist(): """ Test issuance for a wildcard that would cover an exact blacklist entry. It should fail with a policy error. """ # We include "highrisk.le-test.hoffman-andrews.com" in `test/hostname-policy.json` # Issuing for "*.le-test.hoffman-andrews.com" should be blocked domain = "*.le-test.hoffman-andrews.com" # We expect this to produce a policy problem chisel2.expect_problem("urn:ietf:params:acme:error:rejectedIdentifier", lambda: chisel2.auth_and_issue([domain], chall_type="dns-01"))
def test_highrisk_blocklist(): """ Test issuance for a subdomain of a HighRiskBlockedNames entry. It should fail with a policy error. """ # We include "example.org" in `test/hostname-policy.yaml` in the # HighRiskBlockedNames list so issuing for "foo.example.org" should be # blocked. domain = "foo.example.org" # We expect this to produce a policy problem chisel2.expect_problem("urn:ietf:params:acme:error:rejectedIdentifier", lambda: chisel2.auth_and_issue([domain], chall_type="dns-01"))
def test_http_multiva_threshold_pass(): # Only config-next has remote VAs configured and is appropriate for this # integration test. if not CONFIG_NEXT: return client = chisel2.make_client() # These values should match the config in `config-next/va.json` remoteVAs = 2 maxFailures = 1 # Configure a bounceFirst value that will pass the multiVA threshold test. bounceFirst = (remoteVAs - maxFailures) + 1 hostname, cleanup = multiva_setup(client, bounceFirst) try: # With the maximum number of allowed remote VA failures the overall # challenge should still succeed. chisel2.auth_and_issue([hostname], client=client, chall_type="http-01") finally: cleanup()
def test_http_challenge_broken_redirect(): """ test_http_challenge_broken_redirect tests that a common webserver mis-configuration receives the correct specialized error message when attempting an HTTP-01 challenge. """ client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a broken HTTP redirect similar to a sort we see frequently "in the wild" challengePath = "/.well-known/acme-challenge/{0}".format(token) redirect = "http://{0}.well-known/acme-challenge/bad-bad-bad".format(d) challSrv.add_http_redirect(challengePath, redirect) # Expect the specialized error message expectedError = "Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format( redirect, d) # NOTE(@cpu): Can't use chisel2.expect_problem here because it doesn't let # us interrogate the detail message easily. try: chisel2.auth_and_issue([d], client=client, chall_type="http-01") except acme_errors.ValidationError as e: for authzr in e.failed_authzrs: c = chisel2.get_chall(authzr, challenges.HTTP01) error = c.error if error is None or error.typ != "urn:ietf:params:acme:error:connection": raise Exception("Expected connection prob, got %s" % (error.__str__())) if error.detail != expectedError: raise Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)) challSrv.remove_http_redirect(challengePath)
def test_http_challenge_broken_redirect(): """ test_http_challenge_broken_redirect tests that a common webserver mis-configuration receives the correct specialized error message when attempting an HTTP-01 challenge. """ client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a broken HTTP redirect similar to a sort we see frequently "in the wild" challengePath = "/.well-known/acme-challenge/{0}".format(token) redirect = "http://{0}.well-known/acme-challenge/bad-bad-bad".format(d) challSrv.add_http_redirect( challengePath, redirect) # Expect the specialized error message expectedError = "Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format(redirect, d) # NOTE(@cpu): Can't use chisel2.expect_problem here because it doesn't let # us interrogate the detail message easily. try: chisel2.auth_and_issue([d], client=client, chall_type="http-01") except acme_errors.ValidationError as e: for authzr in e.failed_authzrs: c = chisel2.get_chall(authzr, challenges.HTTP01) error = c.error if error is None or error.typ != "urn:ietf:params:acme:error:connection": raise Exception("Expected connection prob, got %s" % (error.__str__())) if error.detail != expectedError: raise Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)) challSrv.remove_http_redirect(challengePath)
def test_http_challenge_loop_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a HTTP redirect from the challenge's validation path to itself challengePath = "/.well-known/acme-challenge/{0}".format(token) challSrv.add_http_redirect( challengePath, "http://{0}{1}".format(d, challengePath)) # Issuing for the the name should fail because of the challenge domains's # redirect loop. chisel2.expect_problem("urn:ietf:params:acme:error:connection", lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01")) challSrv.remove_http_redirect(challengePath)
def test_http_challenge_loop_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a HTTP redirect from the challenge's validation path to itself challengePath = "/.well-known/acme-challenge/{0}".format(token) challSrv.add_http_redirect( challengePath, "http://{0}{1}".format(d, challengePath)) # Issuing for the the name should fail because of the challenge domains's # redirect loop. chisel2.expect_problem("urn:ietf:params:acme:error:connection", lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01")) challSrv.remove_http_redirect(challengePath)
def test_revoke_by_issuer(): client = chisel2.make_client(None) order = chisel2.auth_and_issue([random_domain()], client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) reset_akamai_purges() client.revoke(josepy.ComparableX509(cert), 0) cert_file_pem = os.path.join(tempdir, "revokeme.pem") with open(cert_file_pem, "w") as f: f.write( OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert).decode()) ee_ocsp_url = "http://localhost:4002" if default_config_dir.startswith("test/config-next"): verify_revocation(cert_file_pem, "test/test-ca2.pem", ee_ocsp_url) else: wait_for_ocsp_revoked(cert_file_pem, "test/test-ca2.pem", ee_ocsp_url) verify_akamai_purge()
def test_http_challenge_badproto_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a HTTP redirect from the challenge's validation path to whacky # non-http/https protocol URL. challengePath = "/.well-known/acme-challenge/{0}".format(token) challSrv.add_http_redirect( challengePath, "gopher://{0}{1}".format(d, challengePath)) # Issuing for the name should cause a connection error because the redirect # domain name is an IP address. chisel2.expect_problem("urn:ietf:params:acme:error:connection", lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01")) challSrv.remove_http_redirect(challengePath)
def test_http_challenge_badproto_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Create a HTTP redirect from the challenge's validation path to whacky # non-http/https protocol URL. challengePath = "/.well-known/acme-challenge/{0}".format(token) challSrv.add_http_redirect( challengePath, "gopher://{0}{1}".format(d, challengePath)) # Issuing for the name should cause a connection error because the redirect # domain name is an IP address. chisel2.expect_problem("urn:ietf:params:acme:error:connection", lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01")) challSrv.remove_http_redirect(challengePath)
def test_z1_reuse(): """Test that authzv1's get reused alongside authzv2's once the NewAuthorizationSchema flag is turned on. This relies on the fact that when CONFIG_NEXT is true, the n_days_ago setup phases get run with `test/config` rather than `test/config-next`. """ if not CONFIG_NEXT: return reuse_domains = [] authz_uris = set() for a in z1_reuse_authzs: authz_uris.add(a.uri) reuse_domains.append(a.body.identifier.value) new_domains = [random_domain(), random_domain()] order = chisel2.auth_and_issue(reuse_domains + new_domains, client=z1_reuse_client) for a in order.authorizations: if a.uri in authz_uris: authz_uris.remove(a.uri) if len(authz_uris) != 0: raise Exception("Failed to reuse all authzs. Remaining: %s" % authz_uris)
def run_fuzz_configs(rounds): fuzzy_configs = config_fuzzer.fuzz(rounds) # for challenge in ["http-01", "dns-01", "tls-alpn-01"]: #TODO: do i really need these different auth mechanisms? for challenge in [ "http-01" ]: #TODO: do i really need these different auth mechanisms? if challenge == "tls-alpn-01": challSrv.add_a_record( "test.domain.com", ["10.88.88.88"]) # this domain is in config_fuzzer.py for config in fuzzy_configs: config_fuzzer.write_config(config, "test/fuzz-configs") try: order = chisel2.auth_and_issue(["test.domain.com"], chall_type=challenge) print("CERT", order.fullchain_pem) except Exception: traceback.print_exc() if challenge == "tls-alpn-01": challSrv.remove_a_record( "test.domain.com") # this domain is in config_fuzzer.py
def test_revoke_by_issuer(): client = chisel2.make_client(None) order = chisel2.auth_and_issue([random_domain()], client=client) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) client.revoke(josepy.ComparableX509(cert), 0)
def test_tls_alpn_challenge(): if not default_config_dir.startswith("test/config-next"): return chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="tls-alpn-01")
def test_http_challenge(): chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="http-01")
def test_wildcardmultidomain(): """ Test issuance for a random domain and a random wildcard domain using DNS-01. """ chisel2.auth_and_issue([random_domain(), "*." + random_domain()], chall_type="dns-01")
def test_http_challenge_https_redirect(): client = chisel2.make_client() # Create an authz for a random domain and get its HTTP-01 challenge token d, chall = rand_http_chall(client) token = chall.encode("token") # Calculate its keyauth so we can add it in a special non-standard location # for the redirect result resp = chall.response(client.net.key) keyauth = resp.key_authorization challSrv.add_http01_response("https-redirect", keyauth) # Create a HTTP redirect from the challenge's validation path to an HTTPS # path with some parameters challengePath = "/.well-known/acme-challenge/{0}".format(token) redirectPath = "/.well-known/acme-challenge/https-redirect?params=are&important=to¬=lose" challSrv.add_http_redirect( challengePath, "https://{0}{1}".format(d, redirectPath)) # Also add an A record for the domain pointing to the interface that the # HTTPS HTTP-01 challtestsrv is bound. challSrv.add_a_record(d, ["10.77.77.77"]) try: chisel2.auth_and_issue([d], client=client, chall_type="http-01") except errors.ValidationError as e: problems = [] for authzr in e.failed_authzrs: for chall in authzr.body.challenges: error = chall.error if error: problems.append(error.__str__()) raise Exception("validation problem: %s" % "; ".join(problems)) challSrv.remove_http_redirect(challengePath) challSrv.remove_a_record(d) history = challSrv.http_request_history(d) challSrv.clear_http_request_history(d) # There should have been at least two GET requests made to the challtestsrv by the VA if len(history) < 2: raise Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))) initialRequests = [] redirectedRequests = [] for request in history: # Initial requests should have the expected initial HTTP-01 URL for the challenge if request['URL'] == challengePath: initialRequests.append(request) # Redirected requests should have the expected redirect path URL with all # its parameters elif request['URL'] == redirectPath: redirectedRequests.append(request) else: raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) # There should have been at least 1 initial HTTP-01 validation request. if len(initialRequests) < 1: raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) # All initial requests should have been over HTTP for r in initialRequests: if r['HTTPS'] is True: raise Exception("Expected all initial requests to be HTTP, got %s" % r) # There should have been at least 1 redirected HTTP request for each VA if len(redirectedRequests) < 1: raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))) # All the redirected requests should have been over HTTPS with the correct # SNI value for r in redirectedRequests: if r['HTTPS'] is False: raise Exception("Expected all redirected requests to be HTTPS") # TODO(@cpu): The following ServerName test will fail with config-next # until https://github.com/letsencrypt/boulder/issues/3969 is fixed. if CONFIG_NEXT: return elif r['ServerName'] != d: raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName']))
def test_bad_overlap_wildcard(): chisel2.expect_problem("urn:ietf:params:acme:error:malformed", lambda: chisel2.auth_and_issue(["*.example.com", "www.example.com"]))
def test_multidomain(): chisel2.auth_and_issue([random_domain(), random_domain()])
def test_wildcardmultidomain(): """ Test issuance for a random domain and a random wildcard domain using DNS-01. """ chisel2.auth_and_issue([random_domain(), "*."+random_domain()], chall_type="dns-01")
def test_http_challenge(): chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="http-01")
def test_multidomain(): auth_and_issue([random_domain(), random_domain()])
def test_bad_overlap_wildcard(): if not os.environ.get('BOULDER_CONFIG_DIR', '').startswith("test/config-next"): return chisel2.expect_problem("urn:ietf:params:acme:error:malformed", lambda: chisel2.auth_and_issue(["*.example.com", "www.example.com"]))