integration: use python3 (#4582)

Python 2 is over in 1 month 4 days: https://pythonclock.org/

This rolls forward most of the changes in #4313.

The original change was rolled back in #4323 because it
broke `docker-compose up`. This change fixes those original issues by
(a) making sure `requests` is installed and (b) sourcing a virtualenv
containing the `requests` module before running start.py.

Other notable changes in this:
 - Certbot has changed the developer instructions to install specific packages
rather than rely on `letsencrypt-auto --os-packages-only`, so we follow suit.
 - Python3 now has a `bytes` type that is used in some places that used to
provide `str`, and all `str` are now Unicode. That means going from `bytes` to
`str` and back requires explicit `.decode()` and `.encode()`.
 - Moved from urllib2 to requests in many places.
This commit is contained in:
Jacob Hoffman-Andrews 2019-11-28 06:54:58 -08:00 committed by Daniel McCarney
parent 211985eae7
commit 1146eecac3
15 changed files with 206 additions and 189 deletions

View File

@ -2,13 +2,16 @@ version: '3'
services: services:
boulder: boulder:
# To minimize fetching this should be the same version used below # To minimize fetching this should be the same version used below
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-10-17 image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-11-18
environment: environment:
FAKE_DNS: 10.77.77.77 FAKE_DNS: 10.77.77.77
PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657 PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657
BOULDER_CONFIG_DIR: test/config BOULDER_CONFIG_DIR: test/config
GO111MODULE: "on" GO111MODULE: "on"
GOFLAGS: "-mod=vendor" GOFLAGS: "-mod=vendor"
# This is required so Python doesn't throw an error when printing
# non-ASCII to stdout.
PYTHONIOENCODING: "utf-8"
volumes: volumes:
- .:/go/src/github.com/letsencrypt/boulder - .:/go/src/github.com/letsencrypt/boulder
- ./.gocache:/root/.cache/go-build - ./.gocache:/root/.cache/go-build
@ -55,7 +58,7 @@ services:
working_dir: /go/src/github.com/letsencrypt/boulder working_dir: /go/src/github.com/letsencrypt/boulder
bhsm: bhsm:
# To minimize fetching this should be the same version used above # To minimize fetching this should be the same version used above
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-10-17 image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-11-18
environment: environment:
PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657 PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657
command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm2.so command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm2.so
@ -82,7 +85,7 @@ services:
logging: logging:
driver: none driver: none
netaccess: netaccess:
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-10-17 image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.13.2}:2019-11-18
environment: environment:
GO111MODULE: "on" GO111MODULE: "on"
GOFLAGS: "-mod=vendor" GOFLAGS: "-mod=vendor"

View File

@ -25,8 +25,8 @@ try:
startservers.check() startservers.check()
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print "\nstopping servers." print("\nstopping servers.")
except OSError, v: except OSError as v:
# Ignore EINTR, which happens when we get SIGTERM or SIGINT (i.e. when # Ignore EINTR, which happens when we get SIGTERM or SIGINT (i.e. when
# someone hits Ctrl-C after running docker-compose up or start.py. # someone hits Ctrl-C after running docker-compose up or start.py.
if v.errno != errno.EINTR: if v.errno != errno.EINTR:

View File

@ -103,9 +103,9 @@ if [[ "$RUN" =~ "integration" ]] ; then
args+=("--filter" "${INT_FILTER}") args+=("--filter" "${INT_FILTER}")
fi fi
source ${CERTBOT_PATH:-/certbot}/${VENV_NAME:-venv}/bin/activate source ${CERTBOT_PATH:-/certbot}/${VENV_NAME:-venv3}/bin/activate
DIRECTORY=http://boulder:4000/directory \ DIRECTORY=http://boulder:4000/directory \
python2 test/integration-test.py --chisel --gotest "${args[@]}" python3 test/integration-test.py --chisel --gotest "${args[@]}"
fi fi
# Test that just ./start.py works, which is a proxy for testing that # Test that just ./start.py works, which is a proxy for testing that

View File

@ -9,4 +9,5 @@ RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
WORKDIR $GOPATH WORKDIR $GOPATH
ADD build.sh /tmp/build.sh ADD build.sh /tmp/build.sh
ADD requirements.txt /tmp/requirements.txt
RUN /tmp/build.sh RUN /tmp/build.sh

View File

@ -16,14 +16,21 @@ apt-get install -y --no-install-recommends \
ruby \ ruby \
ruby-dev \ ruby-dev \
rsyslog \ rsyslog \
python3-venv \
softhsm \ softhsm \
build-essential \ build-essential \
cmake \ cmake \
libssl-dev \ libssl-dev \
libseccomp-dev \ libseccomp-dev \
opensc \ opensc \
unzip unzip \
python3-dev \
python3-venv \
gcc \
libaugeas0 \
libssl-dev \
libffi-dev \
ca-certificates \
openssl
curl -L https://github.com/google/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip -o /tmp/protoc.zip curl -L https://github.com/google/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip -o /tmp/protoc.zip
unzip /tmp/protoc.zip -d /usr/local/protoc unzip /tmp/protoc.zip -d /usr/local/protoc
@ -48,8 +55,9 @@ go get \
git clone https://github.com/certbot/certbot /certbot git clone https://github.com/certbot/certbot /certbot
cd /certbot cd /certbot
./letsencrypt-auto --os-packages-only ./tools/venv3.py
./tools/venv.py source venv3/bin/activate
pip install -r /tmp/requirements.txt
cd - cd -
# Install pkcs11-proxy. Checked out commit was master HEAD at time # Install pkcs11-proxy. Checked out commit was master HEAD at time

View File

@ -1,5 +1,5 @@
import urllib2
import json import json
import requests
class ChallTestServer: class ChallTestServer:
""" """
@ -41,9 +41,10 @@ class ChallTestServer:
self._baseURL = url self._baseURL = url
def _postURL(self, url, body): def _postURL(self, url, body):
return urllib2.urlopen( response = requests.post(
url, url,
data=json.dumps(body)).read() data=json.dumps(body))
return response.text
def _URL(self, path): def _URL(self, path):
urlPath = self._paths.get(path, None) urlPath = self._paths.get(path, None)

View File

