certificate_complete_chain: avoid infinite loops, and double roots when root certificate was already part of chain (#360)

* Avoid infinite loops, and double roots when root certificate was already part of chain.

* Refactor tests for readability.
pull/367/head
Felix Fontein 2022-01-04 07:00:09 +01:00 committed by GitHub
parent f3e431912d
commit 6ee238d961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 54 deletions

View File

@ -0,0 +1,3 @@
bugfixes:
- "certificate_complete_chain - do not hang when infinite loop is found (https://github.com/ansible-collections/community.crypto/issues/355, https://github.com/ansible-collections/community.crypto/pull/360)."
- "certificate_complete_chain - do not append root twice if the chain already ends with a root certificate (https://github.com/ansible-collections/community.crypto/pull/360)."

View File

@ -238,12 +238,14 @@ class CertificateSet(object):
self.module = module self.module = module
self.certificates = set() self.certificates = set()
self.certificate_by_issuer = dict() self.certificate_by_issuer = dict()
self.certificate_by_cert = dict()
def _load_file(self, path): def _load_file(self, path):
certs = load_PEM_list(self.module, path, fail_on_error=False) certs = load_PEM_list(self.module, path, fail_on_error=False)
for cert in certs: for cert in certs:
self.certificates.add(cert) self.certificates.add(cert)
self.certificate_by_issuer[cert.cert.subject] = cert self.certificate_by_issuer[cert.cert.subject] = cert
self.certificate_by_cert[cert.cert] = cert
def load(self, path): def load(self, path):
''' '''
@ -275,6 +277,16 @@ def format_cert(cert):
return str(cert.cert) return str(cert.cert)
def check_cycle(module, occured_certificates, next):
'''
Make sure that next is not in occured_certificates so far, and add it.
'''
next_cert = next.cert
if next_cert in occured_certificates:
module.fail_json(msg='Found cycle while building certificate chain')
occured_certificates.add(next_cert)
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
@ -313,13 +325,19 @@ def main():
# Try to complete chain # Try to complete chain
current = chain[-1] current = chain[-1]
completed = [] completed = []
occured_certificates = set([cert.cert for cert in chain])
if current.cert in roots.certificate_by_cert:
# Don't try to complete the chain when it's already ending with a root certificate
current = None
while current: while current:
root = roots.find_parent(current) root = roots.find_parent(current)
if root: if root:
check_cycle(module, occured_certificates, root)
completed.append(root) completed.append(root)
break break
intermediate = intermediates.find_parent(current) intermediate = intermediates.find_parent(current)
if intermediate: if intermediate:
check_cycle(module, occured_certificates, intermediate)
completed.append(intermediate) completed.append(intermediate)
current = intermediate current = intermediate
else: else:

View File

@ -15,73 +15,144 @@
src: '{{ role_path }}/files/' src: '{{ role_path }}/files/'
dest: '{{ remote_tmp_dir }}/files/' dest: '{{ remote_tmp_dir }}/files/'
- name: Find root for cert 1 using directory - block:
certificate_complete_chain: - name: Find root for cert 1 using directory
input_chain: '{{ lookup("file", "cert1-fullchain.pem", rstrip=False) }}' certificate_complete_chain:
root_certificates: input_chain: '{{ fullchain | trim }}'
- '{{ remote_tmp_dir }}/files/roots/' root_certificates:
register: cert1_root - '{{ remote_tmp_dir }}/files/roots/'
- name: Verify root for cert 1 register: cert1_root
assert: - name: Verify root for cert 1
that: assert:
- cert1_root.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False)) that:
- cert1_root.root == lookup('file', 'cert1-root.pem', rstrip=False) - cert1_root.complete_chain | join('') == (fullchain ~ root)
- cert1_root.root == root
vars:
fullchain: "{{ lookup('file', 'cert1-fullchain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}"
- name: Find rootchain for cert 1 using intermediate and root PEM - block:
- name: Find rootchain for cert 1 using intermediate and root PEM
certificate_complete_chain:
input_chain: '{{ cert }}'
intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert1-chain.pem'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem'
register: cert1_rootchain
- name: Verify rootchain for cert 1
assert:
that:
- cert1_rootchain.complete_chain | join('') == (cert ~ chain ~ root)
- cert1_rootchain.chain[:-1] | join('') == chain
- cert1_rootchain.root == root
vars:
cert: "{{ lookup('file', 'cert1.pem', rstrip=False) }}"
chain: "{{ lookup('file', 'cert1-chain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert1-root.pem', rstrip=False) }}"
- block:
- name: Find root for cert 2 using directory
certificate_complete_chain:
input_chain: "{{ fullchain | trim }}"
root_certificates:
- '{{ remote_tmp_dir }}/files/roots/'
register: cert2_root
- name: Verify root for cert 2
assert:
that:
- cert2_root.complete_chain | join('') == (fullchain ~ root)
- cert2_root.root == root
vars:
fullchain: "{{ lookup('file', 'cert2-fullchain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}"
- block:
- name: Find rootchain for cert 2 using intermediate and root PEM
certificate_complete_chain:
input_chain: '{{ cert }}'
intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert2-chain.pem'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem'
register: cert2_rootchain
- name: Verify rootchain for cert 2
assert:
that:
- cert2_rootchain.complete_chain | join('') == (cert ~ chain ~ root)
- cert2_rootchain.chain[:-1] | join('') == chain
- cert2_rootchain.root == root
vars:
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
chain: "{{ lookup('file', 'cert2-chain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert2-root.pem', rstrip=False) }}"
- block:
- name: Find alternate rootchain for cert 2 using intermediate and root PEM
certificate_complete_chain:
input_chain: '{{ cert }}'
intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert2-altchain.pem'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem'
register: cert2_rootchain_alt
- name: Verify rootchain for cert 2
assert:
that:
- cert2_rootchain_alt.complete_chain | join('') == (cert ~ chain ~ root)
- cert2_rootchain_alt.chain[:-1] | join('') == chain
- cert2_rootchain_alt.root == root
vars:
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}"
- block:
- name: Find alternate rootchain for cert 2 when complete chain is already presented to the module
certificate_complete_chain:
input_chain: '{{ cert ~ chain ~ root }}'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem'
register: cert2_complete_chain
- name: Verify rootchain for cert 2
assert:
that:
- cert2_complete_chain.complete_chain | join('') == (cert ~ chain ~ root)
- cert2_complete_chain.chain == []
- cert2_complete_chain.root == root
vars:
cert: "{{ lookup('file', 'cert2.pem', rstrip=False) }}"
chain: "{{ lookup('file', 'cert2-altchain.pem', rstrip=False) }}"
root: "{{ lookup('file', 'cert2-altroot.pem', rstrip=False) }}"
- name: Check failure when no intermediate certificate can be found
certificate_complete_chain: certificate_complete_chain:
input_chain: '{{ lookup("file", "cert1.pem", rstrip=False) }}' input_chain: '{{ lookup("file", "cert2.pem", rstrip=True) }}'
intermediate_certificates: intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert1-chain.pem' - '{{ remote_tmp_dir }}/files/cert1-chain.pem'
root_certificates: root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem' - '{{ remote_tmp_dir }}/files/roots.pem'
register: cert1_rootchain register: cert2_no_intermediate
- name: Verify rootchain for cert 1 ignore_errors: true
- name: Verify failure
assert: assert:
that: that:
- cert1_rootchain.complete_chain | join('') == (lookup('file', 'cert1.pem', rstrip=False) ~ lookup('file', 'cert1-chain.pem', rstrip=False) ~ lookup('file', 'cert1-root.pem', rstrip=False)) - cert2_no_intermediate is failed
- cert1_rootchain.chain[:-1] | join('') == lookup('file', 'cert1-chain.pem', rstrip=False) - "cert2_no_intermediate.msg.startswith('Cannot complete chain. Stuck at certificate ')"
- cert1_rootchain.root == lookup('file', 'cert1-root.pem', rstrip=False)
- name: Find root for cert 2 using directory - name: Check failure when infinite loop is found
certificate_complete_chain: certificate_complete_chain:
input_chain: '{{ lookup("file", "cert2-fullchain.pem", rstrip=False) }}' input_chain: '{{ lookup("file", "cert2-fullchain.pem", rstrip=True) }}'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots/'
register: cert2_root
- name: Verify root for cert 2
assert:
that:
- cert2_root.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False))
- cert2_root.root == lookup('file', 'cert2-root.pem', rstrip=False)
- name: Find rootchain for cert 2 using intermediate and root PEM
certificate_complete_chain:
input_chain: '{{ lookup("file", "cert2.pem", rstrip=False) }}'
intermediate_certificates: intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert2-chain.pem'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem' - '{{ remote_tmp_dir }}/files/roots.pem'
register: cert2_rootchain root_certificates:
- name: Verify rootchain for cert 2 - '{{ remote_tmp_dir }}/files/cert1-chain.pem'
register: cert2_infinite_loop
ignore_errors: true
- name: Verify failure
assert: assert:
that: that:
- cert2_rootchain.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-chain.pem', rstrip=False) ~ lookup('file', 'cert2-root.pem', rstrip=False)) - cert2_infinite_loop is failed
- cert2_rootchain.chain[:-1] | join('') == lookup('file', 'cert2-chain.pem', rstrip=False) - "cert2_infinite_loop.msg == 'Found cycle while building certificate chain'"
- cert2_rootchain.root == lookup('file', 'cert2-root.pem', rstrip=False)
- name: Find alternate rootchain for cert 2 using intermediate and root PEM
certificate_complete_chain:
input_chain: '{{ lookup("file", "cert2.pem", rstrip=True) }}'
intermediate_certificates:
- '{{ remote_tmp_dir }}/files/cert2-altchain.pem'
root_certificates:
- '{{ remote_tmp_dir }}/files/roots.pem'
register: cert2_rootchain_alt
- name: Verify rootchain for cert 2
assert:
that:
- cert2_rootchain_alt.complete_chain | join('') == (lookup('file', 'cert2.pem', rstrip=False) ~ lookup('file', 'cert2-altchain.pem', rstrip=False) ~ lookup('file', 'cert2-altroot.pem', rstrip=False))
- cert2_rootchain_alt.chain[:-1] | join('') == lookup('file', 'cert2-altchain.pem', rstrip=False)
- cert2_rootchain_alt.root == lookup('file', 'cert2-altroot.pem', rstrip=False)
when: cryptography_version.stdout is version('1.5', '>=') when: cryptography_version.stdout is version('1.5', '>=')