github.com/letsencrypt/boulder@v0.20251208.0/test/v2_integration.py (about)

     1  # -*- coding: utf-8 -*-
     2  """
     3  Integration test cases for ACMEv2 as implemented by boulder-wfe2.
     4  """
     5  import subprocess
     6  import requests
     7  import datetime
     8  import time
     9  import os
    10  import json
    11  import re
    12  
    13  from cryptography import x509
    14  from cryptography.hazmat.backends import default_backend
    15  from cryptography.hazmat.primitives.asymmetric import rsa
    16  from cryptography.hazmat.primitives import serialization
    17  
    18  import chisel2
    19  from helpers import *
    20  
    21  from acme import errors as acme_errors
    22  
    23  from acme.messages import Status, CertificateRequest, Directory, NewRegistration
    24  from acme import crypto_util as acme_crypto_util
    25  from acme import client as acme_client
    26  from acme import messages
    27  from acme import challenges
    28  from acme import errors
    29  
    30  import josepy
    31  
    32  import tempfile
    33  import shutil
    34  import atexit
    35  import random
    36  import string
    37  
    38  import threading
    39  from http.server import HTTPServer, BaseHTTPRequestHandler
    40  import socketserver
    41  import socket
    42  
    43  import challtestsrv
    44  challSrv = challtestsrv.ChallTestServer()
    45  
    46  def test_multidomain():
    47      chisel2.auth_and_issue([random_domain(), random_domain()])
    48  
    49  def test_wildcardmultidomain():
    50      """
    51      Test issuance for a random domain and a random wildcard domain using DNS-01.
    52      """
    53      chisel2.auth_and_issue([random_domain(), "*."+random_domain()], chall_type="dns-01")
    54  
    55  def test_http_challenge():
    56      chisel2.auth_and_issue([random_domain(), random_domain()], chall_type="http-01")
    57  
    58  def rand_http_chall(client):
    59      d = random_domain()
    60      csr_pem = chisel2.make_csr([d])
    61      order = client.new_order(csr_pem)
    62      authzs = order.authorizations
    63      for a in authzs:
    64          for c in a.body.challenges:
    65              if isinstance(c.chall, challenges.HTTP01):
    66                  return d, c.chall
    67      raise(Exception("No HTTP-01 challenge found for random domain authz"))
    68  
    69  def check_challenge_dns_err(chalType):
    70      """
    71      check_challenge_dns_err tests that performing an ACME challenge of the
    72      specified type to a hostname that is configured to return SERVFAIL for all
    73      queries produces the correct problem type and detail message.
    74      """
    75      client = chisel2.make_client()
    76  
    77      # Create a random domains.
    78      d = random_domain()
    79  
    80      # Configure the chall srv to SERVFAIL all queries for that domain.
    81      challSrv.add_servfail_response(d)
    82  
    83      # Expect a DNS problem with a detail that matches a regex
    84      expectedProbType = "dns"
    85      expectedProbRegex = re.compile(r"SERVFAIL looking up (A|AAAA|TXT|CAA) for {0}".format(d))
    86  
    87      # Try and issue for the domain with the given challenge type.
    88      failed = False
    89      try:
    90          chisel2.auth_and_issue([d], client=client, chall_type=chalType)
    91      except acme_errors.ValidationError as e:
    92          # Mark that the auth_and_issue failed
    93          failed = True
    94          # Extract the failed challenge from each failed authorization
    95          for authzr in e.failed_authzrs:
    96              c = None
    97              if chalType == "http-01":
    98                  c = chisel2.get_chall(authzr, challenges.HTTP01)
    99              elif chalType == "dns-01":
   100                  c = chisel2.get_chall(authzr, challenges.DNS01)
   101              else:
   102                  raise(Exception("Invalid challenge type requested: {0}".format(challType)))
   103  
   104              # The failed challenge's error should match expected
   105              error = c.error
   106              if error is None or error.typ != "urn:ietf:params:acme:error:{0}".format(expectedProbType):
   107                  raise(Exception("Expected {0} prob, got {1}".format(expectedProbType, error.typ)))
   108              if not expectedProbRegex.search(error.detail):
   109                  raise(Exception("Prob detail did not match expectedProbRegex, got \"{0}\"".format(error.detail)))
   110      finally:
   111          challSrv.remove_servfail_response(d)
   112  
   113      # If there was no exception that means something went wrong. The test should fail.
   114      if failed is False:
   115          raise(Exception("No problem generated issuing for broken DNS identifier"))
   116  
   117  def test_http_challenge_dns_err():
   118      """
   119      test_http_challenge_dns_err tests that a HTTP-01 challenge for a domain
   120      with broken DNS produces the correct problem response.
   121      """
   122      check_challenge_dns_err("http-01")
   123  
   124  def test_dns_challenge_dns_err():
   125      """
   126      test_dns_challenge_dns_err tests that a DNS-01 challenge for a domain
   127      with broken DNS produces the correct problem response.
   128      """
   129      check_challenge_dns_err("dns-01")
   130  
   131  def test_http_challenge_broken_redirect():
   132      """
   133      test_http_challenge_broken_redirect tests that a common webserver
   134      misconfiguration receives the correct specialized error message when attempting
   135      an HTTP-01 challenge.
   136      """
   137      client = chisel2.make_client()
   138  
   139      # Create an authz for a random domain and get its HTTP-01 challenge token
   140      d, chall = rand_http_chall(client)
   141      token = chall.encode("token")
   142  
   143      # Create a broken HTTP redirect similar to a sort we see frequently "in the wild"
   144      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   145      redirect = "http://{0}.well-known/acme-challenge/bad-bad-bad".format(d)
   146      challSrv.add_http_redirect(
   147          challengePath,
   148          redirect)
   149  
   150      # Expect the specialized error message
   151      expectedError = "64.112.117.122: Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format(redirect, d)
   152  
   153      # NOTE(@cpu): Can't use chisel2.expect_problem here because it doesn't let
   154      # us interrogate the detail message easily.
   155      try:
   156          chisel2.auth_and_issue([d], client=client, chall_type="http-01")
   157      except acme_errors.ValidationError as e:
   158          for authzr in e.failed_authzrs:
   159              c = chisel2.get_chall(authzr, challenges.HTTP01)
   160              error = c.error
   161              if error is None or error.typ != "urn:ietf:params:acme:error:connection":
   162                  raise(Exception("Expected connection prob, got %s" % (error.__str__())))
   163              if error.detail != expectedError:
   164                  raise(Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)))
   165  
   166      challSrv.remove_http_redirect(challengePath)
   167  
   168  def test_failed_validation_limit():
   169      """
   170      Fail a challenge repeatedly for the same domain, with the same account. Once
   171      we reach the rate limit we should get a rateLimitedError. Note that this
   172      depends on the specific threshold configured.
   173  
   174      This also incidentally tests a fix for
   175      https://github.com/letsencrypt/boulder/issues/4329. We expect to get
   176      ValidationErrors, eventually followed by a rate limit error.
   177      """
   178      domain = "fail." + random_domain()
   179      csr_pem = chisel2.make_csr([domain])
   180      client = chisel2.make_client()
   181      threshold = 3
   182      for _ in range(threshold):
   183          order = client.new_order(csr_pem)
   184          chall = chisel2.get_any_supported_chall(order.authorizations[0])
   185          client.answer_challenge(chall, chall.response(client.net.key))
   186          try:
   187              client.poll_and_finalize(order)
   188          except errors.ValidationError as e:
   189              pass
   190      chisel2.expect_problem("urn:ietf:params:acme:error:rateLimited",
   191          lambda: chisel2.auth_and_issue([domain], client=client))
   192  
   193  
   194  def test_http_challenge_loop_redirect():
   195      client = chisel2.make_client()
   196  
   197      # Create an authz for a random domain and get its HTTP-01 challenge token
   198      d, chall = rand_http_chall(client)
   199      token = chall.encode("token")
   200  
   201      # Create a HTTP redirect from the challenge's validation path to itself
   202      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   203      challSrv.add_http_redirect(
   204          challengePath,
   205          "http://{0}{1}".format(d, challengePath))
   206  
   207      # Issuing for the name should fail because of the challenge domains's
   208      # redirect loop.
   209      chisel2.expect_problem("urn:ietf:params:acme:error:connection",
   210          lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01"))
   211  
   212      challSrv.remove_http_redirect(challengePath)
   213  
   214  def test_http_challenge_badport_redirect():
   215      client = chisel2.make_client()
   216  
   217      # Create an authz for a random domain and get its HTTP-01 challenge token
   218      d, chall = rand_http_chall(client)
   219      token = chall.encode("token")
   220  
   221      # Create a HTTP redirect from the challenge's validation path to a host with
   222      # an invalid port.
   223      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   224      challSrv.add_http_redirect(
   225          challengePath,
   226          "http://{0}:1337{1}".format(d, challengePath))
   227  
   228      # Issuing for the name should fail because of the challenge domain's
   229      # invalid port redirect.
   230      chisel2.expect_problem("urn:ietf:params:acme:error:connection",
   231          lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01"))
   232  
   233      challSrv.remove_http_redirect(challengePath)
   234  
   235  def test_http_challenge_badhost_redirect():
   236      client = chisel2.make_client()
   237  
   238      # Create an authz for a random domain and get its HTTP-01 challenge token
   239      d, chall = rand_http_chall(client)
   240      token = chall.encode("token")
   241  
   242      # Create a HTTP redirect from the challenge's validation path to a bare IP
   243      # hostname.
   244      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   245      challSrv.add_http_redirect(
   246          challengePath,
   247          "https://127.0.0.1{0}".format(challengePath))
   248  
   249      # Issuing for the name should cause a connection error because the redirect
   250      # domain name is an IP address.
   251      chisel2.expect_problem("urn:ietf:params:acme:error:connection",
   252          lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01"))
   253  
   254      challSrv.remove_http_redirect(challengePath)
   255  
   256  def test_http_challenge_badproto_redirect():
   257      client = chisel2.make_client()
   258  
   259      # Create an authz for a random domain and get its HTTP-01 challenge token
   260      d, chall = rand_http_chall(client)
   261      token = chall.encode("token")
   262  
   263      # Create a HTTP redirect from the challenge's validation path to whacky
   264      # non-http/https protocol URL.
   265      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   266      challSrv.add_http_redirect(
   267          challengePath,
   268          "gopher://{0}{1}".format(d, challengePath))
   269  
   270      # Issuing for the name should cause a connection error because the redirect
   271      # domain name is an IP address.
   272      chisel2.expect_problem("urn:ietf:params:acme:error:connection",
   273          lambda: chisel2.auth_and_issue([d], client=client, chall_type="http-01"))
   274  
   275      challSrv.remove_http_redirect(challengePath)
   276  
   277  def test_http_challenge_http_redirect():
   278      client = chisel2.make_client()
   279  
   280      # Create an authz for a random domain and get its HTTP-01 challenge token
   281      d, chall = rand_http_chall(client)
   282      token = chall.encode("token")
   283      # Calculate its keyauth so we can add it in a special non-standard location
   284      # for the redirect result
   285      resp = chall.response(client.net.key)
   286      keyauth = resp.key_authorization
   287      challSrv.add_http01_response("http-redirect", keyauth)
   288  
   289      # Create a HTTP redirect from the challenge's validation path to some other
   290      # token path where we have registered the key authorization.
   291      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   292      redirectPath = "/.well-known/acme-challenge/http-redirect?params=are&important=to&not=lose"
   293      challSrv.add_http_redirect(
   294          challengePath,
   295          "http://{0}{1}".format(d, redirectPath))
   296  
   297      chisel2.auth_and_issue([d], client=client, chall_type="http-01")
   298  
   299      challSrv.remove_http_redirect(challengePath)
   300      challSrv.remove_http01_response("http-redirect")
   301  
   302      history = challSrv.http_request_history(d)
   303      challSrv.clear_http_request_history(d)
   304  
   305      # There should have been at least two GET requests made to the
   306      # challtestsrv. There may have been more if remote VAs were configured.
   307      if len(history) < 2:
   308          raise(Exception("Expected at least 2 HTTP request events on challtestsrv, found {1}".format(len(history))))
   309  
   310      initialRequests = []
   311      redirectedRequests = []
   312  
   313      for request in history:
   314        # All requests should have been over HTTP
   315        if request['HTTPS'] is True:
   316          raise(Exception("Expected all requests to be HTTP"))
   317        # Initial requests should have the expected initial HTTP-01 URL for the challenge
   318        if request['URL'] == challengePath:
   319          initialRequests.append(request)
   320        # Redirected requests should have the expected redirect path URL with all
   321        # its parameters
   322        elif request['URL'] == redirectPath:
   323          redirectedRequests.append(request)
   324        else:
   325          raise(Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)))
   326  
   327      # There should have been at least 1 initial HTTP-01 validation request.
   328      if len(initialRequests) < 1:
   329          raise(Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))))
   330  
   331      # There should have been at least 1 redirected HTTP request for each VA
   332      if len(redirectedRequests) < 1:
   333          raise(Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))))
   334  
   335  def test_http_challenge_https_redirect():
   336      client = chisel2.make_client()
   337  
   338      # Create an authz for a random domain and get its HTTP-01 challenge token
   339      d, chall = rand_http_chall(client)
   340      token = chall.encode("token")
   341      # Calculate its keyauth so we can add it in a special non-standard location
   342      # for the redirect result
   343      resp = chall.response(client.net.key)
   344      keyauth = resp.key_authorization
   345      challSrv.add_http01_response("https-redirect", keyauth)
   346  
   347      # Create a HTTP redirect from the challenge's validation path to an HTTPS
   348      # path with some parameters
   349      challengePath = "/.well-known/acme-challenge/{0}".format(token)
   350      redirectPath = "/.well-known/acme-challenge/https-redirect?params=are&important=to&not=lose"
   351      challSrv.add_http_redirect(
   352          challengePath,
   353          "https://{0}{1}".format(d, redirectPath))
   354  
   355      # Also add an A record for the domain pointing to the interface that the
   356      # HTTPS HTTP-01 challtestsrv is bound.
   357      challSrv.add_a_record(d, ["64.112.117.122"])
   358  
   359      try:
   360          chisel2.auth_and_issue([d], client=client, chall_type="http-01")
   361      except errors.ValidationError as e:
   362          problems = []
   363          for authzr in e.failed_authzrs:
   364              for chall in authzr.body.challenges:
   365                  error = chall.error
   366                  if error:
   367                      problems.append(error.__str__())
   368          raise(Exception("validation problem: %s" % "; ".join(problems)))
   369  
   370      challSrv.remove_http_redirect(challengePath)
   371      challSrv.remove_a_record(d)
   372  
   373      history = challSrv.http_request_history(d)
   374      challSrv.clear_http_request_history(d)
   375  
   376      # There should have been at least two GET requests made to the challtestsrv by the VA
   377      if len(history) < 2:
   378          raise(Exception("Expected 2 HTTP request events on challtestsrv, found {0}".format(len(history))))
   379  
   380      initialRequests = []
   381      redirectedRequests = []
   382  
   383      for request in history:
   384        # Initial requests should have the expected initial HTTP-01 URL for the challenge
   385        if request['URL'] == challengePath:
   386          initialRequests.append(request)
   387        # Redirected requests should have the expected redirect path URL with all
   388        # its parameters
   389        elif request['URL'] == redirectPath:
   390          redirectedRequests.append(request)
   391        else:
   392          raise(Exception("Unexpected request URL {0} in challtestsrv history: {1}".format(request['URL'], request)))
   393  
   394      # There should have been at least 1 initial HTTP-01 validation request.
   395      if len(initialRequests) < 1:
   396          raise(Exception("Expected {0} initial HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(initialRequests))))
   397       # All initial requests should have been over HTTP
   398      for r in initialRequests:
   399        if r['HTTPS'] is True:
   400          raise(Exception("Expected all initial requests to be HTTP, got %s" % r))
   401  
   402      # There should have been at least 1 redirected HTTP request for each VA
   403      if len(redirectedRequests) < 1:
   404          raise(Exception("Expected {0} redirected HTTP-01 request events on challtestsrv, found {1}".format(validation_attempts, len(redirectedRequests))))
   405      # All the redirected requests should have been over HTTPS with the correct
   406      # SNI value
   407      for r in redirectedRequests:
   408        if r['HTTPS'] is False:
   409          raise(Exception("Expected all redirected requests to be HTTPS"))
   410        if r['ServerName'] != d:
   411          raise(Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName'])))
   412  
   413  class SlowHTTPRequestHandler(BaseHTTPRequestHandler):
   414      def do_GET(self):
   415          try:
   416              # Sleeptime needs to be larger than the RA->VA timeout (20s at the
   417              # time of writing)
   418              sleeptime = 22
   419              print("SlowHTTPRequestHandler: sleeping for {0}s\n".format(sleeptime))
   420              time.sleep(sleeptime)
   421              self.send_response(200)
   422              self.end_headers()
   423              self.wfile.write(b"this is not an ACME key authorization")
   424          except:
   425              pass
   426  
   427  class SlowHTTPServer(HTTPServer):
   428      # Override handle_error so we don't print a misleading stack trace when the
   429      # VA terminates the connection due to timeout.
   430      def handle_error(self, request, client_address):
   431          pass
   432  
   433  def test_http_challenge_timeout():
   434      """
   435      test_http_challenge_timeout tests that the VA times out challenge requests
   436      to a slow HTTP server appropriately.
   437      """
   438      # Start a simple python HTTP server on port 80 in its own thread.
   439      # NOTE(@cpu): The chall-test-srv binds 64.112.117.122:80 for HTTP-01
   440      # challenges so we must use the 64.112.117.134 address for the throw away
   441      # server for this test and add a mock DNS entry that directs the VA to it.
   442      httpd = SlowHTTPServer(("64.112.117.134", 80), SlowHTTPRequestHandler)
   443      thread = threading.Thread(target = httpd.serve_forever)
   444      thread.daemon = False
   445      thread.start()
   446  
   447      # Pick a random domain
   448      hostname = random_domain()
   449  
   450      # Add A record for the domains to ensure the VA's requests are directed
   451      # to the interface that we bound the HTTPServer to.
   452      challSrv.add_a_record(hostname, ["64.112.117.134"])
   453  
   454      start = datetime.datetime.utcnow()
   455      end = 0
   456  
   457      try:
   458          # We expect a connection timeout error to occur
   459          chisel2.expect_problem("urn:ietf:params:acme:error:connection",
   460              lambda: chisel2.auth_and_issue([hostname], chall_type="http-01"))
   461          end = datetime.datetime.utcnow()
   462      finally:
   463          # Shut down the HTTP server gracefully and join on its thread.
   464          httpd.shutdown()
   465          httpd.server_close()
   466          thread.join()
   467  
   468      delta = end - start
   469      # Expected duration should be the RA->VA timeout plus some padding (At
   470      # present the timeout is 20s so adding 2s of padding = 22s)
   471      expectedDuration = 22
   472      if delta.total_seconds() == 0 or delta.total_seconds() > expectedDuration:
   473          raise(Exception("expected timeout to occur in under {0} seconds. Took {1}".format(expectedDuration, delta.total_seconds())))
   474  
   475  
   476  def test_overlapping_wildcard():
   477      """
   478      Test issuance for a random domain and a wildcard version of the same domain
   479      using DNS-01. This should result in *two* distinct authorizations.
   480      """
   481      domain = random_domain()
   482      domains = [ domain, "*."+domain ]
   483      client = chisel2.make_client(None)
   484      csr_pem = chisel2.make_csr(domains)
   485      order = client.new_order(csr_pem)
   486      authzs = order.authorizations
   487  
   488      if len(authzs) != 2:
   489          raise(Exception("order for %s had %d authorizations, expected 2" %
   490                  (domains, len(authzs))))
   491  
   492      cleanup = chisel2.do_dns_challenges(client, authzs)
   493      try:
   494          order = client.poll_and_finalize(order)
   495      finally:
   496          cleanup()
   497  
   498  def test_highrisk_blocklist():
   499      """
   500      Test issuance for a subdomain of a HighRiskBlockedNames entry. It should
   501      fail with a policy error.
   502      """
   503  
   504      # We include "example.org" in `test/ident-policy.yaml` in the
   505      # HighRiskBlockedNames list so issuing for "foo.example.org" should be
   506      # blocked.
   507      domain = "foo.example.org"
   508      # We expect this to produce a policy problem
   509      chisel2.expect_problem("urn:ietf:params:acme:error:rejectedIdentifier",
   510          lambda: chisel2.auth_and_issue([domain], chall_type="dns-01"))
   511  
   512  def test_wildcard_exactblacklist():
   513      """
   514      Test issuance for a wildcard that would cover an exact blacklist entry. It
   515      should fail with a policy error.
   516      """
   517  
   518      # We include "highrisk.le-test.hoffman-andrews.com" in `test/ident-policy.yaml`
   519      # Issuing for "*.le-test.hoffman-andrews.com" should be blocked
   520      domain = "*.le-test.hoffman-andrews.com"
   521      # We expect this to produce a policy problem
   522      chisel2.expect_problem("urn:ietf:params:acme:error:rejectedIdentifier",
   523          lambda: chisel2.auth_and_issue([domain], chall_type="dns-01"))
   524  
   525  def test_wildcard_authz_reuse():
   526      """
   527      Test that an authorization for a base domain obtained via HTTP-01 isn't
   528      reused when issuing a wildcard for that base domain later on.
   529      """
   530  
   531      # Create one client to reuse across multiple issuances
   532      client = chisel2.make_client(None)
   533  
   534      # Pick a random domain to issue for
   535      domains = [ random_domain() ]
   536      csr_pem = chisel2.make_csr(domains)
   537  
   538      # Submit an order for the name
   539      order = client.new_order(csr_pem)
   540      # Complete the order via an HTTP-01 challenge
   541      cleanup = chisel2.do_http_challenges(client, order.authorizations)
   542      try:
   543          order = client.poll_and_finalize(order)
   544      finally:
   545          cleanup()
   546  
   547      # Now try to issue a wildcard for the random domain
   548      domains[0] = "*." + domains[0]
   549      csr_pem = chisel2.make_csr(domains)
   550      order = client.new_order(csr_pem)
   551  
   552      # We expect all of the returned authorizations to be pending status
   553      for authz in order.authorizations:
   554          if authz.body.status != Status("pending"):
   555              raise(Exception("order for %s included a non-pending authorization (status: %s) from a previous HTTP-01 order" %
   556                      ((domains), str(authz.body.status))))
   557  
   558  def test_bad_overlap_wildcard():
   559      chisel2.expect_problem("urn:ietf:params:acme:error:malformed",
   560          lambda: chisel2.auth_and_issue(["*.example.com", "www.example.com"]))
   561  
   562  def test_duplicate_orders():
   563      """
   564      Test that the same client issuing for the same domain names twice in a row
   565      works without error.
   566      """
   567      client = chisel2.make_client(None)
   568      domains = [ random_domain() ]
   569      chisel2.auth_and_issue(domains, client=client)
   570      chisel2.auth_and_issue(domains, client=client)
   571  
   572  def test_order_reuse_failed_authz():
   573      """
   574      Test that creating an order for a domain name, failing an authorization in
   575      that order, and submitting another new order request for the same name
   576      doesn't reuse a failed authorization in the new order.
   577      """
   578  
   579      client = chisel2.make_client(None)
   580      domains = [ random_domain() ]
   581      csr_pem = chisel2.make_csr(domains)
   582  
   583      order = client.new_order(csr_pem)
   584      firstOrderURI = order.uri
   585  
   586      # Pick the first authz's first supported challenge, doesn't matter what
   587      # type it is
   588      chall_body = chisel2.get_any_supported_chall(order.authorizations[0])
   589      # Answer it, but with nothing set up to solve the challenge request
   590      client.answer_challenge(chall_body, chall_body.response(client.net.key))
   591  
   592      deadline = datetime.datetime.now() + datetime.timedelta(seconds=60)
   593      authzFailed = False
   594      try:
   595          # Poll the order's authorizations until they are non-pending, a timeout
   596          # occurs, or there is an invalid authorization status.
   597          client.poll_authorizations(order, deadline)
   598      except acme_errors.ValidationError as e:
   599          # We expect there to be a ValidationError from one of the authorizations
   600          # being invalid.
   601          authzFailed = True
   602  
   603      # If the poll ended and an authz's status isn't invalid then we reached the
   604      # deadline, fail the test
   605      if not authzFailed:
   606          raise(Exception("timed out waiting for order %s to become invalid" % firstOrderURI))
   607  
   608      # Make another order with the same domains
   609      order = client.new_order(csr_pem)
   610  
   611      # It should not be the same order as before
   612      if order.uri == firstOrderURI:
   613          raise(Exception("new-order for %s returned a , now-invalid, order" % domains))
   614  
   615      # We expect all of the returned authorizations to be pending status
   616      for authz in order.authorizations:
   617          if authz.body.status != Status("pending"):
   618              raise(Exception("order for %s included a non-pending authorization (status: %s) from a previous order" %
   619                      ((domains), str(authz.body.status))))
   620  
   621      # We expect the new order can be fulfilled
   622      cleanup = chisel2.do_http_challenges(client, order.authorizations)
   623      try:
   624          order = client.poll_and_finalize(order)
   625      finally:
   626          cleanup()
   627  
   628  def test_only_return_existing_reg():
   629      client = chisel2.uninitialized_client()
   630      email = "test@not-example.com"
   631      client.new_account(messages.NewRegistration.from_data(email=email,
   632              terms_of_service_agreed=True))
   633  
   634      client = chisel2.uninitialized_client(key=client.net.key)
   635      class extendedAcct(dict):
   636          def json_dumps(self, indent=None):
   637              return json.dumps(self)
   638      acct = extendedAcct({
   639          "termsOfServiceAgreed": True,
   640          "contact": [email],
   641          "onlyReturnExisting": True
   642      })
   643      resp = client.net.post(client.directory['newAccount'], acct)
   644      if resp.status_code != 200:
   645          raise(Exception("incorrect response returned for onlyReturnExisting"))
   646  
   647      other_client = chisel2.uninitialized_client()
   648      newAcct = extendedAcct({
   649          "termsOfServiceAgreed": True,
   650          "contact": [email],
   651          "onlyReturnExisting": True
   652      })
   653      chisel2.expect_problem("urn:ietf:params:acme:error:accountDoesNotExist",
   654          lambda: other_client.net.post(other_client.directory['newAccount'], newAcct))
   655  
   656  def BouncerHTTPRequestHandler(redirect, guestlist):
   657      """
   658      BouncerHTTPRequestHandler returns a BouncerHandler class that acts like
   659      a club bouncer in front of another server. The bouncer will respond to
   660      GET requests by looking up the allowed number of requests in the guestlist
   661      for the User-Agent making the request. If there is at least one guestlist
   662      spot for that UA it will be redirected to the real server and the
   663      guestlist will be decremented. Once the guestlist spots for a UA are
   664      expended requests will get a bogus result and have to stand outside in the
   665      cold
   666      """
   667      class BouncerHandler(BaseHTTPRequestHandler):
   668          def __init__(self, *args, **kwargs):
   669              BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
   670  
   671          def do_HEAD(self):
   672              # This is used by wait_for_server
   673              self.send_response(200)
   674              self.end_headers()
   675  
   676          def do_GET(self):
   677              ua = self.headers['User-Agent']
   678              guestlistAllows = BouncerHandler.guestlist.get(ua, 0)
   679              # If there is still space on the guestlist for this UA then redirect
   680              # the request and decrement the guestlist.
   681              if guestlistAllows > 0:
   682                  BouncerHandler.guestlist[ua] -= 1
   683                  self.log_message("BouncerHandler UA {0} is on the Guestlist. {1} requests remaining.".format(ua, BouncerHandler.guestlist[ua]))
   684                  self.send_response(302)
   685                  self.send_header("Location", BouncerHandler.redirect)
   686                  self.end_headers()
   687              # Otherwise return a bogus result
   688              else:
   689                  self.log_message("BouncerHandler UA {0} has no requests on the Guestlist. Sending request to the curb".format(ua))
   690                  self.send_response(200)
   691                  self.end_headers()
   692                  self.wfile.write(u"(• ◡ •) <( VIPs only! )".encode())
   693  
   694      BouncerHandler.guestlist = guestlist
   695      BouncerHandler.redirect = redirect
   696      return BouncerHandler
   697  
   698  def wait_for_server(addr):
   699      while True:
   700          try:
   701              # NOTE(@cpu): Using HEAD here instead of GET because the
   702              # BouncerHandler modifies its state for GET requests.
   703              status = requests.head(addr).status_code
   704              if status == 200:
   705                  return
   706          except requests.exceptions.ConnectionError:
   707              pass
   708          time.sleep(0.5)
   709  
   710  def multiva_setup(client, guestlist):
   711      """
   712      Setup a testing domain and backing multiva server setup. This will block
   713      until the server is ready. The returned cleanup function should be used to
   714      stop the server. The first bounceFirst requests to the server will be sent
   715      to the real challtestsrv for a good answer, the rest will get a bad
   716      answer. Domain name is randomly chosen with random_domain().
   717      """
   718      hostname = random_domain()
   719  
   720      csr_pem = chisel2.make_csr([hostname])
   721      order = client.new_order(csr_pem)
   722      authz = order.authorizations[0]
   723      chall = None
   724      for c in authz.body.challenges:
   725          if isinstance(c.chall, challenges.HTTP01):
   726              chall = c.chall
   727      if chall is None:
   728          raise(Exception("No HTTP-01 challenge found for random domain authz"))
   729  
   730      token = chall.encode("token")
   731  
   732      # Calculate the challenge's keyauth so we can add a good keyauth response on
   733      # the real challtestsrv that we redirect VIP requests to.
   734      resp = chall.response(client.net.key)
   735      keyauth = resp.key_authorization
   736      challSrv.add_http01_response(token, keyauth)
   737  
   738      # Add an A record for the domains to ensure the VA's requests are directed
   739      # to the interface that we bound the HTTPServer to.
   740      challSrv.add_a_record(hostname, ["64.112.117.134"])
   741  
   742      # Add an A record for the redirect target that sends it to the real chall
   743      # test srv for a valid HTTP-01 response.
   744      redirHostname = "chall-test-srv.example.com"
   745      challSrv.add_a_record(redirHostname, ["64.112.117.122"])
   746  
   747      # Start a simple python HTTP server on port 80 in its own thread.
   748      # NOTE(@cpu): The chall-test-srv binds 64.112.117.122:80 for HTTP-01
   749      # challenges so we must use the 64.112.117.134 address for the throw away
   750      # server for this test and add a mock DNS entry that directs the VA to it.
   751      redirect = "http://{0}/.well-known/acme-challenge/{1}".format(
   752              redirHostname, token)
   753      httpd = HTTPServer(("64.112.117.134", 80), BouncerHTTPRequestHandler(redirect, guestlist))
   754      thread = threading.Thread(target = httpd.serve_forever)
   755      thread.daemon = False
   756      thread.start()
   757  
   758      def cleanup():
   759          # Remove the challtestsrv mocks
   760          challSrv.remove_a_record(hostname)
   761          challSrv.remove_a_record(redirHostname)
   762          challSrv.remove_http01_response(token)
   763          # Shut down the HTTP server gracefully and join on its thread.
   764          httpd.shutdown()
   765          httpd.server_close()
   766          thread.join()
   767  
   768      return hostname, cleanup
   769  
   770  def test_http_multiva_threshold_pass():
   771      client = chisel2.make_client()
   772  
   773      # Configure a guestlist that will pass the multiVA threshold test by
   774      # allowing the primary VA at some, but not all, remotes.
   775      # In particular, remoteva-c is missing.
   776      guestlist = {"boulder": 1, "remoteva-a": 1, "remoteva-b": 1}
   777  
   778      hostname, cleanup = multiva_setup(client, guestlist)
   779  
   780      try:
   781          # With the maximum number of allowed remote VA failures the overall
   782          # challenge should still succeed.
   783          chisel2.auth_and_issue([hostname], client=client, chall_type="http-01")
   784      finally:
   785          cleanup()
   786  
   787  def test_http_multiva_primary_fail_remote_pass():
   788      client = chisel2.make_client()
   789  
   790      # Configure a guestlist that will fail the primary VA check but allow all of
   791      # the remote VAs.
   792      guestlist = {"boulder": 0, "remoteva-a": 1, "remoteva-b": 1}
   793  
   794      hostname, cleanup = multiva_setup(client, guestlist)
   795  
   796      foundException = False
   797  
   798      try:
   799          # The overall validation should fail even if the remotes are allowed
   800          # because the primary VA result cannot be overridden.
   801          chisel2.auth_and_issue([hostname], client=client, chall_type="http-01")
   802      except acme_errors.ValidationError as e:
   803          # NOTE(@cpu): Chisel2's expect_problem doesn't work in this case so this
   804          # test needs to unpack an `acme_errors.ValidationError` on its own. It
   805          # might be possible to clean this up in the future.
   806          if len(e.failed_authzrs) != 1:
   807              raise(Exception("expected one failed authz, found {0}".format(len(e.failed_authzrs))))
   808          challs = e.failed_authzrs[0].body.challenges
   809          httpChall = None
   810          for chall_body in challs:
   811              if isinstance(chall_body.chall, challenges.HTTP01):
   812                  httpChall = chall_body
   813          if httpChall is None:
   814              raise(Exception("no HTTP-01 challenge in failed authz"))
   815          if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized":
   816              raise(Exception("expected unauthorized prob, found {0}".format(httpChall.error.typ)))
   817          foundException = True
   818      finally:
   819          cleanup()
   820          if foundException is False:
   821              raise(Exception("Overall validation did not fail"))
   822  
   823  def test_http_multiva_threshold_fail():
   824      client = chisel2.make_client()
   825  
   826      # Configure a guestlist that will fail the multiVA threshold test by
   827      # only allowing the primary VA.
   828      guestlist = {"boulder": 1}
   829  
   830      hostname, cleanup = multiva_setup(client, guestlist)
   831  
   832      failed_authzrs = []
   833      try:
   834          chisel2.auth_and_issue([hostname], client=client, chall_type="http-01")
   835      except acme_errors.ValidationError as e:
   836          # NOTE(@cpu): Chisel2's expect_problem doesn't work in this case so this
   837          # test needs to unpack an `acme_errors.ValidationError` on its own. It
   838          # might be possible to clean this up in the future.
   839          failed_authzrs = e.failed_authzrs
   840      finally:
   841          cleanup()
   842      if len(failed_authzrs) != 1:
   843          raise(Exception("expected one failed authz, found {0}".format(len(failed_authzrs))))
   844      challs = failed_authzrs[0].body.challenges
   845      httpChall = None
   846      for chall_body in challs:
   847          if isinstance(chall_body.chall, challenges.HTTP01):
   848              httpChall = chall_body
   849      if httpChall is None:
   850          raise(Exception("no HTTP-01 challenge in failed authz"))
   851      if httpChall.error.typ != "urn:ietf:params:acme:error:unauthorized":
   852          raise(Exception("expected unauthorized prob, found {0}".format(httpChall.error.typ)))
   853      if not httpChall.error.detail.startswith("During secondary validation: "):
   854          raise(Exception("expected 'During secondary validation' problem detail, found {0}".format(httpChall.error.detail)))
   855  
   856  class FakeH2ServerHandler(socketserver.BaseRequestHandler):
   857      """
   858      FakeH2ServerHandler is a TCP socket handler that writes data representing an
   859      initial HTTP/2 SETTINGS frame as a response to all received data.
   860      """
   861      def handle(self):
   862          # Read whatever the HTTP request was so that the response isn't seen as
   863          # unsolicited.
   864          self.data = self.request.recv(1024).strip()
   865          # Blast some HTTP/2 bytes onto the socket
   866          # Truncated example data from taken from the community forum:
   867          # https://community.letsencrypt.org/t/le-validation-error-if-server-is-in-google-infrastructure/51841
   868          self.request.sendall(b"\x00\x00\x12\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x80\x00")
   869  
   870  def wait_for_tcp_server(addr, port):
   871      """
   872      wait_for_tcp_server attempts to make a TCP connection to the given
   873      address/port every 0.5s until it succeeds.
   874      """
   875      while True:
   876          sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   877          try:
   878              sock.connect((addr, port))
   879              sock.sendall(b"\n")
   880              return
   881          except socket.error:
   882              time.sleep(0.5)
   883              pass
   884  
   885  def test_http2_http01_challenge():
   886      """
   887      test_http2_http01_challenge tests that an HTTP-01 challenge made to a HTTP/2
   888      server fails with a specific error message for this case.
   889      """
   890      client = chisel2.make_client()
   891      hostname = "fake.h2.example.com"
   892  
   893      # Add an A record for the test server to ensure the VA's requests are directed
   894      # to the interface that we bind the FakeH2ServerHandler to.
   895      challSrv.add_a_record(hostname, ["64.112.117.134"])
   896  
   897      # Allow socket address reuse on the base TCPServer class. Failing to do this
   898      # causes subsequent integration tests to fail with "Address in use" errors even
   899      # though this test _does_ call shutdown() and server_close(). Even though the
   900      # server was shut-down Python's socket will be in TIME_WAIT because of prev. client
   901      # connections. Having the TCPServer set SO_REUSEADDR on the socket solves
   902      # the problem.
   903      socketserver.TCPServer.allow_reuse_address = True
   904      # Create, start, and wait for a fake HTTP/2 server.
   905      server = socketserver.TCPServer(("64.112.117.134", 80), FakeH2ServerHandler)
   906      thread = threading.Thread(target = server.serve_forever)
   907      thread.daemon = False
   908      thread.start()
   909      wait_for_tcp_server("64.112.117.134", 80)
   910  
   911      # Issuing an HTTP-01 challenge for this hostname should produce a connection
   912      # problem with an error specific to the HTTP/2 misconfiguration.
   913      expectedError = "Server is speaking HTTP/2 over HTTP"
   914      try:
   915          chisel2.auth_and_issue([hostname], client=client, chall_type="http-01")
   916      except acme_errors.ValidationError as e:
   917          for authzr in e.failed_authzrs:
   918              c = chisel2.get_chall(authzr, challenges.HTTP01)
   919              error = c.error
   920              if error is None or error.typ != "urn:ietf:params:acme:error:connection":
   921                  raise(Exception("Expected connection prob, got %s" % (error.__str__())))
   922              if not error.detail.endswith(expectedError):
   923                  raise(Exception("Expected prob detail ending in %s, got %s" % (expectedError, error.detail)))
   924      finally:
   925          server.shutdown()
   926          server.server_close()
   927          thread.join()
   928  
   929  def test_new_order_policy_errs():
   930      """
   931      Test that creating an order with policy blocked identifiers returns
   932      a problem with subproblems.
   933      """
   934      client = chisel2.make_client(None)
   935  
   936      # 'in-addr.arpa' is present in `test/ident-policy.yaml`'s
   937      # HighRiskBlockedNames list.
   938      csr_pem = chisel2.make_csr(["out-addr.in-addr.arpa", "between-addr.in-addr.arpa"])
   939  
   940      # With two policy blocked names in the order we expect to get back a top
   941      # level rejectedIdentifier with a detail message that references
   942      # subproblems.
   943      #
   944      # TODO(@cpu): After https://github.com/certbot/certbot/issues/7046 is
   945      # implemented in the upstream `acme` module this test should also ensure the
   946      # subproblems are properly represented.
   947      ok = False
   948      try:
   949          order = client.new_order(csr_pem)
   950      except messages.Error as e:
   951          ok = True
   952          if e.typ != "urn:ietf:params:acme:error:rejectedIdentifier":
   953              raise(Exception("Expected rejectedIdentifier type problem, got {0}".format(e.typ)))
   954          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.)':
   955              raise(Exception("Order problem detail did not match expected"))
   956      if not ok:
   957          raise(Exception("Expected problem, got no error"))
   958  
   959  def test_delete_unused_challenges():
   960      order = chisel2.auth_and_issue([random_domain()], chall_type="dns-01")
   961      a = order.authorizations[0]
   962      if len(a.body.challenges) != 1:
   963          raise(Exception("too many challenges (%d) left after validation" % len(a.body.challenges)))
   964      if not isinstance(a.body.challenges[0].chall, challenges.DNS01):
   965          raise(Exception("wrong challenge type left after validation"))
   966  
   967      # intentionally fail a challenge
   968      client = chisel2.make_client()
   969      csr_pem = chisel2.make_csr([random_domain()])
   970      order = client.new_order(csr_pem)
   971      c = chisel2.get_chall(order.authorizations[0], challenges.DNS01)
   972      client.answer_challenge(c, c.response(client.net.key))
   973      for _ in range(5):
   974          a, _ = client.poll(order.authorizations[0])
   975          if a.body.status == Status("invalid"):
   976              break
   977          time.sleep(1)
   978      if len(a.body.challenges) != 1:
   979          raise(Exception("too many challenges (%d) left after failed validation" %
   980              len(a.body.challenges)))
   981      if not isinstance(a.body.challenges[0].chall, challenges.DNS01):
   982          raise(Exception("wrong challenge type left after validation"))
   983  
   984  def test_auth_deactivation_v2():
   985      client = chisel2.make_client(None)
   986      csr_pem = chisel2.make_csr([random_domain()])
   987      order = client.new_order(csr_pem)
   988      resp = client.deactivate_authorization(order.authorizations[0])
   989      if resp.body.status is not messages.STATUS_DEACTIVATED:
   990          raise(Exception("unexpected authorization status"))
   991  
   992      order = chisel2.auth_and_issue([random_domain()], client=client)
   993      resp = client.deactivate_authorization(order.authorizations[0])
   994      if resp.body.status is not messages.STATUS_DEACTIVATED:
   995          raise(Exception("unexpected authorization status"))
   996  
   997  def test_ct_submission():
   998      hostname = random_domain()
   999  
  1000      chisel2.auth_and_issue([hostname])
  1001  
  1002      # These should correspond to the configured logs in ra.json.
  1003      log_groups = [
  1004          ["http://boulder.service.consul:4600/submissions", "http://boulder.service.consul:4601/submissions", "http://boulder.service.consul:4602/submissions", "http://boulder.service.consul:4603/submissions"],
  1005          ["http://boulder.service.consul:4604/submissions", "http://boulder.service.consul:4605/submissions"],
  1006          ["http://boulder.service.consul:4606/submissions"],
  1007          ["http://boulder.service.consul:4607/submissions"],
  1008          ["http://boulder.service.consul:4608/submissions"],
  1009          ["http://boulder.service.consul:4609/submissions"],
  1010      ]
  1011  
  1012      # These should correspond to the logs with `submitFinal` in ra.json.
  1013      final_logs = [
  1014          "http://boulder.service.consul:4600/submissions",
  1015          "http://boulder.service.consul:4601/submissions",
  1016          "http://boulder.service.consul:4606/submissions",
  1017          "http://boulder.service.consul:4609/submissions",
  1018       ]
  1019  
  1020      # We'd like to enforce strict limits here (exactly 1 submission per group,
  1021      # exactly two submissions overall) but the async nature of the race system
  1022      # means we can't -- a slowish submission to one log in a group could trigger
  1023      # a very fast submission to a different log in the same group, and then both
  1024      # submissions could succeed at the same time. Although the Go code will only
  1025      # use one of the SCTs, both logs will still have been submitted to, and it
  1026      # will show up here.
  1027      total_count = 0
  1028      for i in range(len(log_groups)):
  1029          group_count = 0
  1030          for j in range(len(log_groups[i])):
  1031              log = log_groups[i][j]
  1032              count = int(requests.get(log + "?hostnames=%s" % hostname).text)
  1033              threshold = 1
  1034              if log in final_logs:
  1035                  threshold += 1
  1036              if count > threshold:
  1037                  raise(Exception("Got %d submissions for log %s, expected at most %d" % (count, log, threshold)))
  1038              group_count += count
  1039          total_count += group_count
  1040      if total_count < 2:
  1041          raise(Exception("Got %d total submissions, expected at least 2" % total_count))
  1042  
  1043  def test_caa_good():
  1044      domain = random_domain()
  1045      challSrv.add_caa_issue(domain, "happy-hacker-ca.invalid")
  1046      chisel2.auth_and_issue([domain])
  1047  
  1048  def test_caa_reject():
  1049      domain = random_domain()
  1050      challSrv.add_caa_issue(domain, "sad-hacker-ca.invalid")
  1051      chisel2.expect_problem("urn:ietf:params:acme:error:caa",
  1052          lambda: chisel2.auth_and_issue([domain]))
  1053  
  1054  def test_renewal_exemption():
  1055      """
  1056      Under a single domain, issue two certificates for different subdomains of
  1057      the same name, then renewals of each of them. Since the certificatesPerName
  1058      rate limit in testing is 2 per 90 days, and the renewals should not be
  1059      counted under the renewal exemption, each of these issuances should succeed.
  1060      Then do one last issuance (for a third subdomain of the same name) that we
  1061      expect to be rate limited, just to check that the rate limit is actually 2,
  1062      and we are testing what we think we are testing. See
  1063      https://letsencrypt.org/docs/rate-limits/ for more details.
  1064      """
  1065      base_domain = random_domain()
  1066      # First issuance
  1067      chisel2.auth_and_issue(["www." + base_domain])
  1068      # First Renewal
  1069      chisel2.auth_and_issue(["www." + base_domain])
  1070      # Issuance of a different cert
  1071      chisel2.auth_and_issue(["blog." + base_domain])
  1072      # Renew that one
  1073      chisel2.auth_and_issue(["blog." + base_domain])
  1074      # Final, failed issuance, for another different cert
  1075      chisel2.expect_problem("urn:ietf:params:acme:error:rateLimited",
  1076          lambda: chisel2.auth_and_issue(["mail." + base_domain]))
  1077  
  1078  def test_oversized_csr():
  1079      # Number of names is chosen to be one greater than the configured RA/CA maxNames
  1080      numNames = 101
  1081      # Generate numNames subdomains of a random domain
  1082      base_domain = random_domain()
  1083      domains = [ "{0}.{1}".format(str(n),base_domain) for n in range(numNames) ]
  1084      # We expect issuing for these domains to produce a malformed error because
  1085      # there are too many names in the request.
  1086      chisel2.expect_problem("urn:ietf:params:acme:error:malformed",
  1087              lambda: chisel2.auth_and_issue(domains))
  1088  
  1089  def parse_cert(order):
  1090      return x509.load_pem_x509_certificate(order.fullchain_pem.encode(), default_backend())
  1091  
  1092  def test_sct_embedding():
  1093      order = chisel2.auth_and_issue([random_domain()])
  1094      cert = parse_cert(order)
  1095  
  1096      # make sure there is no poison extension
  1097      try:
  1098          cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3"))
  1099          raise(Exception("certificate contains CT poison extension"))
  1100      except x509.ExtensionNotFound:
  1101          # do nothing
  1102          pass
  1103  
  1104      # make sure there is a SCT list extension
  1105      try:
  1106          sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2"))
  1107      except x509.ExtensionNotFound:
  1108          raise(Exception("certificate doesn't contain SCT list extension"))
  1109      if len(sctList.value) != 2:
  1110          raise(Exception("SCT list contains wrong number of SCTs"))
  1111      for sct in sctList.value:
  1112          if sct.version != x509.certificate_transparency.Version.v1:
  1113              raise(Exception("SCT contains wrong version"))
  1114          if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE:
  1115              raise(Exception("SCT contains wrong entry type"))
  1116          delta = sct.timestamp - datetime.datetime.now()
  1117          if abs(delta) > datetime.timedelta(hours=1):
  1118              raise(Exception("Delta between SCT timestamp and now was too great "
  1119                  "%s vs %s (%s)" % (sct.timestamp, datetime.datetime.now(), delta)))
  1120  
  1121  def test_auth_deactivation():
  1122      client = chisel2.make_client(None)
  1123      d = random_domain()
  1124      csr_pem = chisel2.make_csr([d])
  1125      order = client.new_order(csr_pem)
  1126  
  1127      resp = client.deactivate_authorization(order.authorizations[0])
  1128      if resp.body.status is not messages.STATUS_DEACTIVATED:
  1129          raise Exception("unexpected authorization status")
  1130  
  1131      order = chisel2.auth_and_issue([random_domain()], client=client)
  1132      resp = client.deactivate_authorization(order.authorizations[0])
  1133      if resp.body.status is not messages.STATUS_DEACTIVATED:
  1134          raise Exception("unexpected authorization status")