@ -14,7 +14,7 @@ import socket
import sys import sys
import threading import threading
import time import time
import urllib2 import requests
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -73,9 +73,9 @@ def update_email(client, email):
email. email.
""" """
if client is None: if client is None:
raise NoClientError("update_email requires a valid acme.Client argument") raise(NoClientError("update_email requires a valid acme.Client argument"))
if email is None: if email is None:
raise EmailRequiredError("update_email requires an email argument") raise(EmailRequiredError("update_email requires an email argument"))
if not email.startswith("mailto:"): if not email.startswith("mailto:"):
email = "mailto:"+ email email = "mailto:"+ email
acct = client.account acct = client.account
@ -86,7 +86,7 @@ def get_chall(authz, typ):
for chall_body in authz.body.challenges: for chall_body in authz.body.challenges:
if isinstance(chall_body.chall, typ): if isinstance(chall_body.chall, typ):
return chall_body return chall_body
raise Exception("No %s challenge found" % typ) raise(Exception("No %s challenge found" % typ))
class ValidationError(Exception): class ValidationError(Exception):
"""An error that occurs during challenge validation.""" """An error that occurs during challenge validation."""
@ -110,9 +110,9 @@ def issue(client, authzs, cert_output=None):
csr = OpenSSL.crypto.X509Req() csr = OpenSSL.crypto.X509Req()
csr.add_extensions([ csr.add_extensions([
OpenSSL.crypto.X509Extension( OpenSSL.crypto.X509Extension(
'subjectAltName', 'subjectAltName'.encode(),
critical=False, critical=False,
value=', '.join('DNS:' + d for d in domains).encode() value=(', '.join('DNS:' + d for d in domains)).encode()
), ),
]) ])
csr.set_pubkey(pkey) csr.set_pubkey(pkey)
@ -126,19 +126,21 @@ def issue(client, authzs, cert_output=None):
# If we get a PollError, pick the first failed authz and turn it into a more # If we get a PollError, pick the first failed authz and turn it into a more
# useful ValidationError that contains details we can look for in tests. # useful ValidationError that contains details we can look for in tests.
for authz in error.updated: for authz in error.updated:
updated_authz = json.loads(urllib2.urlopen(authz.uri).read()) r = requests.get(authz.uri)
r.raise_for_status()
updated_authz = r.json()
domain = authz.body.identifier.value, domain = authz.body.identifier.value,
for c in updated_authz['challenges']: for c in updated_authz['challenges']:
if 'error' in c: if 'error' in c:
err = c['error'] err = c['error']
raise ValidationError(domain, err['type'], err['detail']) raise(ValidationError(domain, err['type'], err['detail']))
# If none of the authz's had an error, just re-raise. # If none of the authz's had an error, just re-raise.
raise raise
if cert_output is not None: if cert_output is not None:
pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
cert_resource.body) cert_resource.body)
with open(cert_output, 'w') as f: with open(cert_output, 'w') as f:
f.write(pem) f.write(pem.decode())
return cert_resource return cert_resource
def http_01_answer(client, chall_body): def http_01_answer(client, chall_body):
@ -223,7 +225,7 @@ def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, c
elif chall_type == "tls-alpn-01": elif chall_type == "tls-alpn-01":
cleanup = do_tlsalpn_challenges(client, authzs) cleanup = do_tlsalpn_challenges(client, authzs)
else: else:
raise Exception("invalid challenge type %s" % chall_type) raise(Exception("invalid challenge type %s" % chall_type))
try: try:
cert_resource = issue(client, authzs, cert_output) cert_resource = issue(client, authzs, cert_output)
@ -250,15 +252,15 @@ def expect_problem(problem_type, func):
else: else:
raise raise
if not ok: if not ok:
raise Exception('Expected %s, got no error' % problem_type) raise(Exception('Expected %s, got no error' % problem_type))
if __name__ == "__main__": if __name__ == "__main__":
domains = sys.argv[1:] domains = sys.argv[1:]
if len(domains) == 0: if len(domains) == 0:
print __doc__ print(__doc__)
sys.exit(0) sys.exit(0)
try: try:
auth_and_issue(domains) auth_and_issue(domains)
except messages.Error, e: except messages.Error as e:
print e print(e)
sys.exit(1) sys.exit(1)

View File

@ -15,7 +15,6 @@ import sys
import signal import signal
import threading import threading
import time import time
import urllib2
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
@ -189,10 +188,10 @@ if __name__ == "__main__":
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
domains = sys.argv[1:] domains = sys.argv[1:]
if len(domains) == 0: if len(domains) == 0:
print __doc__ print(__doc__)
sys.exit(0) sys.exit(0)
try: try:
auth_and_issue(domains) auth_and_issue(domains)
except messages.Error, e: except messages.Error as e:
print e print(e)
sys.exit(1) sys.exit(1)

View File

@ -51,7 +51,11 @@ if [ -n "${PKCS11_PROXY_SOCKET:-}" ]; then
fi fi
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
exec ./start.py # the activate script touches PS1, which is undefined, so we have to relax
# the "fail on undefined" setting here.
set +u
source ${CERTBOT_PATH:-/certbot}/${VENV_NAME:-venv3}/bin/activate
exec python3 ./start.py
fi fi
exec $@ exec $@

View File

