github.com/letsencrypt/boulder@v0.20251208.0/test/helpers.py (about) 1 import atexit 2 import base64 3 import errno 4 import glob 5 import os 6 import random 7 import re 8 import requests 9 import shutil 10 import socket 11 import subprocess 12 import tempfile 13 import time 14 import urllib 15 16 import challtestsrv 17 18 challSrv = challtestsrv.ChallTestServer() 19 tempdir = tempfile.mkdtemp() 20 21 @atexit.register 22 def stop(): 23 shutil.rmtree(tempdir) 24 25 config_dir = os.environ.get('BOULDER_CONFIG_DIR', '') 26 if config_dir == '': 27 raise Exception("BOULDER_CONFIG_DIR was not set") 28 CONFIG_NEXT = config_dir.startswith("test/config-next") 29 30 def temppath(name): 31 """Creates and returns a closed file inside the tempdir.""" 32 f = tempfile.NamedTemporaryFile( 33 dir=tempdir, 34 suffix='.{0}'.format(name), 35 mode='w+', 36 delete=False 37 ) 38 f.close() 39 return f 40 41 def fakeclock(date): 42 return date.strftime("%a %b %d %H:%M:%S UTC %Y") 43 44 def random_domain(): 45 """Generate a random domain for testing (to avoid rate limiting).""" 46 return "rand.%x.xyz" % random.randrange(2**32) 47 48 def run(cmd, **kwargs): 49 return subprocess.check_call(cmd, stderr=subprocess.STDOUT, **kwargs) 50 51 def fetch_ocsp(request_bytes, url): 52 """Fetch an OCSP response using POST, GET, and GET with URL encoding. 53 54 Returns a tuple of the responses. 55 """ 56 ocsp_req_b64 = base64.b64encode(request_bytes).decode() 57 58 # Make the OCSP request three different ways: by POST, by GET, and by GET with 59 # URL-encoded parameters. All three should have an identical response. 60 get_response = requests.get("%s/%s" % (url, ocsp_req_b64)).content 61 get_encoded_response = requests.get("%s/%s" % (url, urllib.parse.quote(ocsp_req_b64, safe = ""))).content 62 post_response = requests.post("%s/" % (url), data=request_bytes).content 63 64 return (post_response, get_response, get_encoded_response) 65 66 def make_ocsp_req(cert_file, issuer_file): 67 """Return the bytes of an OCSP request for the given certificate file.""" 68 with tempfile.NamedTemporaryFile(dir=tempdir) as f: 69 run(["openssl", "ocsp", "-no_nonce", 70 "-issuer", issuer_file, 71 "-cert", cert_file, 72 "-reqout", f.name]) 73 ocsp_req = f.read() 74 return ocsp_req 75 76 def ocsp_verify(cert_file, issuer_file, ocsp_response): 77 with tempfile.NamedTemporaryFile(dir=tempdir, delete=False) as f: 78 f.write(ocsp_response) 79 f.close() 80 output = subprocess.check_output([ 81 'openssl', 'ocsp', '-no_nonce', 82 '-issuer', issuer_file, 83 '-cert', cert_file, 84 '-verify_other', issuer_file, 85 '-CAfile', 'test/certs/webpki/root-rsa.cert.pem', 86 '-respin', f.name], stderr=subprocess.STDOUT).decode() 87 # OpenSSL doesn't always return non-zero when response verify fails, so we 88 # also look for the string "Response Verify Failure" 89 verify_failure = "Response Verify Failure" 90 if re.search(verify_failure, output): 91 print(output) 92 raise(Exception("OCSP verify failure")) 93 return output 94 95 def verify_ocsp(cert_file, issuer_glob, url, status="revoked", reason=None): 96 # Try to verify the OCSP response using every issuer identified by the glob. 97 # If one works, great. If none work, re-raise the exception produced by the 98 # last attempt 99 lastException = None 100 for issuer_file in glob.glob(issuer_glob): 101 try: 102 output = try_verify_ocsp(cert_file, issuer_file, url, status, reason) 103 return output 104 except Exception as e: 105 lastException = e 106 continue 107 raise(lastException) 108 109 def try_verify_ocsp(cert_file, issuer_file, url, status="revoked", reason=None): 110 ocsp_request = make_ocsp_req(cert_file, issuer_file) 111 responses = fetch_ocsp(ocsp_request, url) 112 113 # Verify all responses are the same 114 for resp in responses: 115 if resp != responses[0]: 116 raise(Exception("OCSP responses differed: %s vs %s" %( 117 base64.b64encode(responses[0]), base64.b64encode(resp)))) 118 119 # Check response is for the correct certificate and is correct 120 # status 121 resp = responses[0] 122 verify_output = ocsp_verify(cert_file, issuer_file, resp) 123 if status is not None: 124 if not re.search("%s: %s" % (cert_file, status), verify_output): 125 print(verify_output) 126 raise(Exception("OCSP response wasn't '%s'" % status)) 127 if reason == "unspecified": 128 if re.search("Reason:", verify_output): 129 print(verify_output) 130 raise(Exception("OCSP response contained unexpected reason")) 131 elif reason is not None: 132 if not re.search("Reason: %s" % reason, verify_output): 133 print(verify_output) 134 raise(Exception("OCSP response wasn't '%s'" % reason)) 135 return verify_output 136 137 def waitport(port, prog, perTickCheck=None): 138 """Wait until a port on localhost is open.""" 139 for _ in range(1000): 140 try: 141 time.sleep(0.1) 142 if perTickCheck is not None and not perTickCheck(): 143 return False 144 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 145 s.connect(('localhost', port)) 146 s.close() 147 return True 148 except socket.error as e: 149 if e.errno == errno.ECONNREFUSED: 150 print("Waiting for debug port %d (%s)" % (port, prog)) 151 else: 152 raise 153 raise(Exception("timed out waiting for debug port %d (%s)" % (port, prog))) 154 155 def waithealth(prog, addr, host_override): 156 if type(addr) == int: 157 addr = "localhost:%d" % (addr) 158 159 subprocess.check_call([ 160 './bin/health-checker', 161 '-addr', addr, 162 '-host-override', host_override, 163 '-config', os.path.join(config_dir, 'health-checker.json')])