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')])