ACME modules: make compatible to Buypass ACME v1 CA, and fix bug in ACME v1 account update (#61693)

(cherry picked from commit c6dcf78f53)
pull/64281/head
Felix Fontein 5 years ago committed by Toshio Kuratomi
parent d55f7a0d26
commit de4d3dc34d

@ -0,0 +1,3 @@
bugfixes:
- "ACME modules: support Buypass' ACME v1 endpoint"
- "ACME modules: fix bug in ACME v1 account update code"

@ -473,6 +473,9 @@ class ACMEAccount(object):
''' '''
def __init__(self, module): def __init__(self, module):
# Set to true to enable logging of all signed requests
self._debug = False
self.module = module self.module = module
self.version = module.params['acme_version'] self.version = module.params['acme_version']
# account_key path and content are mutually exclusive # account_key path and content are mutually exclusive
@ -540,6 +543,16 @@ class ACMEAccount(object):
else: else:
return _sign_request_openssl(self._openssl_bin, self.module, payload64, protected64, key_data) 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): 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 Sends a JWS signed HTTP POST request to the ACME server and returns
@ -558,9 +571,15 @@ class ACMEAccount(object):
if self.version != 1: if self.version != 1:
protected["url"] = url 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) data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload)
if self.version == 1: 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) data = self.module.jsonify(data)
headers = { headers = {
@ -577,6 +596,7 @@ class ACMEAccount(object):
if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600: if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600:
try: try:
decoded_result = self.module.from_json(content.decode('utf8')) 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) # In case of badNonce error, try again (up to 5 times)
# (https://tools.ietf.org/html/rfc8555#section-6.7) # (https://tools.ietf.org/html/rfc8555#section-6.7)
if (400 <= info['status'] < 600 and if (400 <= info['status'] < 600 and
@ -821,6 +841,8 @@ class ACMEAccount(object):
account_data = dict(account_data) account_data = dict(account_data)
account_data.update(update_request) account_data.update(update_request)
else: else:
if self.version == 1:
update_request['resource'] = 'reg'
account_data, dummy = self.send_signed_request(self.uri, update_request) account_data, dummy = self.send_signed_request(self.uri, update_request)
return True, account_data return True, account_data
@ -939,5 +961,5 @@ def process_links(info, callback):
''' '''
if 'link' in info: if 'link' in info:
link = info['link'] 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) callback(unquote(url), relation)

@ -22,9 +22,9 @@ short_description: Create SSL/TLS certificates with the ACME protocol
description: description:
- "Create and renew SSL/TLS certificates with a CA supporting the - "Create and renew SSL/TLS certificates with a CA supporting the
L(ACME protocol,https://tools.ietf.org/html/rfc8555), L(ACME protocol,https://tools.ietf.org/html/rfc8555),
such as L(Let's Encrypt,https://letsencrypt.org/). The current such as L(Let's Encrypt,https://letsencrypt.org/) or
implementation supports the C(http-01), C(dns-01) and C(tls-alpn-01) L(Buypass,https://www.buypass.com/). The current implementation
challenges." 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 - "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 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 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. description: Documentation for the Let's Encrypt Certification Authority.
Provides useful information for example on rate limits. Provides useful information for example on rate limits.
link: https://letsencrypt.org/docs/ 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) - name: Automatic Certificate Management Environment (ACME)
description: The specification of the ACME protocol (RFC 8555). description: The specification of the ACME protocol (RFC 8555).
link: https://tools.ietf.org/html/rfc8555 link: https://tools.ietf.org/html/rfc8555
@ -638,6 +642,7 @@ class ACMEClient(object):
keyauthorization = self.account.get_keyauthorization(token) keyauthorization = self.account.get_keyauthorization(token)
challenge_response["resource"] = "challenge" challenge_response["resource"] = "challenge"
challenge_response["keyAuthorization"] = keyauthorization challenge_response["keyAuthorization"] = keyauthorization
challenge_response["type"] = self.challenge
result, info = self.account.send_signed_request(uri, challenge_response) result, info = self.account.send_signed_request(uri, challenge_response)
if info['status'] not in [200, 202]: if info['status'] not in [200, 202]:
raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result)) raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result))

@ -21,7 +21,8 @@ notes:
C(account_key_content))." C(account_key_content))."
- "Although the defaults are chosen so that the module can be used with - "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 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: requirements:
- python >= 2.6 - python >= 2.6
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5 - either openssl or L(cryptography,https://cryptography.io/) >= 1.5
@ -63,8 +64,8 @@ options:
acme_version: acme_version:
description: description:
- "The ACME version of the endpoint." - "The ACME version of the endpoint."
- "Must be 1 for the classic Let's Encrypt ACME endpoint, or 2 for the - "Must be 1 for the classic Let's Encrypt ACME endpoint and Buypass'
new standardized ACME v2 endpoint." current production endpoint, or 2 for standardized ACME v2 endpoints."
type: int type: int
default: 1 default: 1
choices: [ 1, 2 ] choices: [ 1, 2 ]
@ -77,12 +78,16 @@ options:
server (for the ACME v1 protocol). This will create technically correct, server (for the ACME v1 protocol). This will create technically correct,
but untrusted certificates." but untrusted certificates."
- "For Let's Encrypt, all staging endpoints can be found here: - "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 - "For Let's Encrypt, the production directory URL for ACME v1 is
U(https://acme-v01.api.letsencrypt.org/directory), and the production 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)." 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)."
- "I(Warning): So far, the module has only been tested against Let's Encrypt - "I(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)." L(Pebble testing server,https://github.com/letsencrypt/Pebble)."
type: str type: str
default: https://acme-staging.api.letsencrypt.org/directory default: https://acme-staging.api.letsencrypt.org/directory

Loading…
Cancel
Save