236 lines
8.4 KiB
Python
236 lines
8.4 KiB
Python
#!/usr/bin/env python2.7
|
|
import atexit
|
|
import base64
|
|
import os
|
|
import re
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib
|
|
import time
|
|
import urllib2
|
|
|
|
import startservers
|
|
|
|
|
|
class ExitStatus:
|
|
OK, PythonFailure, NodeFailure, Error, OCSPFailure, CTFailure = range(6)
|
|
|
|
|
|
class ProcInfo:
|
|
"""
|
|
Args:
|
|
cmd (str): The command that was run
|
|
proc(subprocess.Popen): The Popen of the command run
|
|
"""
|
|
|
|
def __init__(self, cmd, proc):
|
|
self.cmd = cmd
|
|
self.proc = proc
|
|
|
|
|
|
def die(status):
|
|
global exit_status
|
|
# Set exit_status so cleanup handler knows what to report.
|
|
exit_status = status
|
|
sys.exit(exit_status)
|
|
|
|
def fetch_ocsp(request_bytes, url):
|
|
"""Fetch an OCSP response using POST, GET, and GET with URL encoding.
|
|
|
|
Returns a tuple of the responses.
|
|
"""
|
|
ocsp_req_b64 = base64.b64encode(request_bytes)
|
|
|
|
# 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.
|
|
get_response = urllib2.urlopen("%s/%s" % (url, ocsp_req_b64)).read()
|
|
get_encoded_response = urllib2.urlopen("%s/%s" % (url, urllib.quote(ocsp_req_b64, safe = ""))).read()
|
|
post_response = urllib2.urlopen("%s/" % (url), request_bytes).read()
|
|
|
|
return (post_response, get_response, get_encoded_response)
|
|
|
|
def make_ocsp_req(cert_file, issuer_file):
|
|
"""Return the bytes of an OCSP request for the given certificate file."""
|
|
ocsp_req_file = os.path.join(tempdir, "ocsp.req")
|
|
# First generate the OCSP request in DER form
|
|
cmd = ("openssl ocsp -no_nonce -issuer %s -cert %s -reqout %s" % (
|
|
issuer_file, cert_file, ocsp_req_file))
|
|
print cmd
|
|
subprocess.check_output(cmd, shell=True)
|
|
with open(ocsp_req_file) as f:
|
|
ocsp_req = f.read()
|
|
return ocsp_req
|
|
|
|
def fetch_until(cert_file, issuer_file, url, initial, final):
|
|
"""Fetch OCSP for cert_file until OCSP status goes from initial to final.
|
|
|
|
Initial and final are treated as regular expressions. Any OCSP response
|
|
whose OpenSSL OCSP verify output doesn't match either initial or final is
|
|
a fatal error.
|
|
|
|
If OCSP responses by the three methods (POST, GET, URL-encoded GET) differ
|
|
from each other, that is a fatal error.
|
|
|
|
If we loop for more than five seconds, that is a fatal error.
|
|
|
|
Returns nothing on success.
|
|
"""
|
|
ocsp_request = make_ocsp_req(cert_file, issuer_file)
|
|
timeout = time.time() + 5
|
|
while True:
|
|
time.sleep(0.25)
|
|
if time.time() > timeout:
|
|
print("Timed out waiting for OCSP to go from '%s' to '%s'" % (
|
|
initial, final))
|
|
die(ExitStatus.OCSPFailure)
|
|
responses = fetch_ocsp(ocsp_request, url)
|
|
# This variable will be true at the end of the loop if all the responses
|
|
# matched the final state.
|
|
all_final = True
|
|
for resp in responses:
|
|
verify_output = ocsp_verify(cert_file, issuer_file, resp)
|
|
if re.search(initial, verify_output):
|
|
all_final = False
|
|
break
|
|
elif re.search(final, verify_output):
|
|
continue
|
|
else:
|
|
print verify_output
|
|
print("OCSP response didn't match '%s' or '%s'" %(
|
|
initial, final))
|
|
die(ExitStatus.OCSPFailure)
|
|
if all_final:
|
|
# Check that all responses were equal to each other.
|
|
for resp in responses:
|
|
if resp != responses[0]:
|
|
print "OCSP responses differed:"
|
|
print(base64.b64encode(responses[0]))
|
|
print(" vs ")
|
|
print(base64.b64encode(resp))
|
|
die(ExitStatus.OCSPFailure)
|
|
return
|
|
|
|
def ocsp_verify(cert_file, issuer_file, ocsp_response):
|
|
ocsp_resp_file = os.path.join(tempdir, "ocsp.resp")
|
|
with open(ocsp_resp_file, "w") as f:
|
|
f.write(ocsp_response)
|
|
ocsp_verify_cmd = """openssl ocsp -no_nonce -issuer %s -cert %s \
|
|
-verify_other %s -CAfile ../test-root.pem \
|
|
-respin %s""" % (issuer_file, cert_file, issuer_file, ocsp_resp_file)
|
|
print ocsp_verify_cmd
|
|
try:
|
|
output = subprocess.check_output(ocsp_verify_cmd,
|
|
shell=True, stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError as e:
|
|
output = e.output
|
|
print output
|
|
print "subprocess returned non-zero: %s" % e
|
|
die(ExitStatus.OCSPFailure)
|
|
# OpenSSL doesn't always return non-zero when response verify fails, so we
|
|
# also look for the string "Response Verify Failure"
|
|
verify_failure = "Response Verify Failure"
|
|
if re.search(verify_failure, output):
|
|
print output
|
|
die(ExitStatus.OCSPFailure)
|
|
return output
|
|
|
|
def wait_for_ocsp_good(cert_file, issuer_file, url):
|
|
fetch_until(cert_file, issuer_file, url, " unauthorized", ": good")
|
|
|
|
def wait_for_ocsp_revoked(cert_file, issuer_file, url):
|
|
fetch_until(cert_file, issuer_file, url, ": good", ": revoked")
|
|
|
|
def verify_ct_submission(expectedSubmissions, url):
|
|
resp = urllib2.urlopen(url)
|
|
submissionStr = resp.read()
|
|
if int(submissionStr) != expectedSubmissions:
|
|
print "Expected %d submissions, found %d" % (expectedSubmissions, int(submissionStr))
|
|
die(ExitStatus.CTFailure)
|
|
|
|
def run_node_test():
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
try:
|
|
s.connect(('localhost', 4000))
|
|
except socket.error, e:
|
|
print("Cannot connect to WFE")
|
|
die(ExitStatus.Error)
|
|
|
|
os.chdir('test/js')
|
|
|
|
if subprocess.Popen('npm install', shell=True).wait() != 0:
|
|
print("\n Installing NPM modules failed")
|
|
die(ExitStatus.Error)
|
|
cert_file = os.path.join(tempdir, "cert.der")
|
|
cert_file_pem = os.path.join(tempdir, "cert.pem")
|
|
key_file = os.path.join(tempdir, "key.pem")
|
|
# Pick a random hostname so we don't run into certificate rate limiting.
|
|
domain = subprocess.check_output("openssl rand -hex 6", shell=True).strip()
|
|
# Issue the certificate and transform it from DER-encoded to PEM-encoded.
|
|
if subprocess.Popen('''
|
|
node test.js --email foo@letsencrypt.org --agree true \
|
|
--domains www.%s-TEST.com --new-reg http://localhost:4000/acme/new-reg \
|
|
--certKey %s --cert %s && \
|
|
openssl x509 -in %s -out %s -inform der -outform pem
|
|
''' % (domain, key_file, cert_file, cert_file, cert_file_pem),
|
|
shell=True).wait() != 0:
|
|
print("\nIssuing failed")
|
|
die(ExitStatus.NodeFailure)
|
|
|
|
ee_ocsp_url = "http://localhost:4002"
|
|
issuer_ocsp_url = "http://localhost:4003"
|
|
|
|
# As OCSP-Updater is generating responses independently of the CA we sit in a loop
|
|
# checking OCSP until we either see a good response or we timeout (5s).
|
|
wait_for_ocsp_good(cert_file_pem, "../test-ca.pem", ee_ocsp_url)
|
|
|
|
# Verify that the static OCSP responder, which answers with a
|
|
# pre-signed, long-lived response for the CA cert, works.
|
|
wait_for_ocsp_good("../test-ca.pem", "../test-root.pem", issuer_ocsp_url)
|
|
|
|
verify_ct_submission(1, "http://localhost:4500/submissions")
|
|
|
|
if subprocess.Popen('''
|
|
node revoke.js %s %s http://localhost:4000/acme/revoke-cert
|
|
''' % (cert_file, key_file), shell=True).wait() != 0:
|
|
print("\nRevoking failed")
|
|
die(ExitStatus.NodeFailure)
|
|
|
|
wait_for_ocsp_revoked(cert_file_pem, "../test-ca.pem", ee_ocsp_url)
|
|
return 0
|
|
|
|
|
|
def run_client_tests():
|
|
root = os.environ.get("LETSENCRYPT_PATH")
|
|
assert root is not None, (
|
|
"Please set LETSENCRYPT_PATH env variable to point at "
|
|
"initialized (virtualenv) client repo root")
|
|
test_script_path = os.path.join(root, 'tests', 'boulder-integration.sh')
|
|
cmd = "source %s/venv/bin/activate && SIMPLE_HTTP_PORT=5002 %s" % (root, test_script_path)
|
|
if subprocess.Popen(cmd, shell=True, cwd=root, executable='/bin/bash').wait() != 0:
|
|
die(ExitStatus.PythonFailure)
|
|
|
|
|
|
@atexit.register
|
|
def cleanup():
|
|
import shutil
|
|
shutil.rmtree(tempdir)
|
|
if exit_status == ExitStatus.OK:
|
|
print("\n\nSUCCESS")
|
|
else:
|
|
print("\n\nFAILURE %d" % exit_status)
|
|
|
|
|
|
exit_status = ExitStatus.OK
|
|
tempdir = tempfile.mkdtemp()
|
|
if not startservers.start(race_detection=True):
|
|
die(ExitStatus.Error)
|
|
run_node_test()
|
|
# Simulate a disconnection from RabbitMQ to make sure reconnects work.
|
|
startservers.bounce_forward()
|
|
run_client_tests()
|
|
if not startservers.check():
|
|
die(ExitStatus.Error)
|