From 16d4d2dba9d62103a198647894f7f82020dca27c Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 29 Oct 2019 08:09:15 +0100 Subject: [PATCH] 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. --- lib/ansible/module_utils/acme.py | 9 +- .../modules/crypto/acme/acme_certificate.py | 221 ++++++++++++++++-- .../targets/acme_certificate/tasks/impl.yml | 110 ++++++++- .../targets/acme_certificate/tasks/main.yml | 71 ++++++ .../acme_certificate/tests/validate.yml | 37 ++- .../targets/setup_acme/tasks/obtain-cert.yml | 3 +- 6 files changed, 423 insertions(+), 28 deletions(-) diff --git a/lib/ansible/module_utils/acme.py b/lib/ansible/module_utils/acme.py index b878678c136..1ee9057b1d0 100644 --- a/lib/ansible/module_utils/acme.py +++ b/lib/ansible/module_utils/acme.py @@ -28,6 +28,7 @@ import sys import tempfile 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.urls import fetch_url from ansible.module_utils.compat import ipaddress as compat_ipaddress @@ -945,15 +946,17 @@ def set_crypto_backend(module): try: cryptography.__version__ except Exception as dummy: - module.fail_json(msg='Cannot find cryptography module!') + module.fail_json(msg=missing_required_lib('cryptography')) HAS_CURRENT_CRYPTOGRAPHY = True else: module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend)) # Inform about choices if HAS_CURRENT_CRYPTOGRAPHY: module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION)) + return 'cryptography' else: module.debug('Using OpenSSL binary backend') + return 'openssl' 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. ''' - set_crypto_backend(module) + backend = set_crypto_backend(module) if not module.params['validate_certs']: 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. module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') locale.setlocale(locale.LC_ALL, 'C') + + return backend diff --git a/lib/ansible/modules/crypto/acme/acme_certificate.py b/lib/ansible/modules/crypto/acme/acme_certificate.py index 9dcfda709be..9527a872c9b 100644 --- a/lib/ansible/modules/crypto/acme/acme_certificate.py +++ b/lib/ansible/modules/crypto/acme/acme_certificate.py @@ -203,13 +203,67 @@ options: version_added: 2.6 retrieve_all_alternates: 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 chain as C(all_chains). See the documentation for the C(all_chains) return value for details." type: bool default: no 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''' @@ -312,6 +366,33 @@ EXAMPLES = r''' remaining_days: 60 data: "{{ sample_com_challenge }}" 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 = ''' @@ -432,16 +513,29 @@ from ansible.module_utils.acme import ( ) import base64 +import binascii import hashlib import os import re import textwrap import time +import traceback from datetime import datetime -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes, to_native 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): @@ -907,6 +1001,60 @@ class ACMEClient(object): identifier_type, identifier = type_identifier.split(':', 1) 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): ''' Request a new certificate and write it to the destination file. @@ -927,7 +1075,8 @@ class ACMEClient(object): else: cert_uri = self._finalize_cert() 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 = [] for alternate in cert['alternates']: try: @@ -936,18 +1085,46 @@ class ACMEClient(object): self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) continue alternate_chains.append(alt_cert) - self.all_chains = [] - - def _append_all_chains(cert_data): - self.all_chains.append(dict( - 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) + # Prepare return value for all alternate chains + if self.module.params['retrieve_all_alternates']: + self.all_chains = [] + + def _append_all_chains(cert_data): + self.all_chains.append(dict( + 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: pem_cert = cert['cert'] @@ -1009,6 +1186,13 @@ def main(): deactivate_authzs=dict(type='bool', default=False), force=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( argument_spec=argument_spec, @@ -1021,7 +1205,12 @@ def main(): ), 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: if module.params.get('dest'): diff --git a/test/integration/targets/acme_certificate/tasks/impl.yml b/test/integration/targets/acme_certificate/tasks/impl.yml index 6e2ec61b5ac..04f02710fcd 100644 --- a/test/integration/targets/acme_certificate/tasks/impl.yml +++ b/test/integration/targets/acme_certificate/tasks/impl.yml @@ -58,9 +58,14 @@ terms_agreed: yes account_email: "example@example.org" 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 set_fact: cert_1_obtain_results: "{{ certificate_obtain_result }}" + cert_1_alternate: "{{ 1 if select_crypto_backend == 'cryptography' else 0 }}" - name: Obtain cert 2 include_tasks: obtain-cert.yml vars: @@ -77,9 +82,21 @@ remaining_days: 10 terms_agreed: no 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 set_fact: cert_2_obtain_results: "{{ certificate_obtain_result }}" + cert_2_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" - name: Obtain cert 3 include_tasks: obtain-cert.yml vars: @@ -96,6 +113,15 @@ remaining_days: 10 terms_agreed: no 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 include_tasks: obtain-cert.yml vars: @@ -113,6 +139,16 @@ remaining_days: 10 terms_agreed: no 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 include_tasks: obtain-cert.yml vars: @@ -129,6 +165,10 @@ remaining_days: 10 terms_agreed: no 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) include_tasks: obtain-cert.yml vars: @@ -145,7 +185,8 @@ remaining_days: 10 terms_agreed: no account_email: "" -- set_fact: +- name: Store obtain results for cert 5b + set_fact: cert_5_recreate_1: "{{ challenge_data is changed }}" - name: Obtain cert 5 (should again by less days) include_tasks: obtain-cert.yml @@ -163,8 +204,10 @@ remaining_days: 1000 terms_agreed: no account_email: "" -- set_fact: +- name: Store obtain results for cert 5c + set_fact: cert_5_recreate_2: "{{ challenge_data is changed }}" + cert_5c_obtain_results: "{{ certificate_obtain_result }}" - name: Obtain cert 5 (should again by force) include_tasks: obtain-cert.yml vars: @@ -181,8 +224,10 @@ remaining_days: 10 terms_agreed: no account_email: "" -- set_fact: +- name: Store obtain results for cert 5d + set_fact: cert_5_recreate_3: "{{ challenge_data is changed }}" + cert_5d_obtain_results: "{{ certificate_obtain_result }}" - name: Obtain cert 6 include_tasks: obtain-cert.yml vars: @@ -200,6 +245,20 @@ remaining_days: 10 terms_agreed: yes 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 include_tasks: obtain-cert.yml vars: @@ -219,6 +278,14 @@ remaining_days: 10 terms_agreed: yes 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 include_tasks: obtain-cert.yml vars: @@ -240,6 +307,10 @@ remaining_days: 10 terms_agreed: yes 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 ####################################################################### # Make sure certificates are valid. Root certificate for Pebble equals the chain certificate. - name: Verifying cert 1 @@ -299,6 +370,39 @@ - name: Dumping cert 8 command: openssl x509 -in "{{ output_dir }}/cert-8.pem" -noout -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 ######################################################################### - name: Don't retrieve orders acme_account_info: diff --git a/test/integration/targets/acme_certificate/tasks/main.yml b/test/integration/targets/acme_certificate/tasks/main.yml index e46c6dc4d0e..da66f5f3e44 100644 --- a/test/integration/targets/acme_certificate/tasks/main.yml +++ b/test/integration/targets/acme_certificate/tasks/main.yml @@ -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: - name: Running tests with OpenSSL backend include_tasks: impl.yml diff --git a/test/integration/targets/acme_certificate/tests/validate.yml b/test/integration/targets/acme_certificate/tests/validate.yml index d2a3bcc39b8..cd7d28b0d85 100644 --- a/test/integration/targets/acme_certificate/tests/validate.yml +++ b/test/integration/targets/acme_certificate/tests/validate.yml @@ -11,10 +11,13 @@ assert: that: - "'all_chains' in cert_1_obtain_results" - - "'chain' in cert_1_obtain_results.all_chains[0]" - - "'full_chain' in cert_1_obtain_results.all_chains[0]" - - "lookup('file', output_dir ~ '/cert-1-chain.pem', rstrip=False) == cert_1_obtain_results.all_chains[0].chain" - - "lookup('file', output_dir ~ '/cert-1-fullchain.pem', rstrip=False) == cert_1_obtain_results.all_chains[0].full_chain" + - "cert_1_obtain_results.all_chains | length > 1" + - "'cert' in cert_1_obtain_results.all_chains[cert_1_alternate | int]" + - "'chain' in cert_1_obtain_results.all_chains[cert_1_alternate | int]" + - "'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 assert: @@ -25,10 +28,17 @@ that: - "'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: 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 assert: @@ -40,6 +50,17 @@ - "'DNS:*.example.com' in cert_3_text.stdout" - "'DNS:example.org' 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 assert: @@ -53,6 +74,10 @@ - "'DNS:test.t2.example.com' in cert_4_text.stdout" - "'DNS: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 assert: diff --git a/test/integration/targets/setup_acme/tasks/obtain-cert.yml b/test/integration/targets/setup_acme/tasks/obtain-cert.yml index 08f3e5251d9..98f5f80440e 100644 --- a/test/integration/targets/setup_acme/tasks/obtain-cert.yml +++ b/test/integration/targets/setup_acme/tasks/obtain-cert.yml @@ -112,6 +112,7 @@ account_email: "{{ account_email }}" data: "{{ challenge_data }}" 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 when: challenge_data is changed - name: ({{ certgen_title }}) Deleting HTTP challenges @@ -134,6 +135,6 @@ when: "challenge_data is changed and challenge == 'tls-alpn-01'" - name: ({{ certgen_title }}) Get root certificate 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" ###############################################################################################