From 669c97a7c302b42d76bad448a31b2e28902d8abe Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 11 Apr 2020 14:37:06 +0200 Subject: [PATCH] Add support for CRLs in DER format. --- plugins/module_utils/crypto/identify.py | 11 +++ plugins/modules/x509_crl.py | 69 +++++++++++-- plugins/modules/x509_crl_info.py | 22 ++++- .../targets/x509_crl/tasks/impl.yml | 99 +++++++++++++++++++ .../targets/x509_crl/tests/validate.yml | 23 ++++- 5 files changed, 212 insertions(+), 12 deletions(-) diff --git a/plugins/module_utils/crypto/identify.py b/plugins/module_utils/crypto/identify.py index 8d790f8e0..4423bc435 100644 --- a/plugins/module_utils/crypto/identify.py +++ b/plugins/module_utils/crypto/identify.py @@ -25,6 +25,17 @@ PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' +def identify_pem_format(content): + '''Given the contents of a binary file, tests whether this could be a PEM file.''' + try: + lines = content.decode('utf-8').splitlines(False) + if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): + return True + except UnicodeDecodeError: + pass + return False + + def identify_private_key_format(content): '''Given the contents of a private key file, identifies its format.''' # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 diff --git a/plugins/modules/x509_crl.py b/plugins/modules/x509_crl.py index d3e9303fc..433d20894 100644 --- a/plugins/modules/x509_crl.py +++ b/plugins/modules/x509_crl.py @@ -63,6 +63,15 @@ type: path required: yes + format: + description: + - Whether the CRL file should be in PEM or DER format. + - If an existing CRL file does match everything but I(format), it will be converted to the correct format + instead of regenerated. + type: str + choices: [pem, der] + default: pem + privatekey_path: description: - Path to the CA's private key to use when signing the CRL. @@ -266,6 +275,12 @@ returned: changed or success type: str sample: /path/to/my-ca.pem +format: + description: + - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). + returned: success + type: str + sample: pem issuer: description: - The CRL's issuer. @@ -340,12 +355,16 @@ type: bool sample: no crl: - description: The (current or generated) CRL's content. + description: + - The (current or generated) CRL's content. + - Will be the CRL itself if I(format) is C(pem), and Base64 of the + CRL if I(format) is C(der). returned: if I(state) is C(present) and I(return_content) is C(yes) type: str ''' +import base64 import os import traceback @@ -387,6 +406,10 @@ cryptography_get_signature_algorithm_oid_from_crl, ) +from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import ( + identify_pem_format, +) + MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' CRYPTOGRAPHY_IMP_ERR = None @@ -423,6 +446,8 @@ def __init__(self, module): module.check_mode ) + self.format = module.params['format'] + self.update = module.params['mode'] == 'update' self.ignore_timestamps = module.params['ignore_timestamps'] self.return_content = module.params['return_content'] @@ -514,11 +539,18 @@ def __init__(self, module): try: with open(self.path, 'rb') as f: data = f.read() - self.crl = x509.load_pem_x509_crl(data, default_backend()) - if self.return_content: - self.crl_content = data + self.actual_format = 'pem' if identify_pem_format(data) else 'der' + if self.actual_format == 'pem': + self.crl = x509.load_pem_x509_crl(data, default_backend()) + if self.return_content: + self.crl_content = data + else: + self.crl = x509.load_der_x509_crl(data, default_backend()) + if self.return_content: + self.crl_content = base64.b64encode(data) except Exception as dummy: self.crl_content = None + self.actual_format = self.format def remove(self): if self.backup: @@ -549,7 +581,7 @@ def _compress_entry(self, entry): entry['invalidity_date_critical'], ) - def check(self, perms_required=True): + def check(self, perms_required=True, ignore_conversion=True): """Ensure the resource is in its desired state.""" state_and_perms = super(CRL, self).check(self.module, perms_required) @@ -584,6 +616,9 @@ def check(self, perms_required=True): if old_entries != new_entries: return False + if self.format != self.actual_format and not ignore_conversion: + return False + return True def _generate_crl(self): @@ -631,13 +666,27 @@ def _generate_crl(self): crl = crl.add_revoked_certificate(revoked_cert.build(backend)) self.crl = crl.sign(self.privatekey, self.digest, backend=backend) - return self.crl.public_bytes(Encoding.PEM) + if self.format == 'pem': + return self.crl.public_bytes(Encoding.PEM) + else: + return self.crl.public_bytes(Encoding.DER) def generate(self): - if not self.check(perms_required=False) or self.force: + result = None + if not self.check(perms_required=False, ignore_conversion=True) or self.force: result = self._generate_crl() + elif not self.check(perms_required=False, ignore_conversion=False) and self.crl: + if self.format == 'pem': + result = self.crl.public_bytes(Encoding.PEM) + else: + result = self.crl.public_bytes(Encoding.DER) + + if result is not None: if self.return_content: - self.crl_content = result + if self.format == 'pem': + self.crl_content = result + else: + self.crl_content = base64.b64encode(result) if self.backup: self.backup_file = self.module.backup_local(self.path) write_file(self.module, result) @@ -652,6 +701,7 @@ def dump(self, check_mode=False): 'changed': self.changed, 'filename': self.path, 'privatekey': self.privatekey_path, + 'format': self.format, 'last_update': None, 'next_update': None, 'digest': None, @@ -704,6 +754,7 @@ def main(): force=dict(type='bool', default=False), backup=dict(type='bool', default=False), path=dict(type='path', required=True), + format=dict(type='str', default='pem', choices=['pem', 'der']), privatekey_path=dict(type='path'), privatekey_content=dict(type='str'), privatekey_passphrase=dict(type='str', no_log=True), @@ -760,7 +811,7 @@ def main(): if module.params['state'] == 'present': if module.check_mode: result = crl.dump(check_mode=True) - result['changed'] = module.params['force'] or not crl.check() + result['changed'] = module.params['force'] or not crl.check() or not crl.check(ignore_conversion=False) module.exit_json(**result) crl.generate() diff --git a/plugins/modules/x509_crl_info.py b/plugins/modules/x509_crl_info.py index f690b6724..29a8ac4c2 100644 --- a/plugins/modules/x509_crl_info.py +++ b/plugins/modules/x509_crl_info.py @@ -29,7 +29,7 @@ type: path content: description: - - Content of the X.509 certificate in PEM format. + - Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL. - Either I(path) or I(content) must be specified, but not both. type: str @@ -51,6 +51,12 @@ ''' RETURN = r''' +format: + description: + - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)). + returned: success + type: str + sample: pem issuer: description: - The CRL's issuer. @@ -127,6 +133,7 @@ ''' +import base64 import traceback from distutils.version import LooseVersion @@ -153,6 +160,10 @@ cryptography_get_signature_algorithm_oid_from_crl, ) +from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import ( + identify_pem_format, +) + # crypto_utils MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' @@ -198,15 +209,22 @@ def __init__(self, module): self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e)) else: data = self.content.encode('utf-8') + if not identify_pem_format(data): + data = base64.b64decode(self.content) + self.crl_pem = identify_pem_format(data) try: - self.crl = x509.load_pem_x509_crl(data, default_backend()) + if self.crl_pem: + self.crl = x509.load_pem_x509_crl(data, default_backend()) + else: + self.crl = x509.load_der_x509_crl(data, default_backend()) except Exception as e: self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) def get_info(self): result = { 'changed': False, + 'format': 'pem' if self.crl_pem else 'der', 'last_update': None, 'next_update': None, 'digest': None, diff --git a/tests/integration/targets/x509_crl/tasks/impl.yml b/tests/integration/targets/x509_crl/tasks/impl.yml index eafb2dad2..68ed2fc77 100644 --- a/tests/integration/targets/x509_crl/tasks/impl.yml +++ b/tests/integration/targets/x509_crl/tasks/impl.yml @@ -46,6 +46,10 @@ x509_crl_info: content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") }}' register: crl_1_info_2 +- name: Retrieve CRL 1 infos via file content (Base64) + x509_crl_info: + content: '{{ lookup("file", output_dir ~ "/ca-crl1.crl") | b64encode }}' + register: crl_1_info_3 - name: Create CRL 1 (idempotent, check mode) x509_crl: path: '{{ output_dir }}/ca-crl1.crl' @@ -124,6 +128,101 @@ - serial_number: 1234 revocation_date: 20191001000000Z register: crl_1_idem_content +- name: Create CRL 1 (format, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_format_check +- name: Create CRL 1 (format) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + register: crl_1_format +- name: Create CRL 1 (format, idempotent, check mode) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + check_mode: yes + register: crl_1_format_idem_check +- name: Create CRL 1 (format, idempotent) + x509_crl: + path: '{{ output_dir }}/ca-crl1.crl' + privatekey_path: '{{ output_dir }}/ca.key' + format: der + issuer: + CN: Ansible + last_update: 20191013000000Z + next_update: 20191113000000Z + revoked_certificates: + - path: '{{ output_dir }}/cert-1.pem' + revocation_date: 20191013000000Z + - path: '{{ output_dir }}/cert-2.pem' + revocation_date: 20191013000000Z + reason: key_compromise + reason_critical: yes + invalidity_date: 20191012000000Z + - serial_number: 1234 + revocation_date: 20191001000000Z + return_content: yes + register: crl_1_format_idem +- name: Retrieve CRL 1 infos via file + x509_crl_info: + path: '{{ output_dir }}/ca-crl1.crl' + register: crl_1_info_4 +- name: Read ca-crl1.crl + slurp: + src: "{{ output_dir }}/ca-crl1.crl" + register: content +- name: Retrieve CRL 1 infos via file content (Base64) + x509_crl_info: + content: '{{ content.content }}' + register: crl_1_info_5 - name: Create CRL 2 (check mode) x509_crl: diff --git a/tests/integration/targets/x509_crl/tests/validate.yml b/tests/integration/targets/x509_crl/tests/validate.yml index 17b31f34a..c31fa9420 100644 --- a/tests/integration/targets/x509_crl/tests/validate.yml +++ b/tests/integration/targets/x509_crl/tests/validate.yml @@ -12,7 +12,7 @@ - name: Validate CRL 1 info assert: that: - - crl_1_info_1 == crl_1_info_2 + - crl_1_info_1.format == 'pem' - crl_1_info_1.digest == 'ecdsa-with-SHA256' - crl_1_info_1.issuer | length == 1 - crl_1_info_1.issuer.commonName == 'Ansible' @@ -44,6 +44,27 @@ - crl_1_info_1.revoked_certificates[2].reason_critical == false - crl_1_info_1.revoked_certificates[2].revocation_date == '20191001000000Z' - crl_1_info_1.revoked_certificates[2].serial_number == 1234 + - crl_1_info_1 == crl_1_info_2 + - crl_1_info_1 == crl_1_info_3 + +- name: Validate CRL 1 + assert: + that: + - crl_1_format_check is changed + - crl_1_format is changed + - crl_1_format_idem_check is not changed + - crl_1_format_idem is not changed + - crl_1_info_4.format == 'der' + - crl_1_info_5.format == 'der' + +- name: Read ca-crl1.crl + slurp: + src: "{{ output_dir }}/ca-crl1.crl" + register: content +- name: Validate CRL 1 Base64 content + assert: + that: + - crl_1_format_idem.crl | b64decode == content.content | b64decode - name: Validate CRL 2 assert: