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:
parent
35a412fab7
commit
16d4d2dba9
6 changed files with 422 additions and 27 deletions
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
))
|
||||
# Prepare return value for all alternate chains
|
||||
if self.module.params['retrieve_all_alternates']:
|
||||
self.all_chains = []
|
||||
|
||||
_append_all_chains(cert)
|
||||
for alt_chain in alternate_chains:
|
||||
_append_all_chains(alt_chain)
|
||||
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'):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
###############################################################################################
|
||||
|
|
Loading…
Reference in a new issue