From c6dcf78f536fc130a3a4ce1f736aaaaa3e2486b6 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 21 Sep 2019 16:53:15 +0200 Subject: [PATCH] ACME modules: make compatible to Buypass ACME v1 CA, and fix bug in ACME v1 account update (#61693) --- .../fragments/61693-acme-buypass-acme-v1.yml | 3 +++ lib/ansible/module_utils/acme.py | 26 +++++++++++++++++-- .../modules/crypto/acme/acme_certificate.py | 11 +++++--- lib/ansible/plugins/doc_fragments/acme.py | 15 +++++++---- 4 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/61693-acme-buypass-acme-v1.yml diff --git a/changelogs/fragments/61693-acme-buypass-acme-v1.yml b/changelogs/fragments/61693-acme-buypass-acme-v1.yml new file mode 100644 index 00000000000..285d9f4a271 --- /dev/null +++ b/changelogs/fragments/61693-acme-buypass-acme-v1.yml @@ -0,0 +1,3 @@ +bugfixes: +- "ACME modules: support Buypass' ACME v1 endpoint" +- "ACME modules: fix bug in ACME v1 account update code" diff --git a/lib/ansible/module_utils/acme.py b/lib/ansible/module_utils/acme.py index cc7443a6d96..b878678c136 100644 --- a/lib/ansible/module_utils/acme.py +++ b/lib/ansible/module_utils/acme.py @@ -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) diff --git a/lib/ansible/modules/crypto/acme/acme_certificate.py b/lib/ansible/modules/crypto/acme/acme_certificate.py index d85f413f8b7..5919777616f 100644 --- a/lib/ansible/modules/crypto/acme/acme_certificate.py +++ b/lib/ansible/modules/crypto/acme/acme_certificate.py @@ -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)) diff --git a/lib/ansible/plugins/doc_fragments/acme.py b/lib/ansible/plugins/doc_fragments/acme.py index e4b16ae45c0..319d182e61b 100644 --- a/lib/ansible/plugins/doc_fragments/acme.py +++ b/lib/ansible/plugins/doc_fragments/acme.py @@ -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: