ACME modules: make compatible to Buypass ACME v1 CA, and fix bug in ACME v1 account update (#61693)
This commit is contained in:
parent
2e5137078d
commit
c6dcf78f53
4 changed files with 45 additions and 10 deletions
3
changelogs/fragments/61693-acme-buypass-acme-v1.yml
Normal file
3
changelogs/fragments/61693-acme-buypass-acme-v1.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
bugfixes:
|
||||
- "ACME modules: support Buypass' ACME v1 endpoint"
|
||||
- "ACME modules: fix bug in ACME v1 account update code"
|
|
@ -474,6 +474,9 @@ class ACMEAccount(object):
|
|||
'''
|
||||
|
||||
def __init__(self, module):
|
||||
# Set to true to enable logging of all signed requests
|
||||
self._debug = False
|
||||
|
||||
self.module = module
|
||||
self.version = module.params['acme_version']
|
||||
# account_key path and content are mutually exclusive
|
||||
|
@ -541,6 +544,16 @@ class ACMEAccount(object):
|
|||
else:
|
||||
return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data)
|
||||
|
||||
def _log(self, msg, data=None):
|
||||
'''
|
||||
Write arguments to acme.log when logging is enabled.
|
||||
'''
|
||||
if self._debug:
|
||||
with open('acme.log', 'ab') as f:
|
||||
f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8'))
|
||||
if data is not None:
|
||||
f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8'))
|
||||
|
||||
def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True, encode_payload=True):
|
||||
'''
|
||||
Sends a JWS signed HTTP POST request to the ACME server and returns
|
||||
|
@ -559,9 +572,15 @@ class ACMEAccount(object):
|
|||
if self.version != 1:
|
||||
protected["url"] = url
|
||||
|
||||
self._log('URL', url)
|
||||
self._log('protected', protected)
|
||||
self._log('payload', payload)
|
||||
data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload)
|
||||
if self.version == 1:
|
||||
data["header"] = jws_header
|
||||
data["header"] = jws_header.copy()
|
||||
for k, v in protected.items():
|
||||
hv = data["header"].pop(k, None)
|
||||
self._log('signed request', data)
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
headers = {
|
||||
|
@ -578,6 +597,7 @@ class ACMEAccount(object):
|
|||
if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600:
|
||||
try:
|
||||
decoded_result = self.module.from_json(content.decode('utf8'))
|
||||
self._log('parsed result', decoded_result)
|
||||
# In case of badNonce error, try again (up to 5 times)
|
||||
# (https://tools.ietf.org/html/rfc8555#section-6.7)
|
||||
if (400 <= info['status'] < 600 and
|
||||
|
@ -822,6 +842,8 @@ class ACMEAccount(object):
|
|||
account_data = dict(account_data)
|
||||
account_data.update(update_request)
|
||||
else:
|
||||
if self.version == 1:
|
||||
update_request['resource'] = 'reg'
|
||||
account_data, dummy = self.send_signed_request(self.uri, update_request)
|
||||
return True, account_data
|
||||
|
||||
|
@ -940,7 +962,7 @@ def process_links(info, callback):
|
|||
'''
|
||||
if 'link' in info:
|
||||
link = info['link']
|
||||
for url, relation in re.findall(r'<([^>]+)>;rel="(\w+)"', link):
|
||||
for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
|
||||
callback(unquote(url), relation)
|
||||
|
||||
|
||||
|
|
|
@ -22,9 +22,9 @@ short_description: Create SSL/TLS certificates with the ACME protocol
|
|||
description:
|
||||
- "Create and renew SSL/TLS certificates with a CA supporting the
|
||||
L(ACME protocol,https://tools.ietf.org/html/rfc8555),
|
||||
such as L(Let's Encrypt,https://letsencrypt.org/). The current
|
||||
implementation supports the C(http-01), C(dns-01) and C(tls-alpn-01)
|
||||
challenges."
|
||||
such as L(Let's Encrypt,https://letsencrypt.org/) or
|
||||
L(Buypass,https://www.buypass.com/). The current implementation
|
||||
supports the C(http-01), C(dns-01) and C(tls-alpn-01) challenges."
|
||||
- "To use this module, it has to be executed twice. Either as two
|
||||
different tasks in the same run or during two runs. Note that the output
|
||||
of the first run needs to be recorded and passed to the second run as the
|
||||
|
@ -54,6 +54,10 @@ seealso:
|
|||
description: Documentation for the Let's Encrypt Certification Authority.
|
||||
Provides useful information for example on rate limits.
|
||||
link: https://letsencrypt.org/docs/
|
||||
- name: Buypass Go SSL
|
||||
description: Documentation for the Buypass Certification Authority.
|
||||
Provides useful information for example on rate limits.
|
||||
link: https://www.buypass.com/ssl/products/acme
|
||||
- name: Automatic Certificate Management Environment (ACME)
|
||||
description: The specification of the ACME protocol (RFC 8555).
|
||||
link: https://tools.ietf.org/html/rfc8555
|
||||
|
@ -639,6 +643,7 @@ class ACMEClient(object):
|
|||
keyauthorization = self.account.get_keyauthorization(token)
|
||||
challenge_response["resource"] = "challenge"
|
||||
challenge_response["keyAuthorization"] = keyauthorization
|
||||
challenge_response["type"] = self.challenge
|
||||
result, info = self.account.send_signed_request(uri, challenge_response)
|
||||
if info['status'] not in [200, 202]:
|
||||
raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))
|
||||
|
|
|
@ -21,7 +21,8 @@ notes:
|
|||
C(account_key_content))."
|
||||
- "Although the defaults are chosen so that the module can be used with
|
||||
the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in
|
||||
principle be used with any CA providing an ACME endpoint."
|
||||
principle be used with any CA providing an ACME endpoint, such as
|
||||
L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
|
||||
requirements:
|
||||
- python >= 2.6
|
||||
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
|
||||
|
@ -63,8 +64,8 @@ options:
|
|||
acme_version:
|
||||
description:
|
||||
- "The ACME version of the endpoint."
|
||||
- "Must be 1 for the classic Let's Encrypt ACME endpoint, or 2 for the
|
||||
new standardized ACME v2 endpoint."
|
||||
- "Must be 1 for the classic Let's Encrypt ACME endpoint and Buypass'
|
||||
current production endpoint, or 2 for standardized ACME v2 endpoints."
|
||||
- "The default value is 1. Note that in Ansible 2.14, this option *will
|
||||
be required* and will no longer have a default."
|
||||
- "Please also note that we will deprecate ACME v1 support eventually."
|
||||
|
@ -82,12 +83,16 @@ options:
|
|||
Note that in Ansible 2.14, this option *will be required* and will no longer
|
||||
have a default."
|
||||
- "For Let's Encrypt, all staging endpoints can be found here:
|
||||
U(https://letsencrypt.org/docs/staging-environment/)"
|
||||
U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
|
||||
endpoints can be found here:
|
||||
U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
|
||||
- "For Let's Encrypt, the production directory URL for ACME v1 is
|
||||
U(https://acme-v01.api.letsencrypt.org/directory), and the production
|
||||
directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory)."
|
||||
- "For Buypass, the production directory URL for ACME v1 is
|
||||
U(https://api.buypass.com/acme/directory)."
|
||||
- "*Warning:* So far, the module has only been tested against Let's Encrypt
|
||||
(staging and production) and against the
|
||||
(staging and production), Buypass (staging and production), and
|
||||
L(Pebble testing server,https://github.com/letsencrypt/Pebble)."
|
||||
type: str
|
||||
validate_certs:
|
||||
|
|
Loading…
Reference in a new issue