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
)
This commit is contained in:
parent
e8a6bdc17a
commit
080aa35c5b
3 changed files with 45 additions and 11 deletions
2
changelogs/fragments/44988-acme-post-as-get.yaml
Normal file
2
changelogs/fragments/44988-acme-post-as-get.yaml
Normal file
|
@ -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."
|
|
@ -451,7 +451,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:
|
||||||
|
@ -534,7 +534,10 @@ class ACMEAccount(object):
|
||||||
|
|
||||||
def sign_request(self, protected, payload, key_data):
|
def sign_request(self, protected, payload, key_data):
|
||||||
try:
|
try:
|
||||||
payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8'))
|
if payload is None:
|
||||||
|
payload64 = ''
|
||||||
|
else:
|
||||||
|
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:
|
||||||
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
|
||||||
|
@ -549,6 +552,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-14#section-6.2
|
https://tools.ietf.org/html/draft-ietf-acme-acme-14#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)
|
||||||
'''
|
'''
|
||||||
key_data = key_data or self.key_data
|
key_data = key_data or self.key_data
|
||||||
jws_header = jws_header or self.jws_header
|
jws_header = jws_header or self.jws_header
|
||||||
|
@ -594,14 +600,31 @@ 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):
|
||||||
resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
|
'''
|
||||||
|
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
|
||||||
|
|
||||||
try:
|
if get_only:
|
||||||
content = resp.read()
|
# Perform unauthenticated GET
|
||||||
except AttributeError:
|
resp, info = fetch_url(self.module, uri, method='GET', headers=headers)
|
||||||
content = info.get('body')
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = resp.read()
|
||||||
|
except AttributeError:
|
||||||
|
content = info.get('body')
|
||||||
|
|
||||||
|
# Process result
|
||||||
if parse_json_result:
|
if parse_json_result:
|
||||||
result = {}
|
result = {}
|
||||||
if content:
|
if content:
|
||||||
|
@ -682,10 +705,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'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
|
if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
|
||||||
# Returned when account is deactivated
|
# Returned when account is deactivated
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -43,7 +43,7 @@ class ACMEProvider(CloudProvider):
|
||||||
if os.environ.get('ANSIBLE_ACME_CONTAINER'):
|
if os.environ.get('ANSIBLE_ACME_CONTAINER'):
|
||||||
self.image = os.environ.get('ANSIBLE_ACME_CONTAINER')
|
self.image = os.environ.get('ANSIBLE_ACME_CONTAINER')
|
||||||
else:
|
else:
|
||||||
self.image = 'quay.io/ansible/acme-test-container:1.3.0'
|
self.image = 'quay.io/ansible/acme-test-container:1.4.1'
|
||||||
self.container_name = ''
|
self.container_name = ''
|
||||||
|
|
||||||
def _wait_for_service(self, protocol, acme_host, port, local_part, name):
|
def _wait_for_service(self, protocol, acme_host, port, local_part, name):
|
||||||
|
|
Loading…
Reference in a new issue