acme_certificate: add select_chain option (#60710)

* Add select_alternate_chain option.

* Fix docs.

* Allow to match via subject key identifier and authority key identifier.

* Simplify test.

* Add comments.

* Add tests.

* Fix bugs.

* Also consider main chain when searching for alternatives.

* Bump version_added.

* Rename select_alternate_chain -> select_chain.
This commit is contained in:
Felix Fontein 2019-10-29 08:09:15 +01:00 committed by GitHub
parent 35a412fab7
commit 16d4d2dba9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 422 additions and 27 deletions

View file

@ -28,6 +28,7 @@ import sys
import tempfile import tempfile
import traceback import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils._text import to_native, to_text, to_bytes from ansible.module_utils._text import to_native, to_text, to_bytes
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
from ansible.module_utils.compat import ipaddress as compat_ipaddress from ansible.module_utils.compat import ipaddress as compat_ipaddress
@ -945,15 +946,17 @@ def set_crypto_backend(module):
try: try:
cryptography.__version__ cryptography.__version__
except Exception as dummy: except Exception as dummy:
module.fail_json(msg='Cannot find cryptography module!') module.fail_json(msg=missing_required_lib('cryptography'))
HAS_CURRENT_CRYPTOGRAPHY = True HAS_CURRENT_CRYPTOGRAPHY = True
else: else:
module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend)) module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
# Inform about choices # Inform about choices
if HAS_CURRENT_CRYPTOGRAPHY: if HAS_CURRENT_CRYPTOGRAPHY:
module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION)) module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
return 'cryptography'
else: else:
module.debug('Using OpenSSL binary backend') module.debug('Using OpenSSL binary backend')
return 'openssl'
def process_links(info, callback): def process_links(info, callback):
@ -985,7 +988,7 @@ def handle_standard_module_arguments(module, needs_acme_v2=False):
''' '''
Do standard module setup, argument handling and warning emitting. Do standard module setup, argument handling and warning emitting.
''' '''
set_crypto_backend(module) backend = set_crypto_backend(module)
if not module.params['validate_certs']: if not module.params['validate_certs']:
module.warn( module.warn(
@ -1008,3 +1011,5 @@ def handle_standard_module_arguments(module, needs_acme_v2=False):
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates. # AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
locale.setlocale(locale.LC_ALL, 'C') locale.setlocale(locale.LC_ALL, 'C')
return backend

View file

@ -203,13 +203,67 @@ options:
version_added: 2.6 version_added: 2.6
retrieve_all_alternates: retrieve_all_alternates:
description: description:
- "When set to C(yes), will retrieve all alternate chains offered by the ACME CA. - "When set to C(yes), will retrieve all alternate trust chains offered by the ACME CA.
These will not be written to disk, but will be returned together with the main These will not be written to disk, but will be returned together with the main
chain as C(all_chains). See the documentation for the C(all_chains) return chain as C(all_chains). See the documentation for the C(all_chains) return
value for details." value for details."
type: bool type: bool
default: no default: no
version_added: "2.9" version_added: "2.9"
select_chain:
description:
- "Allows to specify criteria by which an (alternate) trust chain can be selected."
- "The list of criteria will be processed one by one until a chain is found
matching a criterium. If such a chain is found, it will be used by the
module instead of the default chain."
- "If a criterium matches multiple chains, the first one matching will be
returned. The order is determined by the ordering of the C(Link) headers
returned by the ACME server and might not be deterministic."
- "Every criterium can consist of multiple different conditions, like I(issuer)
and I(subject). For the criterium to match a chain, all conditions must apply
to the same certificate in the chain."
- "This option can only be used with the C(cryptography) backend."
type: list
version_added: "2.10"
suboptions:
test_certificates:
description:
- "Determines which certificates in the chain will be tested."
- "I(all) tests all certificates in the chain (excluding the leaf, which is
identical in all chains)."
- "I(last) only tests the last certificate in the chain, i.e. the one furthest
away from the leaf. Its issuer is the root certificate of this chain."
type: str
default: all
choices: [last, all]
issuer:
description:
- "Allows to specify parts of the issuer of a certificate in the chain must
have to be selected."
- "If I(issuer) is empty, any certificate will match."
- 'An example value would be C({"commonName": "My Preferred CA Root"}).'
type: dict
subject:
description:
- "Allows to specify parts of the subject of a certificate in the chain must
have to be selected."
- "If I(subject) is empty, any certificate will match."
- 'An example value would be C({"CN": "My Preferred CA Intermediate"})'
type: dict
subject_key_identifier:
description:
- "Checks for the SubjectKeyIdentifier extension. This is an identifier based
on the private key of the intermediate certificate."
- "The identifier must be of the form
C(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)."
type: str
authority_key_identifier:
description:
- "Checks for the AuthorityKeyIdentifier extension. This is an identifier based
on the private key of the issuer of the intermediate certificate."
- "The identifier must be of the form
C(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
type: str
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@ -312,6 +366,33 @@ EXAMPLES = r'''
remaining_days: 60 remaining_days: 60
data: "{{ sample_com_challenge }}" data: "{{ sample_com_challenge }}"
when: sample_com_challenge is changed when: sample_com_challenge is changed
# Alternative second step:
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
acme_certificate:
account_key_src: /etc/pki/cert/private/account.key
account_email: myself@sample.com
src: /etc/pki/cert/csr/sample.com.csr
cert: /etc/httpd/ssl/sample.com.crt
fullchain: /etc/httpd/ssl/sample.com-fullchain.crt
chain: /etc/httpd/ssl/sample.com-intermediate.crt
challenge: tls-alpn-01
remaining_days: 60
data: "{{ sample_com_challenge }}"
# We use Let's Encrypt's ACME v2 endpoint
acme_directory: https://acme-v02.api.letsencrypt.org/directory
acme_version: 2
# The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided
# as an alternative, it will be selected. These are the roots cross-signed by IdenTrust.
# As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when
# switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed
# root. This chain is more compatible with older TLS clients.
select_chain:
- test_certificates: last
issuer:
CN: DST Root CA X3
O: Digital Signature Trust Co.
when: sample_com_challenge is changed
''' '''
RETURN = ''' RETURN = '''
@ -432,16 +513,29 @@ from ansible.module_utils.acme import (
) )
import base64 import base64
import binascii
import hashlib import hashlib
import os import os
import re import re
import textwrap import textwrap
import time import time
import traceback
from datetime import datetime from datetime import datetime
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.compat import ipaddress as compat_ipaddress from ansible.module_utils.compat import ipaddress as compat_ipaddress
from ansible.module_utils import crypto as crypto_utils
try:
import cryptography
import cryptography.hazmat.backends
import cryptography.x509
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
CRYPTOGRAPHY_FOUND = False
else:
CRYPTOGRAPHY_FOUND = True
def get_cert_days(module, cert_file): def get_cert_days(module, cert_file):
@ -907,6 +1001,60 @@ class ACMEClient(object):
identifier_type, identifier = type_identifier.split(':', 1) identifier_type, identifier = type_identifier.split(':', 1)
self._validate_challenges(identifier_type, identifier, auth) self._validate_challenges(identifier_type, identifier, auth)
def _chain_matches(self, chain, criterium):
'''
Check whether an alternate chain matches the specified criterium.
'''
if criterium['test_certificates'] == 'last':
chain = chain[-1:]
for cert in chain:
try:
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend())
matches = True
if criterium['subject']:
for k, v in crypto_utils.parse_name_field(criterium['subject']):
oid = crypto_utils.cryptography_name_to_oid(k)
value = to_native(v)
found = False
for attribute in x509.subject:
if attribute.oid == oid and value == to_native(attribute.value):
found = True
break
if not found:
matches = False
break
if criterium['issuer']:
for k, v in crypto_utils.parse_name_field(criterium['issuer']):
oid = crypto_utils.cryptography_name_to_oid(k)
value = to_native(v)
found = False
for attribute in x509.issuer:
if attribute.oid == oid and value == to_native(attribute.value):
found = True
break
if not found:
matches = False
break
if criterium['subject_key_identifier']:
try:
ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
if criterium['subject_key_identifier'] != ext.value.digest:
matches = False
except cryptography.x509.ExtensionNotFound:
matches = False
if criterium['authority_key_identifier']:
try:
ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
if criterium['authority_key_identifier'] != ext.value.key_identifier:
matches = False
except cryptography.x509.ExtensionNotFound:
matches = False
if matches:
return True
except Exception as e:
self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e))
return False
def get_certificate(self): def get_certificate(self):
''' '''
Request a new certificate and write it to the destination file. Request a new certificate and write it to the destination file.
@ -927,7 +1075,8 @@ class ACMEClient(object):
else: else:
cert_uri = self._finalize_cert() cert_uri = self._finalize_cert()
cert = self._download_cert(cert_uri) cert = self._download_cert(cert_uri)
if self.module.params['retrieve_all_alternates']: if self.module.params['retrieve_all_alternates'] or self.module.params['select_chain']:
# Retrieve alternate chains
alternate_chains = [] alternate_chains = []
for alternate in cert['alternates']: for alternate in cert['alternates']:
try: try:
@ -936,18 +1085,46 @@ class ACMEClient(object):
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e))
continue continue
alternate_chains.append(alt_cert) alternate_chains.append(alt_cert)
self.all_chains = []
def _append_all_chains(cert_data): # Prepare return value for all alternate chains
self.all_chains.append(dict( if self.module.params['retrieve_all_alternates']:
cert=cert_data['cert'].encode('utf8'), self.all_chains = []
chain=("\n".join(cert_data.get('chain', []))).encode('utf8'),
full_chain=(cert_data['cert'] + "\n".join(cert_data.get('chain', []))).encode('utf8'),
))
_append_all_chains(cert) def _append_all_chains(cert_data):
for alt_chain in alternate_chains: self.all_chains.append(dict(
_append_all_chains(alt_chain) cert=cert_data['cert'].encode('utf8'),
chain=("\n".join(cert_data.get('chain', []))).encode('utf8'),
full_chain=(cert_data['cert'] + "\n".join(cert_data.get('chain', []))).encode('utf8'),
))
_append_all_chains(cert)
for alt_chain in alternate_chains:
_append_all_chains(alt_chain)
# Try to select alternate chain depending on criteria
if self.module.params['select_chain']:
matching_chain = None
all_chains = [cert] + alternate_chains
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
for v in ('subject_key_identifier', 'authority_key_identifier'):
if criterium[v]:
try:
criterium[v] = binascii.unhexlify(criterium[v].replace(':', ''))
except Exception:
self.module.warn('Criterium {0} in select_chain has invalid {1} value. '
'Ignoring criterium.'.format(criterium_idx, v))
continue
for alt_chain in all_chains:
if self._chain_matches(alt_chain.get('chain', []), criterium):
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx))
matching_chain = alt_chain
break
if matching_chain:
break
if matching_chain:
cert.update(matching_chain)
else:
self.module.debug('Found no matching alternative chain')
if cert['cert'] is not None: if cert['cert'] is not None:
pem_cert = cert['cert'] pem_cert = cert['cert']
@ -1009,6 +1186,13 @@ def main():
deactivate_authzs=dict(type='bool', default=False), deactivate_authzs=dict(type='bool', default=False),
force=dict(type='bool', default=False), force=dict(type='bool', default=False),
retrieve_all_alternates=dict(type='bool', default=False), retrieve_all_alternates=dict(type='bool', default=False),
select_chain=dict(type='list', elements='dict', options=dict(
test_certificates=dict(type='str', default='all', choices=['last', 'all']),
issuer=dict(type='dict'),
subject=dict(type='dict'),
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
)) ))
module = AnsibleModule( module = AnsibleModule(
argument_spec=argument_spec, argument_spec=argument_spec,
@ -1021,7 +1205,12 @@ def main():
), ),
supports_check_mode=True, supports_check_mode=True,
) )
handle_standard_module_arguments(module) backend = handle_standard_module_arguments(module)
if module.params['select_chain']:
if backend != 'cryptography':
module.fail_json(msg="The 'select_chain' can only be used with the 'cryptography' backend.")
elif not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography'))
try: try:
if module.params.get('dest'): if module.params.get('dest'):

View file

@ -58,9 +58,14 @@
terms_agreed: yes terms_agreed: yes
account_email: "example@example.org" account_email: "example@example.org"
retrieve_all_alternates: yes retrieve_all_alternates: yes
acme_expected_root_number: 1
select_chain:
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
- name: Store obtain results for cert 1 - name: Store obtain results for cert 1
set_fact: set_fact:
cert_1_obtain_results: "{{ certificate_obtain_result }}" cert_1_obtain_results: "{{ certificate_obtain_result }}"
cert_1_alternate: "{{ 1 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 2 - name: Obtain cert 2
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -77,9 +82,21 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
acme_expected_root_number: 0
retrieve_all_alternates: yes
select_chain:
# All intermediates have the same subject, so always the first
# chain will be found, and we need a second condition to make sure
# that the first condition actually works. (The second condition
# has been tested above.)
- test_certificates: all
subject: "{{ acme_intermediates[0].subject }}"
- test_certificates: all
issuer: "{{ acme_roots[2].subject }}"
- name: Store obtain results for cert 2 - name: Store obtain results for cert 2
set_fact: set_fact:
cert_2_obtain_results: "{{ certificate_obtain_result }}" cert_2_obtain_results: "{{ certificate_obtain_result }}"
cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 3 - name: Obtain cert 3
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -96,6 +113,15 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
acme_expected_root_number: 0
retrieve_all_alternates: yes
select_chain:
- test_certificates: last
subject: "{{ acme_roots[1].subject }}"
- name: Store obtain results for cert 3
set_fact:
cert_3_obtain_results: "{{ certificate_obtain_result }}"
cert_3_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 4 - name: Obtain cert 4
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -113,6 +139,16 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
acme_expected_root_number: 2
select_chain:
- test_certificates: last
issuer: "{{ acme_roots[2].subject }}"
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
- name: Store obtain results for cert 4
set_fact:
cert_4_obtain_results: "{{ certificate_obtain_result }}"
cert_4_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 5 - name: Obtain cert 5
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -129,6 +165,10 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
- name: Store obtain results for cert 5a
set_fact:
cert_5a_obtain_results: "{{ certificate_obtain_result }}"
cert_5_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 5 (should not, since already there and valid for more than 10 days) - name: Obtain cert 5 (should not, since already there and valid for more than 10 days)
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -145,7 +185,8 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
- set_fact: - name: Store obtain results for cert 5b
set_fact:
cert_5_recreate_1: "{{ challenge_data is changed }}" cert_5_recreate_1: "{{ challenge_data is changed }}"
- name: Obtain cert 5 (should again by less days) - name: Obtain cert 5 (should again by less days)
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
@ -163,8 +204,10 @@
remaining_days: 1000 remaining_days: 1000
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
- set_fact: - name: Store obtain results for cert 5c
set_fact:
cert_5_recreate_2: "{{ challenge_data is changed }}" cert_5_recreate_2: "{{ challenge_data is changed }}"
cert_5c_obtain_results: "{{ certificate_obtain_result }}"
- name: Obtain cert 5 (should again by force) - name: Obtain cert 5 (should again by force)
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -181,8 +224,10 @@
remaining_days: 10 remaining_days: 10
terms_agreed: no terms_agreed: no
account_email: "" account_email: ""
- set_fact: - name: Store obtain results for cert 5d
set_fact:
cert_5_recreate_3: "{{ challenge_data is changed }}" cert_5_recreate_3: "{{ challenge_data is changed }}"
cert_5d_obtain_results: "{{ certificate_obtain_result }}"
- name: Obtain cert 6 - name: Obtain cert 6
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -200,6 +245,20 @@
remaining_days: 10 remaining_days: 10
terms_agreed: yes terms_agreed: yes
account_email: "example@example.org" account_email: "example@example.org"
acme_expected_root_number: 0
select_chain:
# All intermediates have the same subject key identifier, so always
# the first chain will be found, and we need a second condition to
# make sure that the first condition actually works. (The second
# condition has been tested above.)
- test_certificates: last
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
- test_certificates: last
issuer: "{{ acme_roots[1].subject }}"
- name: Store obtain results for cert 6
set_fact:
cert_6_obtain_results: "{{ certificate_obtain_result }}"
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 7 - name: Obtain cert 7
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -219,6 +278,14 @@
remaining_days: 10 remaining_days: 10
terms_agreed: yes terms_agreed: yes
account_email: "example@example.org" account_email: "example@example.org"
acme_expected_root_number: 2
select_chain:
- test_certificates: last
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
- name: Store obtain results for cert 7
set_fact:
cert_7_obtain_results: "{{ certificate_obtain_result }}"
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
- name: Obtain cert 8 - name: Obtain cert 8
include_tasks: obtain-cert.yml include_tasks: obtain-cert.yml
vars: vars:
@ -240,6 +307,10 @@
remaining_days: 10 remaining_days: 10
terms_agreed: yes terms_agreed: yes
account_email: "example@example.org" account_email: "example@example.org"
- name: Store obtain results for cert 8
set_fact:
cert_8_obtain_results: "{{ certificate_obtain_result }}"
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
## 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
@ -299,6 +370,39 @@
- name: Dumping cert 8 - name: Dumping cert 8
command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -text
register: cert_8_text register: cert_8_text
# Dump certificate info
- name: Dumping cert 1
openssl_certificate_info:
path: "{{ output_dir }}/cert-1.pem"
register: cert_1_info
- name: Dumping cert 2
openssl_certificate_info:
path: "{{ output_dir }}/cert-2.pem"
register: cert_2_info
- name: Dumping cert 3
openssl_certificate_info:
path: "{{ output_dir }}/cert-3.pem"
register: cert_3_info
- name: Dumping cert 4
openssl_certificate_info:
path: "{{ output_dir }}/cert-4.pem"
register: cert_4_info
- name: Dumping cert 5
openssl_certificate_info:
path: "{{ output_dir }}/cert-5.pem"
register: cert_5_info
- name: Dumping cert 6
openssl_certificate_info:
path: "{{ output_dir }}/cert-6.pem"
register: cert_6_info
- name: Dumping cert 7
openssl_certificate_info:
path: "{{ output_dir }}/cert-7.pem"
register: cert_7_info
- name: Dumping cert 8
openssl_certificate_info:
path: "{{ output_dir }}/cert-8.pem"
register: cert_8_info
## GET ACCOUNT ORDERS ######################################################################### ## GET ACCOUNT ORDERS #########################################################################
- name: Don't retrieve orders - name: Don't retrieve orders
acme_account_info: acme_account_info:

View file

@ -1,4 +1,75 @@
--- ---
- block:
- name: Obtain root and intermediate certificates
get_url:
url: "http://{{ acme_host }}:5000/{{ item.0 }}-certificate-for-ca/{{ item.1 }}"
dest: "{{ output_dir }}/acme-{{ item.0 }}-{{ item.1 }}.pem"
loop: "{{ query('nested', types, root_numbers) }}"
- name: Analyze root certificates
openssl_certificate_info:
path: "{{ output_dir }}/acme-root-{{ item }}.pem"
loop: "{{ root_numbers }}"
register: acme_roots
- name: Analyze intermediate certificates
openssl_certificate_info:
path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem"
loop: "{{ root_numbers }}"
register: acme_intermediates
- set_fact:
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
y__: "{{ lookup('file', output_dir ~ '/acme-root-' ~ item.item ~ '.pem', rstrip=False) }}"
loop: "{{ acme_roots.results }}"
register: acme_roots_tmp
- set_fact:
x__: "{{ item | dict2items | selectattr('key', 'in', interesting_keys) | list | items2dict }}"
y__: "{{ lookup('file', output_dir ~ '/acme-intermediate-' ~ item.item ~ '.pem', rstrip=False) }}"
loop: "{{ acme_intermediates.results }}"
register: acme_intermediates_tmp
- set_fact:
acme_roots: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.x__') | list }}"
acme_root_certs: "{{ acme_roots_tmp.results | map(attribute='ansible_facts.y__') | list }}"
acme_intermediates: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.x__') | list }}"
acme_intermediate_certs: "{{ acme_intermediates_tmp.results | map(attribute='ansible_facts.y__') | list }}"
vars:
types:
- root
- intermediate
root_numbers:
# The number 3 comes from here: https://github.com/ansible/acme-test-container/blob/master/run.sh#L12
- 0
- 1
- 2
- 3
interesting_keys:
- authority_key_identifier
- subject_key_identifier
- issuer
- subject
#- serial_number
#- public_key_fingerprints
- name: ACME root certificate info
debug:
var: acme_roots
#- name: ACME root certificates as PEM
# debug:
# var: acme_root_certs
- name: ACME intermediate certificate info
debug:
var: acme_intermediates
#- name: ACME intermediate certificates as PEM
# debug:
# var: acme_intermediate_certs
- block: - block:
- name: Running tests with OpenSSL backend - name: Running tests with OpenSSL backend
include_tasks: impl.yml include_tasks: impl.yml

View file

@ -11,10 +11,13 @@
assert: assert:
that: that:
- "'all_chains' in cert_1_obtain_results" - "'all_chains' in cert_1_obtain_results"
- "'chain' in cert_1_obtain_results.all_chains[0]" - "cert_1_obtain_results.all_chains | length > 1"
- "'full_chain' in cert_1_obtain_results.all_chains[0]" - "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
- "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[0].chain" - "'chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
- "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[0].full_chain" - "'full_chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]"
- "lookup('file', output_dir ~ '/cert-1.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].cert"
- "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].chain"
- "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[cert_1_alternate | int].full_chain"
- name: Check that certificate 2 is valid - name: Check that certificate 2 is valid
assert: assert:
@ -25,10 +28,17 @@
that: that:
- "'DNS:*.example.com' in cert_2_text.stdout" - "'DNS:*.example.com' in cert_2_text.stdout"
- "'DNS:example.com' in cert_2_text.stdout" - "'DNS:example.com' in cert_2_text.stdout"
- name: Check that certificate 2 retrieval did not get all chains - name: Check that certificate 1 retrieval got all chains
assert: assert:
that: that:
- "'all_chains' not in cert_2_obtain_results" - "'all_chains' in cert_2_obtain_results"
- "cert_2_obtain_results.all_chains | length > 1"
- "'cert' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
- "'chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
- "'full_chain' in cert_2_obtain_results.all_chains[cert_2_alternate | int]"
- "lookup('file', output_dir ~ '/cert-2.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].cert"
- "lookup('file', output_dir ~ '/cert-2-chain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].chain"
- "lookup('file', output_dir ~ '/cert-2-fullchain.pem', rstrip=False) == cert_2_obtain_results.all_chains[cert_2_alternate | int].full_chain"
- name: Check that certificate 3 is valid - name: Check that certificate 3 is valid
assert: assert:
@ -40,6 +50,17 @@
- "'DNS:*.example.com' in cert_3_text.stdout" - "'DNS:*.example.com' in cert_3_text.stdout"
- "'DNS:example.org' in cert_3_text.stdout" - "'DNS:example.org' in cert_3_text.stdout"
- "'DNS:t1.example.com' in cert_3_text.stdout" - "'DNS:t1.example.com' in cert_3_text.stdout"
- name: Check that certificate 1 retrieval got all chains
assert:
that:
- "'all_chains' in cert_3_obtain_results"
- "cert_3_obtain_results.all_chains | length > 1"
- "'cert' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
- "'chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
- "'full_chain' in cert_3_obtain_results.all_chains[cert_3_alternate | int]"
- "lookup('file', output_dir ~ '/cert-3.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].cert"
- "lookup('file', output_dir ~ '/cert-3-chain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].chain"
- "lookup('file', output_dir ~ '/cert-3-fullchain.pem', rstrip=False) == cert_3_obtain_results.all_chains[cert_3_alternate | int].full_chain"
- name: Check that certificate 4 is valid - name: Check that certificate 4 is valid
assert: assert:
@ -53,6 +74,10 @@
- "'DNS:test.t2.example.com' in cert_4_text.stdout" - "'DNS:test.t2.example.com' in cert_4_text.stdout"
- "'DNS:example.org' in cert_4_text.stdout" - "'DNS:example.org' in cert_4_text.stdout"
- "'DNS:test.example.org' in cert_4_text.stdout" - "'DNS:test.example.org' in cert_4_text.stdout"
- name: Check that certificate 4 retrieval did not get all chains
assert:
that:
- "'all_chains' not in cert_4_obtain_results"
- name: Check that certificate 5 is valid - name: Check that certificate 5 is valid
assert: assert:

View file

@ -112,6 +112,7 @@
account_email: "{{ account_email }}" account_email: "{{ account_email }}"
data: "{{ challenge_data }}" data: "{{ challenge_data }}"
retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}" retrieve_all_alternates: "{{ retrieve_all_alternates | default(omit) }}"
select_chain: "{{ select_chain | default(omit) if select_crypto_backend == 'cryptography' else omit }}"
register: certificate_obtain_result register: certificate_obtain_result
when: challenge_data is changed when: challenge_data is changed
- name: ({{ certgen_title }}) Deleting HTTP challenges - name: ({{ certgen_title }}) Deleting HTTP challenges
@ -134,6 +135,6 @@
when: "challenge_data is changed and challenge == 'tls-alpn-01'" when: "challenge_data is changed and challenge == 'tls-alpn-01'"
- name: ({{ certgen_title }}) Get root certificate - name: ({{ certgen_title }}) Get root certificate
get_url: get_url:
url: "http://{{ acme_host }}:5000/root-certificate-for-ca/0" url: "http://{{ acme_host }}:5000/root-certificate-for-ca/{{ acme_expected_root_number | default(0) if select_crypto_backend == 'cryptography' else 0 }}"
dest: "{{ output_dir }}/{{ certificate_name }}-root.pem" dest: "{{ output_dir }}/{{ certificate_name }}-root.pem"
############################################################################################### ###############################################################################################