1"""
2A simple client that uses the Python ACME library to run a test issuance against
3a local Boulder server.
4Usage:
5
6$ virtualenv venv
7$ . venv/bin/activate
8$ pip install -r requirements.txt
9$ python chisel2.py foo.com bar.com
10"""
11import json
12import logging
13import os
14import sys
15import signal
16import threading
17import time
18
19from cryptography.hazmat.backends import default_backend
20from cryptography.hazmat.primitives.asymmetric import rsa
21from cryptography import x509
22from cryptography.hazmat.primitives import hashes
23
24import OpenSSL
25import josepy
26
27from acme import challenges
28from acme import client as acme_client
29from acme import crypto_util as acme_crypto_util
30from acme import errors as acme_errors
31from acme import messages
32from acme import standalone
33
34logging.basicConfig()
35logger = logging.getLogger()
36logger.setLevel(int(os.getenv('LOGLEVEL', 20)))
37
38DIRECTORY_V2 = os.getenv('DIRECTORY_V2', 'http://boulder.service.consul:4001/directory')
39ACCEPTABLE_TOS = os.getenv('ACCEPTABLE_TOS',"https://boulder.service.consul:4431/terms/v7")
40PORT = os.getenv('PORT', '80')
41
42os.environ.setdefault('REQUESTS_CA_BUNDLE', 'test/wfe-tls/minica.pem')
43
44import challtestsrv
45challSrv = challtestsrv.ChallTestServer()
46
47def 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
54def 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
65class 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
72class EmailRequiredError(ValueError):
73 """
74 An error that occurs when a None email is provided to update_email.
75 """
76
77def 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
93def 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
99def make_csr(domains):
100 key = OpenSSL.crypto.PKey()
101 key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
102 pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
103 return acme_crypto_util.make_csr(pem, domains, False)
104
105def http_01_answer(client, chall_body):
106 """Return an HTTP01Resource to server in response to the given challenge."""
107 response, validation = chall_body.response_and_validation(client.net.key)
108 return standalone.HTTP01RequestHandler.HTTP01Resource(
109 chall=chall_body.chall, response=response,
110 validation=validation)
111
112def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, client=None):
113 """Make authzs for each of the given domains, set up a server to answer the
114 challenges in those authzs, tell the ACME server to validate the challenges,
115 then poll for the authzs to be ready and issue a cert."""
116 if client is None:
117 client = make_client(email)
118
119 csr_pem = make_csr(domains)
120 order = client.new_order(csr_pem)
121 authzs = order.authorizations
122
123 if chall_type == "http-01":
124 cleanup = do_http_challenges(client, authzs)
125 elif chall_type == "dns-01":
126 cleanup = do_dns_challenges(client, authzs)
127 elif chall_type == "tls-alpn-01":
128 cleanup = do_tlsalpn_challenges(client, authzs)
129 else:
130 raise Exception("invalid challenge type %s" % chall_type)
131
132 try:
133 order = client.poll_and_finalize(order)
134 if cert_output is not None:
135 with open(cert_output, "w") as f:
136 f.write(order.fullchain_pem)
137 finally:
138 cleanup()
139
140 return order
141
142def do_dns_challenges(client, authzs):
143 cleanup_hosts = []
144 for a in authzs:
145 c = get_chall(a, challenges.DNS01)
146 name, value = (c.validation_domain_name(a.body.identifier.value),
147 c.validation(client.net.key))
148 cleanup_hosts.append(name)
149 challSrv.add_dns01_response(name, value)
150 client.answer_challenge(c, c.response(client.net.key))
151 def cleanup():
152 for host in cleanup_hosts:
153 challSrv.remove_dns01_response(host)
154 return cleanup
155
156def do_http_challenges(client, authzs):
157 cleanup_tokens = []
158 challs = [get_chall(a, challenges.HTTP01) for a in authzs]
159
160 for chall_body in challs:
161 # Determine the token and key auth for the challenge
162 token = chall_body.chall.encode("token")
163 resp = chall_body.response(client.net.key)
164 keyauth = resp.key_authorization
165
166 # Add the HTTP-01 challenge response for this token/key auth to the
167 # challtestsrv
168 challSrv.add_http01_response(token, keyauth)
169 cleanup_tokens.append(token)
170
171 # Then proceed initiating the challenges with the ACME server
172 client.answer_challenge(chall_body, chall_body.response(client.net.key))
173
174 def cleanup():
175 # Cleanup requires removing each of the HTTP-01 challenge responses for
176 # the tokens we added.
177 for token in cleanup_tokens:
178 challSrv.remove_http01_response(token)
179 return cleanup
180
181def do_tlsalpn_challenges(client, authzs):
182 cleanup_hosts = []
183 for a in authzs:
184 c = get_chall(a, challenges.TLSALPN01)
185 name, value = (a.body.identifier.value, c.key_authorization(client.net.key))
186 cleanup_hosts.append(name)
187 challSrv.add_tlsalpn01_response(name, value)
188 client.answer_challenge(c, c.response(client.net.key))
189 def cleanup():
190 for host in cleanup_hosts:
191 challSrv.remove_tlsalpn01_response(host)
192 return cleanup
193
194def expect_problem(problem_type, func):
195 """Run a function. If it raises an acme_errors.ValidationError or messages.Error that
196 contains the given problem_type, return. If it raises no error or the wrong
197 error, raise an exception."""
198 ok = False
199 try:
200 func()
201 except messages.Error as e:
202 if e.typ == problem_type:
203 ok = True
204 else:
205 raise Exception("Expected %s, got %s" % (problem_type, e.__str__()))
206 except acme_errors.ValidationError as e:
207 for authzr in e.failed_authzrs:
208 for chall in authzr.body.challenges:
209 error = chall.error
210 if error and error.typ == problem_type:
211 ok = True
212 elif error:
213 raise Exception("Expected %s, got %s" % (problem_type, error.__str__()))
214 if not ok:
215 raise Exception('Expected %s, got no error' % problem_type)
216
217if __name__ == "__main__":
218 # Die on SIGINT
219 signal.signal(signal.SIGINT, signal.SIG_DFL)
220 domains = sys.argv[1:]
221 if len(domains) == 0:
222 print(__doc__)
223 sys.exit(0)
224 try:
225 auth_and_issue(domains)
226 except messages.Error as e:
227 print(e)
228 sys.exit(1)
View as plain text