@ -1,11 +1,11 @@
#!/usr/bin/env python2.7
import base64 import base64
import os import os
import urllib2 import urllib
import time import time
import re import re
import random import random
import json import json
import requests
import socket import socket
import tempfile import tempfile
import shutil import shutil
@ -38,20 +38,20 @@ def random_domain():
return "rand.%x.xyz" % random.randrange(2**32) return "rand.%x.xyz" % random.randrange(2**32)
def run(cmd, **kwargs): def run(cmd, **kwargs):
return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, **kwargs) return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, **kwargs).decode()
def fetch_ocsp(request_bytes, url): def fetch_ocsp(request_bytes, url):
"""Fetch an OCSP response using POST, GET, and GET with URL encoding. """Fetch an OCSP response using POST, GET, and GET with URL encoding.
Returns a tuple of the responses. Returns a tuple of the responses.
""" """
ocsp_req_b64 = base64.b64encode(request_bytes) ocsp_req_b64 = base64.b64encode(request_bytes).decode()
# Make the OCSP request three different ways: by POST, by GET, and by GET with # Make the OCSP request three different ways: by POST, by GET, and by GET with
# URL-encoded parameters. All three should have an identical response. # URL-encoded parameters. All three should have an identical response.
get_response = urllib2.urlopen("%s/%s" % (url, ocsp_req_b64)).read() get_response = requests.get("%s/%s" % (url, ocsp_req_b64)).content
get_encoded_response = urllib2.urlopen("%s/%s" % (url, urllib2.quote(ocsp_req_b64, safe = ""))).read() get_encoded_response = requests.get("%s/%s" % (url, urllib.parse.quote(ocsp_req_b64, safe = ""))).content
post_response = urllib2.urlopen("%s/" % (url), request_bytes).read() post_response = requests.post("%s/" % (url), data=request_bytes).content
return (post_response, get_response, get_encoded_response) return (post_response, get_response, get_encoded_response)
@ -61,13 +61,13 @@ def make_ocsp_req(cert_file, issuer_file):
# First generate the OCSP request in DER form # First generate the OCSP request in DER form
run("openssl ocsp -no_nonce -issuer %s -cert %s -reqout %s" % ( run("openssl ocsp -no_nonce -issuer %s -cert %s -reqout %s" % (
issuer_file, cert_file, ocsp_req_file)) issuer_file, cert_file, ocsp_req_file))
with open(ocsp_req_file) as f: with open(ocsp_req_file, mode='rb') as f:
ocsp_req = f.read() ocsp_req = f.read()
return ocsp_req return ocsp_req
def ocsp_verify(cert_file, issuer_file, ocsp_response): def ocsp_verify(cert_file, issuer_file, ocsp_response):
ocsp_resp_file = os.path.join(tempdir, "ocsp.resp") ocsp_resp_file = os.path.join(tempdir, "ocsp.resp")
with open(ocsp_resp_file, "w") as f: with open(ocsp_resp_file, "wb") as f:
f.write(ocsp_response) f.write(ocsp_response)
output = run("openssl ocsp -no_nonce -issuer %s -cert %s \ output = run("openssl ocsp -no_nonce -issuer %s -cert %s \
-verify_other %s -CAfile test/test-root.pem \ -verify_other %s -CAfile test/test-root.pem \
@ -76,8 +76,8 @@ def ocsp_verify(cert_file, issuer_file, ocsp_response):
# also look for the string "Response Verify Failure" # also look for the string "Response Verify Failure"
verify_failure = "Response Verify Failure" verify_failure = "Response Verify Failure"
if re.search(verify_failure, output): if re.search(verify_failure, output):
print output print(output)
raise Exception("OCSP verify failure") raise(Exception("OCSP verify failure"))
return output return output
def verify_ocsp(cert_file, issuer_file, url, status): def verify_ocsp(cert_file, issuer_file, url, status):
@ -87,28 +87,28 @@ def verify_ocsp(cert_file, issuer_file, url, status):
# Verify all responses are the same # Verify all responses are the same
for resp in responses: for resp in responses:
if resp != responses[0]: if resp != responses[0]:
raise Exception("OCSP responses differed: %s vs %s" %( raise(Exception("OCSP responses differed: %s vs %s" %(
base64.b64encode(responses[0]), base64.b64encode(resp))) base64.b64encode(responses[0]), base64.b64encode(resp))))
# Check response is for the correct certificate and is correct # Check response is for the correct certificate and is correct
# status # status
resp = responses[0] resp = responses[0]
verify_output = ocsp_verify(cert_file, issuer_file, resp) verify_output = ocsp_verify(cert_file, issuer_file, resp)
if not re.search("%s: %s" % (cert_file, status), verify_output): if not re.search("%s: %s" % (cert_file, status), verify_output):
print verify_output print(verify_output)
raise Exception("OCSP response wasn't '%s'" % status) raise(Exception("OCSP response wasn't '%s'" % status))
def reset_akamai_purges(): def reset_akamai_purges():
urllib2.urlopen("http://localhost:6789/debug/reset-purges", "{}") requests.post("http://localhost:6789/debug/reset-purges", data="{}")
def verify_akamai_purge(): def verify_akamai_purge():
deadline = time.time() + 0.25 deadline = time.time() + 0.25
while True: while True:
time.sleep(0.05) time.sleep(0.05)
if time.time() > deadline: if time.time() > deadline:
raise Exception("Timed out waiting for Akamai purge") raise(Exception("Timed out waiting for Akamai purge"))
response = urllib2.urlopen("http://localhost:6789/debug/get-purges") response = requests.get("http://localhost:6789/debug/get-purges")
purgeData = json.load(response) purgeData = response.json()
if len(purgeData["V3"]) is not 1: if len(purgeData["V3"]) is not 1:
continue continue
break break
@ -150,7 +150,7 @@ def waitport(port, prog, perTickCheck=None):
return True return True
except socket.error as e: except socket.error as e:
if e.errno == errno.ECONNREFUSED: if e.errno == errno.ECONNREFUSED:
print "Waiting for debug port %d (%s)" % (port, prog) print("Waiting for debug port %d (%s)" % (port, prog))
else: else:
raise raise
raise Exception("timed out waiting for debug port %d (%s)" % (port, prog)) raise(Exception("timed out waiting for debug port %d (%s)" % (port, prog)))

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2.7 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
This file contains basic infrastructure for running the integration test cases. This file contains basic infrastructure for running the integration test cases.
@ -60,13 +60,13 @@ def run_expired_authz_purger():
tool = "expired-authz-purger2" tool = "expired-authz-purger2"
out = get_future_output("./bin/expired-authz-purger2 --single-run --config cmd/expired-authz-purger2/config.json", target_time) out = get_future_output("./bin/expired-authz-purger2 --single-run --config cmd/expired-authz-purger2/config.json", target_time)
if 'via FAKECLOCK' not in out: if 'via FAKECLOCK' not in out:
raise Exception("%s was not built with `integration` build tag" % (tool)) raise(Exception("%s was not built with `integration` build tag" % (tool)))
if num is None: if num is None:
return return
expected_output = 'deleted %d expired authorizations' % (num) expected_output = 'deleted %d expired authorizations' % (num)
if expected_output not in out: if expected_output not in out:
raise Exception("%s did not print '%s'. Output:\n%s" % ( raise(Exception("%s did not print '%s'. Output:\n%s" % (
tool, expected_output, out)) tool, expected_output, out)))
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
@ -112,7 +112,7 @@ def run_janitor():
def get_stat_line(port, stat): def get_stat_line(port, stat):
url = "http://localhost:%d/metrics" % port url = "http://localhost:%d/metrics" % port
response = requests.get(url) response = requests.get(url)
for l in response.content.split("\n"): for l in response.text.split("\n"):
if l.strip().startswith(stat): if l.strip().startswith(stat):
return l return l
return None return None
@ -120,7 +120,7 @@ def run_janitor():
def stat_value(line): def stat_value(line):
parts = line.split(" ") parts = line.split(" ")
if len(parts) != 2: if len(parts) != 2:
raise Exception("stat line {0} was missing required parts".format(line)) raise(Exception("stat line {0} was missing required parts".format(line)))
return parts[1] return parts[1]
# Wait for the janitor to finish its work. The easiest way to tell this # Wait for the janitor to finish its work. The easiest way to tell this
@ -129,7 +129,7 @@ def run_janitor():
attempts = 0 attempts = 0
while True: while True:
if attempts > 5: if attempts > 5:
raise Exception("timed out waiting for janitor workbatch counts to stabilize") raise(Exception("timed out waiting for janitor workbatch counts to stabilize"))
certStatusWorkBatch = get_stat_line(8014, statline("workbatch", "certificateStatus")) certStatusWorkBatch = get_stat_line(8014, statline("workbatch", "certificateStatus"))
certsWorkBatch = get_stat_line(8014, statline("workbatch", "certificates")) certsWorkBatch = get_stat_line(8014, statline("workbatch", "certificates"))
@ -166,7 +166,7 @@ def run_janitor():
for l in [certStatusDeletes, certsDeletes, certsPerNameDeletes, ordersDeletes]: for l in [certStatusDeletes, certsDeletes, certsPerNameDeletes, ordersDeletes]:
if stat_value(l) == "0": if stat_value(l) == "0":
raise Exception("Expected a non-zero number of deletes to be performed. Found {0}".format(l)) raise(Exception("Expected a non-zero number of deletes to be performed. Found {0}".format(l)))
# Check that all error stats are empty # Check that all error stats are empty
errorStats = [ errorStats = [
@ -178,7 +178,7 @@ def run_janitor():
for eStat in errorStats: for eStat in errorStats:
actual = get_stat_line(8014, eStat) actual = get_stat_line(8014, eStat)
if actual is not None: if actual is not None:
raise Exception("Expected to find no error stat lines but found {0}\n".format(eStat)) raise(Exception("Expected to find no error stat lines but found {0}\n".format(eStat)))
# Terminate the janitor # Terminate the janitor
p.terminate() p.terminate()
@ -221,9 +221,9 @@ def test_stats():
def expect_stat(port, stat): def expect_stat(port, stat):
url = "http://localhost:%d/metrics" % port url = "http://localhost:%d/metrics" % port
response = requests.get(url) response = requests.get(url)
if not stat in response.content: if not stat in response.text:
print(response.content) print(response.content)
raise Exception("%s not present in %s" % (stat, url)) raise(Exception("%s not present in %s" % (stat, url)))
expect_stat(8000, "\nresponse_time_count{") expect_stat(8000, "\nresponse_time_count{")
expect_stat(8000, "\ngo_goroutines ") expect_stat(8000, "\ngo_goroutines ")
expect_stat(8000, '\ngrpc_client_handling_seconds_count{grpc_method="NewRegistration",grpc_service="ra.RegistrationAuthority",grpc_type="unary"} ') expect_stat(8000, '\ngrpc_client_handling_seconds_count{grpc_method="NewRegistration",grpc_service="ra.RegistrationAuthority",grpc_type="unary"} ')
@ -251,27 +251,27 @@ def main():
test_case_filter="", skip_setup=False) test_case_filter="", skip_setup=False)
args = parser.parse_args() args = parser.parse_args()
if not (args.run_certbot or args.run_chisel or args.run_loadtest or args.custom is not None): if not (args.run_certbot or args.run_chisel or args.custom is not None):
raise Exception("must run at least one of the letsencrypt or chisel tests with --certbot, --chisel, or --custom") raise(Exception("must run at least one of the letsencrypt or chisel tests with --certbot, --chisel, or --custom"))
if not args.test_case_filter: if not args.test_case_filter:
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
six_months_ago = now+datetime.timedelta(days=-30*6) six_months_ago = now+datetime.timedelta(days=-30*6)
if not startservers.start(race_detection=True, fakeclock=fakeclock(six_months_ago)): if not startservers.start(race_detection=True, fakeclock=fakeclock(six_months_ago)):
raise Exception("startservers failed (mocking six months ago)") raise(Exception("startservers failed (mocking six months ago)"))
v1_integration.caa_client = caa_client = chisel.make_client() v1_integration.caa_client = caa_client = chisel.make_client()
setup_six_months_ago() setup_six_months_ago()
startservers.stop() startservers.stop()
twenty_days_ago = now+datetime.timedelta(days=-20) twenty_days_ago = now+datetime.timedelta(days=-20)
if not startservers.start(race_detection=True, fakeclock=fakeclock(twenty_days_ago)): if not startservers.start(race_detection=True, fakeclock=fakeclock(twenty_days_ago)):
raise Exception("startservers failed (mocking twenty days ago)") raise(Exception("startservers failed (mocking twenty days ago)"))
setup_twenty_days_ago() setup_twenty_days_ago()
startservers.stop() startservers.stop()
if not startservers.start(race_detection=True, fakeclock=None): if not startservers.start(race_detection=True, fakeclock=None):
raise Exception("startservers failed") raise(Exception("startservers failed"))
if args.run_chisel: if args.run_chisel:
run_chisel(args.test_case_filter) run_chisel(args.test_case_filter)
@ -304,7 +304,7 @@ def main():
run_loadtest() run_loadtest()
if not startservers.check(): if not startservers.check():
raise Exception("startservers.check failed") raise(Exception("startservers.check failed"))
check_slow_queries() check_slow_queries()
@ -390,7 +390,7 @@ def check_balance():
for address in addresses: for address in addresses:
metrics = requests.get("http://%s/metrics" % address) metrics = requests.get("http://%s/metrics" % address)
if not "grpc_server_handled_total" in metrics.text: if not "grpc_server_handled_total" in metrics.text:
raise Exception("no gRPC traffic processed by %s; load balancing problem?" raise(Exception("no gRPC traffic processed by %s; load balancing problem?")
% address) % address)
def run_cert_checker(): def run_cert_checker():
@ -400,7 +400,7 @@ if __name__ == "__main__":
try: try:
main() main()
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise Exception("%s. Output:\n%s" % (e, e.output)) raise(Exception("%s. Output:\n%s" % (e, e.output)))
@atexit.register @atexit.register
def stop(): def stop():

View File

@ -1,5 +1,4 @@
import atexit import atexit
import BaseHTTPServer
import os import os
import shutil import shutil
import signal import signal
@ -98,7 +97,7 @@ def start(race_detection, fakeclock):
print(e) print(e)
return False return False
print "All servers running. Hit ^C to kill." print("All servers running. Hit ^C to kill.")
return True return True
def check(): def check():
@ -116,9 +115,9 @@ def check():
else: else:
busted.append(p) busted.append(p)
if busted: if busted:
print "\n\nThese processes exited early (check above for their output):" print("\n\nThese processes exited early (check above for their output):")
for p in busted: for p in busted:
print "\t'%s' with pid %d exited %d" % (p.cmd, p.pid, p.returncode) print("\t'%s' with pid %d exited %d" % (p.cmd, p.pid, p.returncode))
processes = stillok processes = stillok
return not busted return not busted
@ -129,7 +128,7 @@ def startChallSrv():
""" """
global challSrvProcess global challSrvProcess
if challSrvProcess is not None: if challSrvProcess is not None:
raise Exception("startChallSrv called more than once") raise(Exception("startChallSrv called more than once"))
# NOTE(@cpu): We specify explicit bind addresses for -https01 and # NOTE(@cpu): We specify explicit bind addresses for -https01 and
# --tlsalpn01 here to allow HTTPS HTTP-01 responses on 5001 for on interface # --tlsalpn01 here to allow HTTPS HTTP-01 responses on 5001 for on interface

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import json import json
@ -7,7 +6,6 @@ import random
import re import re
import requests import requests
import time import time
import urllib2
import startservers import startservers
@ -40,7 +38,7 @@ def rand_http_chall(client):
for c in authz.body.challenges: for c in authz.body.challenges:
if isinstance(c.chall, challenges.HTTP01): if isinstance(c.chall, challenges.HTTP01):
return d, c.chall return d, c.chall
raise Exception("No HTTP-01 challenge found for random domain authz") raise(Exception("No HTTP-01 challenge found for random domain authz"))
def test_http_challenge_loop_redirect(): def test_http_challenge_loop_redirect():
client = chisel.make_client() client = chisel.make_client()
@ -158,7 +156,7 @@ def test_http_challenge_http_redirect():
# There should have been at least two GET requests made to the # There should have been at least two GET requests made to the
# challtestsrv. There may have been more if remote VAs were configured. # challtestsrv. There may have been more if remote VAs were configured.
if len(history) < 2: if len(history) < 2:
raise Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))) raise(Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))))
initialRequests = [] initialRequests = []
redirectedRequests = [] redirectedRequests = []
@ -166,7 +164,7 @@ def test_http_challenge_http_redirect():
for request in history: for request in history:
# All requests should have been over HTTP # All requests should have been over HTTP
if request['HTTPS'] is True: if request['HTTPS'] is True:
raise Exception("Expected all requests to be HTTP") raise(Exception("Expected all requests to be HTTP"))
# Initial requests should have the expected initial HTTP-01 URL for the challenge # Initial requests should have the expected initial HTTP-01 URL for the challenge
if request['URL'] == challengePath: if request['URL'] == challengePath:
initialRequests.append(request) initialRequests.append(request)
@ -175,15 +173,15 @@ def test_http_challenge_http_redirect():
elif request['URL'] == redirectPath: elif request['URL'] == redirectPath:
redirectedRequests.append(request) redirectedRequests.append(request)
else: else:
raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) 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. # There should have been at least 1 initial HTTP-01 validation request.
if len(initialRequests) < 1: if len(initialRequests) < 1:
raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) 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 # There should have been at least 1 redirected HTTP request for each VA
if len(redirectedRequests) < 1: if len(redirectedRequests) < 1:
raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))) raise(Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))))
def test_http_challenge_https_redirect(): def test_http_challenge_https_redirect():
client = chisel.make_client() client = chisel.make_client()
@ -220,7 +218,7 @@ def test_http_challenge_https_redirect():
# There should have been at least two GET requests made to the challtestsrv by the VA # There should have been at least two GET requests made to the challtestsrv by the VA
if len(history) < 2: if len(history) < 2:
raise Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))) raise(Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))))
initialRequests = [] initialRequests = []
redirectedRequests = [] redirectedRequests = []
@ -234,26 +232,26 @@ def test_http_challenge_https_redirect():
elif request['URL'] == redirectPath: elif request['URL'] == redirectPath:
redirectedRequests.append(request) redirectedRequests.append(request)
else: else:
raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) 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. # There should have been at least 1 initial HTTP-01 validation request.
if len(initialRequests) < 1: if len(initialRequests) < 1:
raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) 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 # All initial requests should have been over HTTP
for r in initialRequests: for r in initialRequests:
if r['HTTPS'] is True: if r['HTTPS'] is True:
raise Exception("Expected all initial requests to be HTTP") raise(Exception("Expected all initial requests to be HTTP"))
# There should have been at least 1 redirected HTTP request for each VA # There should have been at least 1 redirected HTTP request for each VA
if len(redirectedRequests) < 1: if len(redirectedRequests) < 1:
raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))) 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 # All the redirected requests should have been over HTTPS with the correct
# SNI value # SNI value
for r in redirectedRequests: for r in redirectedRequests:
if r['HTTPS'] is False: if r['HTTPS'] is False:
raise Exception("Expected all redirected requests to be HTTPS") raise(Exception("Expected all redirected requests to be HTTPS"))
elif r['ServerName'] != d: elif r['ServerName'] != d:
raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName'])) raise(Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName'])))
class SlowHTTPRequestHandler(BaseHTTPRequestHandler): class SlowHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
@ -315,7 +313,7 @@ def test_http_challenge_timeout():
# present the timeout is 20s so adding 2s of padding = 22s) # present the timeout is 20s so adding 2s of padding = 22s)
expectedDuration = 22 expectedDuration = 22
if delta.total_seconds() == 0 or delta.total_seconds() > expectedDuration: if delta.total_seconds() == 0 or delta.total_seconds() > expectedDuration:
raise Exception("expected timeout to occur in under {0} seconds. Took {1}".format(expectedDuration, delta.total_seconds())) raise(Exception("expected timeout to occur in under {0} seconds. Took {1}".format(expectedDuration, delta.total_seconds())))
def test_tls_alpn_challenge(): def test_tls_alpn_challenge():
# Pick two random domains # Pick two random domains
@ -339,7 +337,7 @@ def test_issuer():
of length exactly 1. of length exactly 1.
""" """
certr, authzs = auth_and_issue([random_domain()]) certr, authzs = auth_and_issue([random_domain()])
cert = urllib2.urlopen(certr.uri).read() cert = requests.get(certr.uri).content
# In the future the chain URI will use HTTPS so include the root certificate # In the future the chain URI will use HTTPS so include the root certificate
# for the WFE's PKI. Note: We use the requests library here so we honor the # for the WFE's PKI. Note: We use the requests library here so we honor the
# REQUESTS_CA_BUNDLE passed by test.sh. # REQUESTS_CA_BUNDLE passed by test.sh.
@ -382,7 +380,7 @@ def test_ct_submission():
def submissions(group): def submissions(group):
count = 0 count = 0
for log in group: for log in group:
count += int(urllib2.urlopen(log + "?hostnames=%s" % hostname).read()) count += int(requests.get(log + "?hostnames=%s" % hostname).text)
return count return count
auth_and_issue([hostname]) auth_and_issue([hostname])
@ -392,29 +390,29 @@ def test_ct_submission():
for i in range(len(log_groups)): for i in range(len(log_groups)):
if got[i] < expected[i]: if got[i] < expected[i]:
raise Exception("For log group %d, got %d submissions, expected %d." % raise(Exception("For log group %d, got %d submissions, expected %d." %
(i, got[i], expected[i])) (i, got[i], expected[i])))
def test_expiration_mailer(): def test_expiration_mailer():
email_addr = "integration.%x@letsencrypt.org" % random.randrange(2**16) email_addr = "integration.%x@letsencrypt.org" % random.randrange(2**16)
cert, _ = auth_and_issue([random_domain()], email=email_addr) cert, _ = auth_and_issue([random_domain()], email=email_addr)
# Check that the expiration mailer sends a reminder # Check that the expiration mailer sends a reminder
expiry = datetime.datetime.strptime(cert.body.get_notAfter(), '%Y%m%d%H%M%SZ') expiry = datetime.datetime.strptime(cert.body.get_notAfter().decode(), '%Y%m%d%H%M%SZ')
no_reminder = expiry + datetime.timedelta(days=-31) no_reminder = expiry + datetime.timedelta(days=-31)
first_reminder = expiry + datetime.timedelta(days=-13) first_reminder = expiry + datetime.timedelta(days=-13)
last_reminder = expiry + datetime.timedelta(days=-2) last_reminder = expiry + datetime.timedelta(days=-2)
urllib2.urlopen("http://localhost:9381/clear", data='') requests.post("http://localhost:9381/clear", data='')
print get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' % print(get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' %
config_dir, no_reminder) config_dir, no_reminder))
print get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' % print(get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' %
config_dir, first_reminder) config_dir, first_reminder))
print get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' % print(get_future_output('./bin/expiration-mailer --config %s/expiration-mailer.json' %
config_dir, last_reminder) config_dir, last_reminder))
resp = urllib2.urlopen("http://localhost:9381/count?to=%s" % email_addr) resp = requests.get("http://localhost:9381/count?to=%s" % email_addr)
mailcount = int(resp.read()) mailcount = int(resp.text)
if mailcount != 2: if mailcount != 2:
raise Exception("\nExpiry mailer failed: expected 2 emails, got %d" % mailcount) raise(Exception("\nExpiry mailer failed: expected 2 emails, got %d" % mailcount))
def test_revoke_by_account(): def test_revoke_by_account():
client = chisel.make_client() client = chisel.make_client()
@ -450,13 +448,13 @@ def test_recheck_caa():
recheck CAA and reject the request. recheck CAA and reject the request.
""" """
if len(caa_recheck_authzs) == 0: if len(caa_recheck_authzs) == 0:
raise Exception("CAA authzs not prepared for test_caa") raise(Exception("CAA authzs not prepared for test_caa"))
domains = [] domains = []
for a in caa_recheck_authzs: for a in caa_recheck_authzs:
response = requests.get(a.uri) response = requests.get(a.uri)
if response.status_code != 200: if response.status_code != 200:
raise Exception("Unexpected response for CAA authz: ", raise(Exception("Unexpected response for CAA authz: ",
response.status_code) response.status_code))
domain = a.body.identifier.value domain = a.body.identifier.value
domains.append(domain) domains.append(domain)
challSrv.add_caa_issue(domain, ";") challSrv.add_caa_issue(domain, ";")
@ -524,11 +522,11 @@ def test_account_update():
result = chisel.update_email(client, email=email) result = chisel.update_email(client, email=email)
# We expect one contact in the result # We expect one contact in the result
if len(result.body.contact) != 1: if len(result.body.contact) != 1:
raise Exception("\nUpdate account failed: expected one contact in result, got 0") raise(Exception("\nUpdate account failed: expected one contact in result, got 0"))
# We expect it to be the email we just updated to # We expect it to be the email we just updated to
actual = result.body.contact[0] actual = result.body.contact[0]
if actual != "mailto:"+email: if actual != "mailto:"+email:
raise Exception("\nUpdate account failed: expected contact %s, got %s" % (email, actual)) raise(Exception("\nUpdate account failed: expected contact %s, got %s" % (email, actual)))
def test_renewal_exemption(): def test_renewal_exemption():
""" """
@ -584,13 +582,13 @@ def test_admin_revoker_cert():
def test_sct_embedding(): def test_sct_embedding():
certr, authzs = auth_and_issue([random_domain()]) certr, authzs = auth_and_issue([random_domain()])
certBytes = urllib2.urlopen(certr.uri).read() certBytes = requests.get(certr.uri).content
cert = x509.load_der_x509_certificate(certBytes, default_backend()) cert = x509.load_der_x509_certificate(certBytes, default_backend())
# make sure there is no poison extension # make sure there is no poison extension
try: try:
cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")) 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") raise(Exception("certificate contains CT poison extension"))
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
# do nothing # do nothing
pass pass
@ -599,18 +597,18 @@ def test_sct_embedding():
try: try:
sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")) sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2"))
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
raise Exception("certificate doesn't contain SCT list extension") raise(Exception("certificate doesn't contain SCT list extension"))
if len(sctList.value) != 2: if len(sctList.value) != 2:
raise Exception("SCT list contains wrong number of SCTs") raise(Exception("SCT list contains wrong number of SCTs"))
for sct in sctList.value: for sct in sctList.value:
if sct.version != x509.certificate_transparency.Version.v1: if sct.version != x509.certificate_transparency.Version.v1:
raise Exception("SCT contains wrong version") raise(Exception("SCT contains wrong version"))
if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE: if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE:
raise Exception("SCT contains wrong entry type") raise(Exception("SCT contains wrong entry type"))
delta = sct.timestamp - datetime.datetime.now() delta = sct.timestamp - datetime.datetime.now()
if abs(delta) > datetime.timedelta(hours=1): if abs(delta) > datetime.timedelta(hours=1):
raise Exception("Delta between SCT timestamp and now was too great " raise(Exception("Delta between SCT timestamp and now was too great "
"%s vs %s (%s)" % (sct.timestamp, datetime.datetime.now(), delta)) "%s vs %s (%s)" % (sct.timestamp, datetime.datetime.now(), delta)))
def test_auth_deactivation(): def test_auth_deactivation():
client = chisel.make_client(None) client = chisel.make_client(None)

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Integration test cases for ACMEv2 as implemented by boulder-wfe2. Integration test cases for ACMEv2 as implemented by boulder-wfe2.
@ -67,7 +66,7 @@ def rand_http_chall(client):
for c in a.body.challenges: for c in a.body.challenges:
if isinstance(c.chall, challenges.HTTP01): if isinstance(c.chall, challenges.HTTP01):
return d, c.chall return d, c.chall
raise Exception("No HTTP-01 challenge found for random domain authz") raise(Exception("No HTTP-01 challenge found for random domain authz"))
def check_challenge_dns_err(chalType): def check_challenge_dns_err(chalType):
""" """
@ -104,20 +103,20 @@ def check_challenge_dns_err(chalType):
elif chalType == "tls-alpn-01": elif chalType == "tls-alpn-01":
c = chisel2.get_chall(authzr, challenges.TLSALPN01) c = chisel2.get_chall(authzr, challenges.TLSALPN01)
else: else:
raise Exception("Invalid challenge type requested: {0}".format(challType)) raise(Exception("Invalid challenge type requested: {0}".format(challType)))
# The failed challenge's error should match expected # The failed challenge's error should match expected
error = c.error error = c.error
if error is None or error.typ != "urn:ietf:params:acme:error:{0}".format(expectedProbType): 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)) raise(Exception("Expected {0} prob, got {1}".format(expectedProbType, error.typ)))
if not expectedProbRegex.match(error.detail): if not expectedProbRegex.match(error.detail):
raise Exception("Prob detail did not match expectedProbRegex, got \"{0}\"".format(error.detail)) raise(Exception("Prob detail did not match expectedProbRegex, got \"{0}\"".format(error.detail)))
finally: finally:
challSrv.remove_servfail_response(d) challSrv.remove_servfail_response(d)
# If there was no exception that means something went wrong. The test should fail. # If there was no exception that means something went wrong. The test should fail.
if failed is False: if failed is False:
raise Exception("No problem generated issuing for broken DNS identifier") raise(Exception("No problem generated issuing for broken DNS identifier"))
def test_http_challenge_dns_err(): def test_http_challenge_dns_err():
""" """
@ -171,9 +170,9 @@ def test_http_challenge_broken_redirect():
c = chisel2.get_chall(authzr, challenges.HTTP01) c = chisel2.get_chall(authzr, challenges.HTTP01)
error = c.error error = c.error
if error is None or error.typ != "urn:ietf:params:acme:error:connection": if error is None or error.typ != "urn:ietf:params:acme:error:connection":
raise Exception("Expected connection prob, got %s" % (error.__str__())) raise(Exception("Expected connection prob, got %s" % (error.__str__())))
if error.detail != expectedError: if error.detail != expectedError:
raise Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)) raise(Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)))
challSrv.remove_http_redirect(challengePath) challSrv.remove_http_redirect(challengePath)
@ -317,7 +316,7 @@ def test_http_challenge_http_redirect():
# There should have been at least two GET requests made to the # There should have been at least two GET requests made to the
# challtestsrv. There may have been more if remote VAs were configured. # challtestsrv. There may have been more if remote VAs were configured.
if len(history) < 2: if len(history) < 2:
raise Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))) raise(Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))))
initialRequests = [] initialRequests = []
redirectedRequests = [] redirectedRequests = []
@ -325,7 +324,7 @@ def test_http_challenge_http_redirect():
for request in history: for request in history:
# All requests should have been over HTTP # All requests should have been over HTTP
if request['HTTPS'] is True: if request['HTTPS'] is True:
raise Exception("Expected all requests to be HTTP") raise(Exception("Expected all requests to be HTTP"))
# Initial requests should have the expected initial HTTP-01 URL for the challenge # Initial requests should have the expected initial HTTP-01 URL for the challenge
if request['URL'] == challengePath: if request['URL'] == challengePath:
initialRequests.append(request) initialRequests.append(request)
@ -334,15 +333,15 @@ def test_http_challenge_http_redirect():
elif request['URL'] == redirectPath: elif request['URL'] == redirectPath:
redirectedRequests.append(request) redirectedRequests.append(request)
else: else:
raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) 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. # There should have been at least 1 initial HTTP-01 validation request.
if len(initialRequests) < 1: if len(initialRequests) < 1:
raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) 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 # There should have been at least 1 redirected HTTP request for each VA
if len(redirectedRequests) < 1: if len(redirectedRequests) < 1:
raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))) raise(Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))))
def test_http_challenge_https_redirect(): def test_http_challenge_https_redirect():
client = chisel2.make_client() client = chisel2.make_client()
@ -377,7 +376,7 @@ def test_http_challenge_https_redirect():
error = chall.error error = chall.error
if error: if error:
problems.append(error.__str__()) problems.append(error.__str__())
raise Exception("validation problem: %s" % "; ".join(problems)) raise(Exception("validation problem: %s" % "; ".join(problems)))
challSrv.remove_http_redirect(challengePath) challSrv.remove_http_redirect(challengePath)
challSrv.remove_a_record(d) challSrv.remove_a_record(d)
@ -387,7 +386,7 @@ def test_http_challenge_https_redirect():
# There should have been at least two GET requests made to the challtestsrv by the VA # There should have been at least two GET requests made to the challtestsrv by the VA
if len(history) < 2: if len(history) < 2:
raise Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))) raise(Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))))
initialRequests = [] initialRequests = []
redirectedRequests = [] redirectedRequests = []
@ -401,30 +400,30 @@ def test_http_challenge_https_redirect():
elif request['URL'] == redirectPath: elif request['URL'] == redirectPath:
redirectedRequests.append(request) redirectedRequests.append(request)
else: else:
raise Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)) 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. # There should have been at least 1 initial HTTP-01 validation request.
if len(initialRequests) < 1: if len(initialRequests) < 1:
raise Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))) 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 # All initial requests should have been over HTTP
for r in initialRequests: for r in initialRequests:
if r['HTTPS'] is True: if r['HTTPS'] is True:
raise Exception("Expected all initial requests to be HTTP, got %s" % r) 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 # There should have been at least 1 redirected HTTP request for each VA
if len(redirectedRequests) < 1: if len(redirectedRequests) < 1:
raise Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))) 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 # All the redirected requests should have been over HTTPS with the correct
# SNI value # SNI value
for r in redirectedRequests: for r in redirectedRequests:
if r['HTTPS'] is False: if r['HTTPS'] is False:
raise Exception("Expected all redirected requests to be HTTPS") raise(Exception("Expected all redirected requests to be HTTPS"))
# TODO(@cpu): The following ServerName test will fail with config-next # TODO(@cpu): The following ServerName test will fail with config-next
# until https://github.com/letsencrypt/boulder/issues/3969 is fixed. # until https://github.com/letsencrypt/boulder/issues/3969 is fixed.
if CONFIG_NEXT: if CONFIG_NEXT:
return return
elif r['ServerName'] != d: elif r['ServerName'] != d:
raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName'])) raise(Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName'])))
def test_tls_alpn_challenge(): def test_tls_alpn_challenge():
# Pick two random domains # Pick two random domains
@ -453,8 +452,8 @@ def test_overlapping_wildcard():
authzs = order.authorizations authzs = order.authorizations
if len(authzs) != 2: if len(authzs) != 2:
raise Exception("order for %s had %d authorizations, expected 2" % raise(Exception("order for %s had %d authorizations, expected 2" %
(domains, len(authzs))) (domains, len(authzs))))
cleanup = chisel2.do_dns_challenges(client, authzs) cleanup = chisel2.do_dns_challenges(client, authzs)
try: try:
@ -519,8 +518,8 @@ def test_wildcard_authz_reuse():
# We expect all of the returned authorizations to be pending status # We expect all of the returned authorizations to be pending status
for authz in order.authorizations: for authz in order.authorizations:
if authz.body.status != Status("pending"): if authz.body.status != Status("pending"):
raise Exception("order for %s included a non-pending authorization (status: %s) from a previous HTTP-01 order" % raise(Exception("order for %s included a non-pending authorization (status: %s) from a previous HTTP-01 order" %
((domains), str(authz.body.status))) ((domains), str(authz.body.status))))
def test_bad_overlap_wildcard(): def test_bad_overlap_wildcard():
chisel2.expect_problem("urn:ietf:params:acme:error:malformed", chisel2.expect_problem("urn:ietf:params:acme:error:malformed",
@ -569,20 +568,20 @@ def test_order_reuse_failed_authz():
# If the poll ended and an authz's status isn't invalid then we reached the # If the poll ended and an authz's status isn't invalid then we reached the
# deadline, fail the test # deadline, fail the test
if not authzFailed: if not authzFailed:
raise Exception("timed out waiting for order %s to become invalid" % firstOrderURI) raise(Exception("timed out waiting for order %s to become invalid" % firstOrderURI))
# Make another order with the same domains # Make another order with the same domains
order = client.new_order(csr_pem) order = client.new_order(csr_pem)
# It should not be the same order as before # It should not be the same order as before
if order.uri == firstOrderURI: if order.uri == firstOrderURI:
raise Exception("new-order for %s returned a , now-invalid, order" % domains) raise(Exception("new-order for %s returned a , now-invalid, order" % domains))
# We expect all of the returned authorizations to be pending status # We expect all of the returned authorizations to be pending status
for authz in order.authorizations: for authz in order.authorizations:
if authz.body.status != Status("pending"): if authz.body.status != Status("pending"):
raise Exception("order for %s included a non-pending authorization (status: %s) from a previous order" % raise(Exception("order for %s included a non-pending authorization (status: %s) from a previous order" %
((domains), str(authz.body.status))) ((domains), str(authz.body.status))))
# We expect the new order can be fulfilled # We expect the new order can be fulfilled
cleanup = chisel2.do_http_challenges(client, order.authorizations) cleanup = chisel2.do_http_challenges(client, order.authorizations)
@ -683,12 +682,13 @@ def test_revoke_by_privkey():
def test_sct_embedding(): def test_sct_embedding():
order = chisel2.auth_and_issue([random_domain()]) order = chisel2.auth_and_issue([random_domain()])
cert = x509.load_pem_x509_certificate(str(order.fullchain_pem), default_backend()) print(order.fullchain_pem.encode())
cert = x509.load_pem_x509_certificate(order.fullchain_pem.encode(), default_backend())
# make sure there is no poison extension # make sure there is no poison extension
try: try:
cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")) 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") raise(Exception("certificate contains CT poison extension"))
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
# do nothing # do nothing
pass pass
@ -697,14 +697,14 @@ def test_sct_embedding():
try: try:
sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")) sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2"))
except x509.ExtensionNotFound: except x509.ExtensionNotFound:
raise Exception("certificate doesn't contain SCT list extension") raise(Exception("certificate doesn't contain SCT list extension"))
if len(sctList.value) != 2: if len(sctList.value) != 2:
raise Exception("SCT list contains wrong number of SCTs") raise(Exception("SCT list contains wrong number of SCTs"))
for sct in sctList.value: for sct in sctList.value:
if sct.version != x509.certificate_transparency.Version.v1: if sct.version != x509.certificate_transparency.Version.v1:
raise Exception("SCT contains wrong version") raise(Exception("SCT contains wrong version"))
if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE: if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE:
raise Exception("SCT contains wrong entry type") raise(Exception("SCT contains wrong entry type"))
def test_only_return_existing_reg(): def test_only_return_existing_reg():
client = chisel2.uninitialized_client() client = chisel2.uninitialized_client()
@ -723,7 +723,7 @@ def test_only_return_existing_reg():
}) })
resp = client.net.post(client.directory['newAccount'], acct, acme_version=2) resp = client.net.post(client.directory['newAccount'], acct, acme_version=2)
if resp.status_code != 200: if resp.status_code != 200:
raise Exception("incorrect response returned for onlyReturnExisting") raise(Exception("incorrect response returned for onlyReturnExisting"))
other_client = chisel2.uninitialized_client() other_client = chisel2.uninitialized_client()
newAcct = extendedAcct({ newAcct = extendedAcct({
@ -770,7 +770,7 @@ def BouncerHTTPRequestHandler(redirect, guestlist):
self.log_message("BouncerHandler UA {0} has no requests on the Guestlist. Sending request to the curb".format(ua)) self.log_message("BouncerHandler UA {0} has no requests on the Guestlist. Sending request to the curb".format(ua))
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
self.wfile.write(b'(• ◡ •) <( VIPs only! )') self.wfile.write(u'(• ◡ •) <( VIPs only! )')
BouncerHandler.guestlist = guestlist BouncerHandler.guestlist = guestlist
BouncerHandler.redirect = redirect BouncerHandler.redirect = redirect
@ -810,7 +810,7 @@ def multiva_setup(client, guestlist, domain=None):
if isinstance(c.chall, challenges.HTTP01): if isinstance(c.chall, challenges.HTTP01):
chall = c.chall chall = c.chall
if chall is None: if chall is None:
raise Exception("No HTTP-01 challenge found for random domain authz") raise(Exception("No HTTP-01 challenge found for random domain authz"))
token = chall.encode("token") token = chall.encode("token")
@ -894,16 +894,16 @@ def test_http_multiva_threshold_fail():
# test needs to unpack an `acme_errors.ValidationError` on its own. It # test needs to unpack an `acme_errors.ValidationError` on its own. It
# might be possible to clean this up in the future. # might be possible to clean this up in the future.
if len(e.failed_authzrs) != 1: if len(e.failed_authzrs) != 1:
raise Exception("expected one failed authz, found {0}".format(len(e.failed_authzrs))) raise(Exception("expected one failed authz, found {0}".format(len(e.failed_authzrs))))
challs = e.failed_authzrs[0].body.challenges challs = e.failed_authzrs[0].body.challenges
httpChall = None httpChall = None
for chall_body in challs: for chall_body in challs:
if isinstance(chall_body.chall, challenges.HTTP01): if isinstance(chall_body.chall, challenges.HTTP01):
httpChall = chall_body httpChall = chall_body
if httpChall is None: if httpChall is None:
raise Exception("no HTTP-01 challenge in failed authz") raise(Exception("no HTTP-01 challenge in failed authz"))
if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized": if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized":
raise Exception("expected unauthorized prob, found {0}".format(httpChall.error.typ)) raise(Exception("expected unauthorized prob, found {0}".format(httpChall.error.typ)))
finally: finally:
cleanup() cleanup()
@ -945,7 +945,7 @@ def test_http_multiva_threshold_fail_account_disabled():
# Find the numeric ID it was assigned by the ACME server # Find the numeric ID it was assigned by the ACME server
acctURI = client.net.account.uri acctURI = client.net.account.uri
if len(acctURI.split("/")) < 1: if len(acctURI.split("/")) < 1:
raise Exception("invalid account URI for newly registered account: {0}".format(acctURI)) raise(Exception("invalid account URI for newly registered account: {0}".format(acctURI)))
acctID = acctURI.split("/")[-1:][0] acctID = acctURI.split("/")[-1:][0]
def run_query(query): def run_query(query):
@ -1043,7 +1043,7 @@ def wait_for_tcp_server(addr, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
sock.connect((addr, port)) sock.connect((addr, port))
sock.sendall("\n") sock.sendall(b"\n")
return return
except socket.error: except socket.error:
time.sleep(0.5) time.sleep(0.5)
@ -1085,9 +1085,9 @@ def test_http2_http01_challenge():
c = chisel2.get_chall(authzr, challenges.HTTP01) c = chisel2.get_chall(authzr, challenges.HTTP01)
error = c.error error = c.error
if error is None or error.typ != "urn:ietf:params:acme:error:connection": if error is None or error.typ != "urn:ietf:params:acme:error:connection":
raise Exception("Expected connection prob, got %s" % (error.__str__())) raise(Exception("Expected connection prob, got %s" % (error.__str__())))
if not error.detail.endswith(expectedError): if not error.detail.endswith(expectedError):
raise Exception("Expected prob detail ending in %s, got %s" % (expectedError, error.detail)) raise(Exception("Expected prob detail ending in %s, got %s" % (expectedError, error.detail)))
finally: finally:
server.shutdown() server.shutdown()
server.server_close() server.server_close()
@ -1121,26 +1121,26 @@ def test_new_order_policy_errs():
if e.detail != 'Error creating new order :: Cannot issue for "between-addr.in-addr.arpa": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy (and 1 more problems. Refer to sub-problems for more information.)': if e.detail != 'Error creating new order :: Cannot issue for "between-addr.in-addr.arpa": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy (and 1 more problems. Refer to sub-problems for more information.)':
raise(Exception('Order problem detail did not match expected')) raise(Exception('Order problem detail did not match expected'))
if not ok: if not ok:
raise Exception('Expected problem, got no error') raise(Exception('Expected problem, got no error'))
def test_long_san_no_cn(): def test_long_san_no_cn():
try: try:
chisel2.auth_and_issue([''.join(random.choice(string.ascii_uppercase) for x in range(61)) + ".com"]) 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 # 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") 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: except messages.Error as e:
if e.typ != "urn:ietf:params:acme:error:badCSR": if e.typ != "urn:ietf:params:acme:error:badCSR":
raise Exception('Expected malformed type problem, got {0}'.format(e.typ)) 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': 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') raise(Exception('Problem detail did not match expected'))
def test_delete_unused_challenges(): def test_delete_unused_challenges():
order = chisel2.auth_and_issue([random_domain()], chall_type="dns-01") order = chisel2.auth_and_issue([random_domain()], chall_type="dns-01")
a = order.authorizations[0] a = order.authorizations[0]
if len(a.body.challenges) != 1: if len(a.body.challenges) != 1:
raise Exception("too many challenges (%d) left after validation" % len(a.body.challenges)) raise(Exception("too many challenges (%d) left after validation" % len(a.body.challenges)))
if not isinstance(a.body.challenges[0].chall, challenges.DNS01): if not isinstance(a.body.challenges[0].chall, challenges.DNS01):
raise Exception("wrong challenge type left after validation") raise(Exception("wrong challenge type left after validation"))
# intentionally fail a challenge # intentionally fail a challenge
client = chisel2.make_client() client = chisel2.make_client()
@ -1154,10 +1154,10 @@ def test_delete_unused_challenges():
break break
time.sleep(1) time.sleep(1)
if len(a.body.challenges) != 1: if len(a.body.challenges) != 1:
raise Exception("too many challenges (%d) left after failed validation" % raise(Exception("too many challenges (%d) left after failed validation" %
len(a.body.challenges)) len(a.body.challenges)))
if not isinstance(a.body.challenges[0].chall, challenges.DNS01): if not isinstance(a.body.challenges[0].chall, challenges.DNS01):
raise Exception("wrong challenge type left after validation") raise(Exception("wrong challenge type left after validation"))
def test_auth_deactivation_v2(): def test_auth_deactivation_v2():
client = chisel2.make_client(None) client = chisel2.make_client(None)
@ -1165,12 +1165,12 @@ def test_auth_deactivation_v2():
order = client.new_order(csr_pem) order = client.new_order(csr_pem)
resp = client.deactivate_authorization(order.authorizations[0]) resp = client.deactivate_authorization(order.authorizations[0])
if resp.body.status is not messages.STATUS_DEACTIVATED: if resp.body.status is not messages.STATUS_DEACTIVATED:
raise Exception("unexpected authorization status") raise(Exception("unexpected authorization status"))
order = chisel2.auth_and_issue([random_domain()], client=client) order = chisel2.auth_and_issue([random_domain()], client=client)
resp = client.deactivate_authorization(order.authorizations[0]) resp = client.deactivate_authorization(order.authorizations[0])
if resp.body.status is not messages.STATUS_DEACTIVATED: if resp.body.status is not messages.STATUS_DEACTIVATED:
raise Exception("unexpected authorization status") raise(Exception("unexpected authorization status"))
def check_ocsp_basic_oid(cert_file, issuer_file, url): def check_ocsp_basic_oid(cert_file, issuer_file, url):
@ -1189,8 +1189,8 @@ def check_ocsp_basic_oid(cert_file, issuer_file, url):
expected = bytearray.fromhex("06 09 2B 06 01 05 05 07 30 01 01") expected = bytearray.fromhex("06 09 2B 06 01 05 05 07 30 01 01")
for resp in responses: for resp in responses:
if not expected in bytearray(resp): if not expected in bytearray(resp):
raise Exception("Did not receive successful OCSP response: %s doesn't contain %s" % raise(Exception("Did not receive successful OCSP response: %s doesn't contain %s" %
(base64.b64encode(resp), base64.b64encode(expected))) (base64.b64encode(resp), base64.b64encode(expected))))
expired_cert_name = "" expired_cert_name = ""
@register_six_months_ago @register_six_months_ago
@ -1213,17 +1213,19 @@ def ocsp_exp_unauth_setup():
def test_ocsp_exp_unauth(): def test_ocsp_exp_unauth():
tries = 0 tries = 0
if expired_cert_name == "":
raise Exception("ocsp_exp_unauth_setup didn't run")
while True: while True:
try: try:
verify_ocsp(expired_cert_name, "test/test-ca2.pem", "http://localhost:4002", "XXX") verify_ocsp(expired_cert_name, "test/test-ca2.pem", "http://localhost:4002", "XXX")
raise Exception("Unexpected return from verify_ocsp") raise(Exception("Unexpected return from verify_ocsp"))
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
if cpe.output == 'Responder Error: unauthorized (6)\n': if cpe.output == b'Responder Error: unauthorized (6)\n':
break break
except: except:
pass pass
if tries is 5: if tries is 5:
raise Exception("timed out waiting for unauthorized OCSP response for expired certificate") raise(Exception("timed out waiting for unauthorized OCSP response for expired certificate"))
tries += 1 tries += 1
time.sleep(0.25) time.sleep(0.25)
@ -1247,13 +1249,13 @@ def test_blocked_key_account():
terms_of_service_agreed=True)) terms_of_service_agreed=True))
except acme_errors.Error as e: except acme_errors.Error as e:
if e.typ != "urn:ietf:params:acme:error:badPublicKey": if e.typ != "urn:ietf:params:acme:error:badPublicKey":
raise Exception("problem did not have correct error type, had {0}".format(e.typ)) raise(Exception("problem did not have correct error type, had {0}".format(e.typ)))
if e.detail != "public key is forbidden": if e.detail != "public key is forbidden":
raise Exception("problem did not have correct error detail, had {0}".format(e.detail)) raise(Exception("problem did not have correct error detail, had {0}".format(e.detail)))
testPass = True testPass = True
if testPass is False: if testPass is False:
raise Exception("expected account creation to fail with Error when using blocked key") raise(Exception("expected account creation to fail with Error when using blocked key"))
def test_blocked_key_cert(): def test_blocked_key_cert():
# Only config-next has a blocked keys file configured. # Only config-next has a blocked keys file configured.
@ -1276,13 +1278,13 @@ def test_blocked_key_cert():
order = client.poll_and_finalize(order) order = client.poll_and_finalize(order)
except acme_errors.Error as e: except acme_errors.Error as e:
if e.typ != "urn:ietf:params:acme:error:badPublicKey": if e.typ != "urn:ietf:params:acme:error:badPublicKey":
raise Exception("problem did not have correct error type, had {0}".format(e.typ)) raise(Exception("problem did not have correct error type, had {0}".format(e.typ)))
if e.detail != "Error finalizing order :: invalid public key in CSR: public key is forbidden": if e.detail != "Error finalizing order :: invalid public key in CSR: public key is forbidden":
raise Exception("problem did not have correct error detail, had {0}".format(e.detail)) raise(Exception("problem did not have correct error detail, had {0}".format(e.detail)))
testPass = True testPass = True
if testPass is False: if testPass is False:
raise Exception("expected cert creation to fail with Error when using blocked key") raise(Exception("expected cert creation to fail with Error when using blocked key"))
def run(cmd, **kwargs): def run(cmd, **kwargs):
return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, **kwargs) return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, **kwargs)