1"""Example ACME-V2 API for HTTP-01 challenge.
2
3Brief:
4
5This a complete usage example of the python-acme API.
6
7Limitations of this example:
8    - Works for only one Domain name
9    - Performs only HTTP-01 challenge
10    - Uses ACME-v2
11
12Workflow:
13    (Account creation)
14    - Create account key
15    - Register account and accept TOS
16    (Certificate actions)
17    - Select HTTP-01 within offered challenges by the CA server
18    - Set up http challenge resource
19    - Set up standalone web server
20    - Create domain private key and CSR
21    - Issue certificate
22    - Renew certificate
23    - Revoke certificate
24    (Account update actions)
25    - Change contact information
26    - Deactivate Account
27"""
28from contextlib import contextmanager
29
30from cryptography.hazmat.backends import default_backend
31from cryptography.hazmat.primitives.asymmetric import rsa
32import josepy as jose
33import OpenSSL
34
35from acme import challenges
36from acme import client
37from acme import crypto_util
38from acme import errors
39from acme import messages
40from acme import standalone
41
42# Constants:
43
44# This is the staging point for ACME-V2 within Let's Encrypt.
45DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'
46
47USER_AGENT = 'python-acme-example'
48
49# Account key size
50ACC_KEY_BITS = 2048
51
52# Certificate private key size
53CERT_PKEY_BITS = 2048
54
55# Domain name for the certificate.
56DOMAIN = 'client.example.com'
57
58# If you are running Boulder locally, it is possible to configure any port
59# number to execute the challenge, but real CA servers will always use port
60# 80, as described in the ACME specification.
61PORT = 80
62
63
64# Useful methods and classes:
65
66
67def new_csr_comp(domain_name, pkey_pem=None):
68    """Create certificate signing request."""
69    if pkey_pem is None:
70        # Create private key.
71        pkey = OpenSSL.crypto.PKey()
72        pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS)
73        pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
74                                                  pkey)
75    csr_pem = crypto_util.make_csr(pkey_pem, [domain_name])
76    return pkey_pem, csr_pem
77
78
79def select_http01_chall(orderr):
80    """Extract authorization resource from within order resource."""
81    # Authorization Resource: authz.
82    # This object holds the offered challenges by the server and their status.
83    authz_list = orderr.authorizations
84
85    for authz in authz_list:
86        # Choosing challenge.
87        # authz.body.challenges is a set of ChallengeBody objects.
88        for i in authz.body.challenges:
89            # Find the supported challenge.
90            if isinstance(i.chall, challenges.HTTP01):
91                return i
92
93    raise Exception('HTTP-01 challenge was not offered by the CA server.')
94
95
96@contextmanager
97def challenge_server(http_01_resources):
98    """Manage standalone server set up and shutdown."""
99
100    # Setting up a fake server that binds at PORT and any address.
101    address = ('', PORT)
102    try:
103        servers = standalone.HTTP01DualNetworkedServers(address,
104                                                        http_01_resources)
105        # Start client standalone web server.
106        servers.serve_forever()
107        yield servers
108    finally:
109        # Shutdown client web server and unbind from PORT
110        servers.shutdown_and_server_close()
111
112
113def perform_http01(client_acme, challb, orderr):
114    """Set up standalone webserver and perform HTTP-01 challenge."""
115
116    response, validation = challb.response_and_validation(client_acme.net.key)
117
118    resource = standalone.HTTP01RequestHandler.HTTP01Resource(
119        chall=challb.chall, response=response, validation=validation)
120
121    with challenge_server({resource}):
122        # Let the CA server know that we are ready for the challenge.
123        client_acme.answer_challenge(challb, response)
124
125        # Wait for challenge status and then issue a certificate.
126        # It is possible to set a deadline time.
127        finalized_orderr = client_acme.poll_and_finalize(orderr)
128
129    return finalized_orderr.fullchain_pem
130
131
132# Main examples:
133
134
135def example_http():
136    """This example executes the whole process of fulfilling a HTTP-01
137    challenge for one specific domain.
138
139    The workflow consists of:
140    (Account creation)
141    - Create account key
142    - Register account and accept TOS
143    (Certificate actions)
144    - Select HTTP-01 within offered challenges by the CA server
145    - Set up http challenge resource
146    - Set up standalone web server
147    - Create domain private key and CSR
148    - Issue certificate
149    - Renew certificate
150    - Revoke certificate
151    (Account update actions)
152    - Change contact information
153    - Deactivate Account
154
155    """
156    # Create account key
157
158    acc_key = jose.JWKRSA(
159        key=rsa.generate_private_key(public_exponent=65537,
160                                     key_size=ACC_KEY_BITS,
161                                     backend=default_backend()))
162
163    # Register account and accept TOS
164
165    net = client.ClientNetwork(acc_key, user_agent=USER_AGENT)
166    directory = messages.Directory.from_json(net.get(DIRECTORY_URL).json())
167    client_acme = client.ClientV2(directory, net=net)
168
169    # Terms of Service URL is in client_acme.directory.meta.terms_of_service
170    # Registration Resource: regr
171    # Creates account with contact information.
172    email = ('fake@example.com')
173    regr = client_acme.new_account(
174        messages.NewRegistration.from_data(
175            email=email, terms_of_service_agreed=True))
176
177    # Create domain private key and CSR
178    pkey_pem, csr_pem = new_csr_comp(DOMAIN)
179
180    # Issue certificate
181
182    orderr = client_acme.new_order(csr_pem)
183
184    # Select HTTP-01 within offered challenges by the CA server
185    challb = select_http01_chall(orderr)
186
187    # The certificate is ready to be used in the variable "fullchain_pem".
188    fullchain_pem = perform_http01(client_acme, challb, orderr)
189
190    # Renew certificate
191
192    _, csr_pem = new_csr_comp(DOMAIN, pkey_pem)
193
194    orderr = client_acme.new_order(csr_pem)
195
196    challb = select_http01_chall(orderr)
197
198    # Performing challenge
199    fullchain_pem = perform_http01(client_acme, challb, orderr)
200
201    # Revoke certificate
202
203    fullchain_com = jose.ComparableX509(
204        OpenSSL.crypto.load_certificate(
205            OpenSSL.crypto.FILETYPE_PEM, fullchain_pem))
206
207    try:
208        client_acme.revoke(fullchain_com, 0)  # revocation reason = 0
209    except errors.ConflictError:
210        # Certificate already revoked.
211        pass
212
213    # Query registration status.
214    client_acme.net.account = regr
215    try:
216        regr = client_acme.query_registration(regr)
217    except errors.Error as err:
218        if err.typ == messages.OLD_ERROR_PREFIX + 'unauthorized' \
219                or err.typ == messages.ERROR_PREFIX + 'unauthorized':
220            # Status is deactivated.
221            pass
222        raise
223
224    # Change contact information
225
226    email = 'newfake@example.com'
227    regr = client_acme.update_registration(
228        regr.update(
229            body=regr.body.update(
230                contact=('mailto:' + email,)
231            )
232        )
233    )
234
235    # Deactivate account/registration
236
237    regr = client_acme.deactivate_registration(regr)
238
239
240if __name__ == "__main__":
241    example_http()
242