mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
a24898b715
commit
7b7709ae75
4 changed files with 59 additions and 8 deletions
|
@ -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…
Reference in a new issue