ACME: add support for POST-as-GET if GET fails with 405. (#44988)

* Add support for POST-as-GET if GET fails with 405.

* Bumping ACME test container version to 1.4. This includes letsencrypt/pebble#162 and letsencrypt/pebble#168.

* Also use POST-as-GET for account data retrival.

This is not yet supported by any ACME server (see letsencrypt/pebble#171),
so we fall back to a regular empty update if a 'malformedRequest' error is
returned.

* Using newest ACME test container image.

Includes letsencrypt/pebble#171 and letsencrypt/pebble#172, which make Pebble behave closer to the current specs.

* Remove workaround for old Pebble version.

* Add changelog entry.

* First try POST-as-GET, then fall back to unauthenticated GET.

(cherry picked from commit 92d9569bc9)
pull/48175/head
Felix Fontein 6 years ago committed by Matt Clay
parent 83e2fa7473
commit 61f76d7410

@ -0,0 +1,2 @@
bugfixes:
- "ACME modules support `POST-as-GET <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380>`__ and will be able to access Let's Encrypt ACME v2 endpoint after November 1st, 2019."

@ -140,7 +140,7 @@ class ACMEDirectory(object):
self.directory_root = module.params['acme_directory'] self.directory_root = module.params['acme_directory']
self.version = module.params['acme_version'] self.version = module.params['acme_version']
self.directory, dummy = account.get_request(self.directory_root) self.directory, dummy = account.get_request(self.directory_root, get_only=True)
# Check whether self.version matches what we expect # Check whether self.version matches what we expect
if self.version == 1: if self.version == 1:
@ -310,6 +310,9 @@ class ACMEAccount(object):
def sign_request(self, protected, payload, key_data, key): def sign_request(self, protected, payload, key_data, key):
try: try:
if payload is None:
payload64 = ''
else:
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8')) payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
except Exception as e: except Exception as e:
@ -346,6 +349,9 @@ class ACMEAccount(object):
Sends a JWS signed HTTP POST request to the ACME server and returns Sends a JWS signed HTTP POST request to the ACME server and returns
the response as dictionary the response as dictionary
https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.2 https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.2
If payload is None, a POST-as-GET is performed.
(https://tools.ietf.org/html/draft-ietf-acme-acme-15#section-6.3)
''' '''
failed_tries = 0 failed_tries = 0
while True: while True:
@ -389,7 +395,23 @@ class ACMEAccount(object):
return result, info return result, info
def get_request(self, uri, parse_json_result=True, headers=None): def get_request(self, uri, parse_json_result=True, headers=None, get_only=False):
'''
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
to GET if server replies with a status code of 405.
'''
if not get_only and self.version != 1:
# Try POST-as-GET
content, info = self.send_signed_request(uri, None, parse_json_result=False)
if info['status'] == 405:
# Instead, do unauthenticated GET
get_only = True
else:
# Do unauthenticated GET
get_only = True
if get_only:
# Perform unauthenticated GET
resp, info = fetch_url(self.module, uri, method='GET', headers=headers) resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
try: try:
@ -397,6 +419,7 @@ class ACMEAccount(object):
except AttributeError: except AttributeError:
content = info.get('body') content = info.get('body')
# Process result
if parse_json_result: if parse_json_result:
result = {} result = {}
if content: if content:
@ -477,10 +500,19 @@ class ACMEAccount(object):
''' '''
if self.uri is None: if self.uri is None:
raise ModuleFailException("Account URI unknown") raise ModuleFailException("Account URI unknown")
data = {}
if self.version == 1: if self.version == 1:
data = {}
data['resource'] = 'reg' data['resource'] = 'reg'
result, info = self.send_signed_request(self.uri, data) result, info = self.send_signed_request(self.uri, data)
else:
# try POST-as-GET first (draft-15 or newer)
data = None
result, info = self.send_signed_request(self.uri, data)
# check whether that failed with a malformed request error
if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed':
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
data = {}
result, info = self.send_signed_request(self.uri, data)
if info['status'] == 403 and result.get('type') == 'urn:ietf:params:acme:error:unauthorized': if info['status'] == 403 and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
return None return None
if info['status'] < 200 or info['status'] >= 300: if info['status'] < 200 or info['status'] >= 300:

Loading…
Cancel
Save