github.com/letsencrypt/boulder@v0.20251208.0/test/chisel2.py (about) 1 """ 2 A simple client that uses the Python ACME library to run a test issuance against 3 a local Boulder server. 4 Usage: 5 6 $ virtualenv venv 7 $ . venv/bin/activate 8 $ pip install -r requirements.txt 9 $ python chisel2.py foo.com bar.com 10 """ 11 import json 12 import logging 13 import os 14 import sys 15 import signal 16 import threading 17 import time 18 19 from cryptography.hazmat.backends import default_backend 20 from cryptography.hazmat.primitives.asymmetric import rsa 21 from cryptography import x509 22 from cryptography.hazmat.primitives import hashes 23 24 import OpenSSL 25 import josepy 26 27 from acme import challenges 28 from acme import client as acme_client 29 from acme import crypto_util as acme_crypto_util 30 from acme import errors as acme_errors 31 from acme import messages 32 from acme import standalone 33 34 logging.basicConfig() 35 logger = logging.getLogger() 36 logger.setLevel(int(os.getenv('LOGLEVEL', 20))) 37 38 DIRECTORY_V2 = os.getenv('DIRECTORY_V2', 'http://boulder.service.consul:4001/directory') 39 ACCEPTABLE_TOS = os.getenv('ACCEPTABLE_TOS',"https://boulder.service.consul:4431/terms/v7") 40 PORT = os.getenv('PORT', '80') 41 42 os.environ.setdefault('REQUESTS_CA_BUNDLE', 'test/certs/ipki/minica.pem') 43 44 import challtestsrv 45 challSrv = challtestsrv.ChallTestServer() 46 47 def uninitialized_client(key=None): 48 if key is None: 49 key = josepy.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend())) 50 net = acme_client.ClientNetwork(key, user_agent="Boulder integration tester") 51 directory = messages.Directory.from_json(net.get(DIRECTORY_V2).json()) 52 return acme_client.ClientV2(directory, net) 53 54 def make_client(email=None): 55 """Build an acme.Client and register a new account with a random key.""" 56 client = uninitialized_client() 57 tos = client.directory.meta.terms_of_service 58 if tos == ACCEPTABLE_TOS: 59 client.net.account = client.new_account(messages.NewRegistration.from_data(email=email, 60 terms_of_service_agreed=True)) 61 else: 62 raise Exception("Unrecognized terms of service URL %s" % tos) 63 return client 64 65 class NoClientError(ValueError): 66 """ 67 An error that occurs when no acme.Client is provided to a function that 68 requires one. 69 """ 70 pass 71 72 class EmailRequiredError(ValueError): 73 """ 74 An error that occurs when a None email is provided to update_email. 75 """ 76 77 def update_email(client, email): 78 """ 79 Use a provided acme.Client to update the client's account to the specified 80 email. 81 """ 82 if client is None: 83 raise(NoClientError("update_email requires a valid acme.Client argument")) 84 if email is None: 85 raise(EmailRequiredError("update_email requires an email argument")) 86 if not email.startswith("mailto:"): 87 email = "mailto:"+ email 88 acct = client.net.account 89 updatedAcct = acct.update(body=acct.body.update(contact=(email,))) 90 return client.update_registration(updatedAcct) 91 92 93 def get_chall(authz, typ): 94 for chall_body in authz.body.challenges: 95 if isinstance(chall_body.chall, typ): 96 return chall_body 97 raise Exception("No %s challenge found" % typ.typ) 98 99 def get_any_supported_chall(authz): 100 """ 101 Return the first supported challenge from the given authorization. 102 Supports HTTP01 and DNS01. 103 104 Note: DNS-ACCOUNT-01 challenge type is excluded from the list of supported 105 challenge types until the Python ACME library adds support for it. 106 """ 107 for chall_body in authz.body.challenges: 108 if isinstance(chall_body.chall, (challenges.HTTP01, challenges.DNS01)): 109 return chall_body 110 raise Exception("No supported challenge types found in authorization") 111 112 def make_csr(domains): 113 key = OpenSSL.crypto.PKey() 114 key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 115 pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) 116 return acme_crypto_util.make_csr(pem, domains, False) 117 118 def http_01_answer(client, chall_body): 119 """Return an HTTP01Resource to server in response to the given challenge.""" 120 response, validation = chall_body.response_and_validation(client.net.key) 121 return standalone.HTTP01RequestHandler.HTTP01Resource( 122 chall=chall_body.chall, response=response, 123 validation=validation) 124 125 def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, client=None): 126 """Make authzs for each of the given domains, set up a server to answer the 127 challenges in those authzs, tell the ACME server to validate the challenges, 128 then poll for the authzs to be ready and issue a cert.""" 129 if client is None: 130 client = make_client(email) 131 132 csr_pem = make_csr(domains) 133 order = client.new_order(csr_pem) 134 authzs = order.authorizations 135 136 if chall_type == "http-01": 137 cleanup = do_http_challenges(client, authzs) 138 elif chall_type == "dns-01": 139 cleanup = do_dns_challenges(client, authzs) 140 else: 141 raise Exception("invalid challenge type %s" % chall_type) 142 143 # Make up to three attempts, retrying on badNonce errors 144 for n in range(3): 145 time.sleep(0.2 * n) # No sleep before the first attempt, then backoff 146 try: 147 order = client.poll_and_finalize(order) 148 if cert_output is not None: 149 with open(cert_output, "w") as f: 150 f.write(order.fullchain_pem) 151 except messages.Error as e: 152 if e.typ == "urn:ietf:params:acme:error:badNonce": 153 continue 154 else: 155 break 156 finally: 157 cleanup() 158 159 return order 160 161 def do_dns_challenges(client, authzs): 162 cleanup_hosts = [] 163 for a in authzs: 164 c = get_chall(a, challenges.DNS01) 165 name, value = (c.validation_domain_name(a.body.identifier.value), 166 c.validation(client.net.key)) 167 cleanup_hosts.append(name) 168 challSrv.add_dns01_response(name, value) 169 client.answer_challenge(c, c.response(client.net.key)) 170 def cleanup(): 171 for host in cleanup_hosts: 172 challSrv.remove_dns01_response(host) 173 return cleanup 174 175 def do_http_challenges(client, authzs): 176 cleanup_tokens = [] 177 challs = [get_chall(a, challenges.HTTP01) for a in authzs] 178 179 for chall_body in challs: 180 # Determine the token and key auth for the challenge 181 token = chall_body.chall.encode("token") 182 resp = chall_body.response(client.net.key) 183 keyauth = resp.key_authorization 184 185 # Add the HTTP-01 challenge response for this token/key auth to the 186 # challtestsrv 187 challSrv.add_http01_response(token, keyauth) 188 cleanup_tokens.append(token) 189 190 # Then proceed initiating the challenges with the ACME server 191 client.answer_challenge(chall_body, chall_body.response(client.net.key)) 192 193 def cleanup(): 194 # Cleanup requires removing each of the HTTP-01 challenge responses for 195 # the tokens we added. 196 for token in cleanup_tokens: 197 challSrv.remove_http01_response(token) 198 return cleanup 199 200 def expect_problem(problem_type, func): 201 """Run a function. If it raises an acme_errors.ValidationError or messages.Error that 202 contains the given problem_type, return. If it raises no error or the wrong 203 error, raise an exception.""" 204 ok = False 205 try: 206 func() 207 except messages.Error as e: 208 if e.typ == problem_type: 209 ok = True 210 else: 211 raise Exception("Expected %s, got %s" % (problem_type, e.__str__())) 212 except acme_errors.ValidationError as e: 213 for authzr in e.failed_authzrs: 214 for chall in authzr.body.challenges: 215 error = chall.error 216 if error and error.typ == problem_type: 217 ok = True 218 elif error: 219 raise Exception("Expected %s, got %s" % (problem_type, error.__str__())) 220 if not ok: 221 raise Exception('Expected %s, got no error' % problem_type) 222 223 if __name__ == "__main__": 224 # Die on SIGINT 225 signal.signal(signal.SIGINT, signal.SIG_DFL) 226 domains = sys.argv[1:] 227 if len(domains) == 0: 228 print(__doc__) 229 sys.exit(0) 230 try: 231 auth_and_issue(domains) 232 except messages.Error as e: 233 print(e) 234 sys.exit(1)