ACME: support for TLS-ALPN-01 (#42158)

* Added support for TLS-ALPN-01 verification.

* Unrelated commit to re-trigger tests.

* Added test for TLS-ALPN-01.

* Try to remove to_bytes in the hope that binary data survives in Python 2.

* Using Base64 encoding for TLS-ALPN-01 value.
pull/43758/head
Felix Fontein 6 years ago committed by René Moser
parent a24898b715
commit 7b7709ae75

@ -23,7 +23,8 @@ description:
- "Create and renew SSL certificates with a CA supporting the - "Create and renew SSL certificates with a CA supporting the
L(ACME protocol,https://tools.ietf.org/html/draft-ietf-acme-acme-12), L(ACME protocol,https://tools.ietf.org/html/draft-ietf-acme-acme-12),
such as L(Let's Encrypt,https://letsencrypt.org/). The current such as L(Let's Encrypt,https://letsencrypt.org/). The current
implementation supports the C(http-01) and C(dns-01) challenges." 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 - "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
@ -31,10 +32,12 @@ description:
- "Between these two tasks you have to fulfill the required steps for the - "Between these two tasks you have to fulfill the required steps for the
chosen challenge by whatever means necessary. For C(http-01) that means chosen challenge by whatever means necessary. For C(http-01) that means
creating the necessary challenge file on the destination webserver. For creating the necessary challenge file on the destination webserver. For
C(dns-01) the necessary dns record has to be created. C(dns-01) the necessary dns record has to be created. For C(tls-alpn-01)
the necessary certificate has to be created and served.
It is I(not) the responsibility of this module to perform these steps." It is I(not) the responsibility of this module to perform these steps."
- "For details on how to fulfill these challenges, you might have to read through - "For details on how to fulfill these challenges, you might have to read through
L(the specification,https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-8). L(the main ACME specification,https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-8)
and the L(TLS-ALPN-01 specification,U(https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3).
Also, consider the examples provided for this module." Also, consider the examples provided for this module."
- "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 Let's Encrypt CA, the module can be used with any service using the ACME the Let's Encrypt CA, the module can be used with any service using the ACME
@ -84,7 +87,7 @@ options:
version_added: "2.6" version_added: "2.6"
challenge: challenge:
description: The challenge to be performed. description: The challenge to be performed.
choices: [ 'http-01', 'dns-01'] choices: [ 'http-01', 'dns-01', 'tls-alpn-01' ]
default: 'http-01' default: 'http-01'
csr: csr:
description: description:
@ -137,6 +140,8 @@ options:
If C(cert_days < remaining_days), then it will be renewed. If C(cert_days < remaining_days), then it will be renewed.
If the certificate is not renewed, module return values will not If the certificate is not renewed, module return values will not
include C(challenge_data)." include C(challenge_data)."
- "To make sure that the certificate is renewed in any case, you can
use the C(force) option."
default: 10 default: 10
deactivate_authzs: deactivate_authzs:
description: description:
@ -152,7 +157,7 @@ options:
force: force:
description: description:
- Enforces the execution of the challenge and validation, even if an - Enforces the execution of the challenge and validation, even if an
existing certificate is still valid. existing certificate is still valid for more than C(remaining_days).
- This is especially helpful when having an updated CSR e.g. with - This is especially helpful when having an updated CSR e.g. with
additional domains for which a new certificate is desired. additional domains for which a new certificate is desired.
type: bool type: bool
@ -273,7 +278,15 @@ challenge_data:
type: string type: string
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_value: resource_value:
description: the value the resource has to produce for the validation description:
- The value the resource has to produce for the validation.
- For C(http-01) and C(dns-01) challenges, the value can be used as-is.
- "For C(tls-alpn-01) challenges, note that this return value contains a
Base64 encoded version of the correct binary blob which has to be put
into the acmeValidation x509 extension; see
U(https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3)
for details. To do this, you might need the C(b64decode) Jinja filter
to extract the binary blob from this return value."
returned: changed returned: changed
type: string type: string
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
@ -482,6 +495,11 @@ class ACMEClient(object):
value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest()) value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest())
record = (resource + domain[1:]) if domain.startswith('*.') else (resource + '.' + domain) record = (resource + domain[1:]) if domain.startswith('*.') else (resource + '.' + domain)
data[type] = {'resource': resource, 'resource_value': value, 'record': record} data[type] = {'resource': resource, 'resource_value': value, 'record': record}
elif type == 'tls-alpn-01':
# https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3
resource = domain
value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest())
data[type] = {'resource': resource, 'resource_value': value}
else: else:
continue continue
@ -856,7 +874,7 @@ def main():
account_email=dict(required=False, default=None, type='str'), account_email=dict(required=False, default=None, type='str'),
agreement=dict(required=False, type='str'), agreement=dict(required=False, type='str'),
terms_agreed=dict(required=False, default=False, type='bool'), terms_agreed=dict(required=False, default=False, type='bool'),
challenge=dict(required=False, default='http-01', choices=['http-01', 'dns-01'], type='str'), challenge=dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01'], type='str'),
csr=dict(required=True, aliases=['src'], type='path'), csr=dict(required=True, aliases=['src'], type='path'),
data=dict(required=False, default=None, type='dict'), data=dict(required=False, default=None, type='dict'),
dest=dict(aliases=['cert'], type='path'), dest=dict(aliases=['cert'], type='path'),

@ -174,6 +174,23 @@
account_email: "" account_email: ""
- set_fact: - set_fact:
cert_5_recreate_3: "{{ challenge_data is changed }}" cert_5_recreate_3: "{{ challenge_data is changed }}"
- name: Obtain cert 6
include_tasks: obtain-cert.yml
vars:
certgen_title: Certificate 6
certificate_name: cert-6
key_type: rsa
rsa_bits: 2048
subject_alt_name: "DNS:example.org"
subject_alt_name_critical: no
account_key: account-ec256
challenge: tls-alpn-01
modify_account: yes
deactivate_authzs: no
force: no
remaining_days: 10
terms_agreed: yes
account_email: "example@example.org"
## DISSECT CERTIFICATES ####################################################################### ## DISSECT CERTIFICATES #######################################################################
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate. # Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
- name: Verifying cert 1 - name: Verifying cert 1
@ -196,6 +213,10 @@
command: openssl verify -CAfile "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem" command: openssl verify -CAfile "{{ output_dir }}/cert-5-chain.pem" "{{ output_dir }}/cert-5.pem"
ignore_errors: yes ignore_errors: yes
register: cert_5_valid register: cert_5_valid
- name: Verifying cert 6
command: openssl verify -CAfile "{{ output_dir }}/cert-6-chain.pem" "{{ output_dir }}/cert-6.pem"
ignore_errors: yes
register: cert_6_valid
# Dump certificate info # Dump certificate info
- name: Dumping cert 1 - name: Dumping cert 1
command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text command: openssl x509 -in "{{ output_dir }}/cert-1.pem" -noout -text
@ -212,6 +233,9 @@
- name: Dumping cert 5 - name: Dumping cert 5
command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text command: openssl x509 -in "{{ output_dir }}/cert-5.pem" -noout -text
register: cert_5_text register: cert_5_text
- name: Dumping cert 6
command: openssl x509 -in "{{ output_dir }}/cert-6.pem" -noout -text
register: cert_6_text
- import_tasks: ../tests/validate.yml - import_tasks: ../tests/validate.yml

@ -62,3 +62,12 @@
assert: assert:
that: that:
- cert_5_recreate_3 == True - cert_5_recreate_3 == True
- name: Check that certificate 6 is valid
assert:
that:
- cert_6_valid is not failed
- name: Check that certificate 6 contains correct SANs
assert:
that:
- "'DNS:example.org' in cert_6_text.stdout"

@ -88,7 +88,7 @@
url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}" url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}"
method: PUT method: PUT
body_format: raw body_format: raw
body: "{{ item.value['tls-alpn-01'].resource_value | b64encode }}" body: "{{ item.value['tls-alpn-01'].resource_value }}"
headers: headers:
content-type: "application/octet-stream" content-type: "application/octet-stream"
with_dict: "{{ challenge_data.challenge_data }}" with_dict: "{{ challenge_data.challenge_data }}"

Loading…
Cancel
Save