#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2020, Felix Fontein # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = r''' --- module: x509_crl_info version_added: "2.10" short_description: Retrieve information on Certificate Revocation Lists (CRLs) description: - This module allows one to retrieve information on Certificate Revocation Lists (CRLs). requirements: - cryptography >= 1.2 author: - Felix Fontein (@felixfontein) options: path: description: - Remote absolute path where the generated CRL file should be created or is already located. - Either I(path) or I(content) must be specified, but not both. type: path content: description: - Content of the X.509 certificate in PEM format. - Either I(path) or I(content) must be specified, but not both. type: str notes: - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern. They are all in UTC. seealso: - module: x509_crl ''' EXAMPLES = r''' - name: Get information on CRL x509_crl_info: path: /etc/ssl/my-ca.crl register: result - debug: msg: "{{ result }}" ''' RETURN = r''' issuer: description: - The CRL's issuer. - Note that for repeated values, only the last one will be returned. returned: success type: dict sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' issuer_ordered: description: The CRL's issuer as an ordered list of tuples. returned: success type: list elements: list sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' last_update: description: The point in time from which this CRL can be trusted as ASN.1 TIME. returned: success type: str sample: 20190413202428Z next_update: description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. returned: success type: str sample: 20190413202428Z digest: description: The signature algorithm used to sign the CRL. returned: success type: str sample: sha256WithRSAEncryption revoked_certificates: description: List of certificates to be revoked. returned: success type: list elements: dict contains: serial_number: description: Serial number of the certificate. type: int sample: 1234 revocation_date: description: The point in time the certificate was revoked as ASN.1 TIME. type: str sample: 20190413202428Z issuer: description: The certificate's issuer. type: list elements: str sample: '["DNS:ca.example.org"]' issuer_critical: description: Whether the certificate issuer extension is critical. type: bool sample: no reason: description: - The value for the revocation reason extension. - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded), C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and C(remove_from_crl). type: str sample: key_compromise reason_critical: description: Whether the revocation reason extension is critical. type: bool sample: no invalidity_date: description: | The point in time it was known/suspected that the private key was compromised or that the certificate otherwise became invalid as ASN.1 TIME. type: str sample: 20190413202428Z invalidity_date_critical: description: Whether the invalidity date extension is critical. type: bool sample: no ''' import traceback from ansible.module_utils.compat.version import LooseVersion from ansible.module_utils import crypto as crypto_utils from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule, missing_required_lib MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' CRYPTOGRAPHY_IMP_ERR = None try: import cryptography from cryptography import x509 from cryptography.hazmat.backends import default_backend CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) except ImportError: CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_FOUND = False else: CRYPTOGRAPHY_FOUND = True TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" class CRLError(crypto_utils.OpenSSLObjectError): pass class CRLInfo(crypto_utils.OpenSSLObject): """The main module implementation.""" def __init__(self, module): super(CRLInfo, self).__init__( module.params['path'] or '', 'present', False, module.check_mode ) self.content = module.params['content'] self.module = module self.crl = None if self.content is None: try: with open(self.path, 'rb') as f: data = f.read() except Exception as e: self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e)) else: data = self.content.encode('utf-8') try: self.crl = x509.load_pem_x509_crl(data, default_backend()) except Exception as e: self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) def _dump_revoked(self, entry): return { 'serial_number': entry['serial_number'], 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), 'issuer': [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] if entry['issuer'] is not None else None, 'issuer_critical': entry['issuer_critical'], 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, 'reason_critical': entry['reason_critical'], 'invalidity_date': entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) if entry['invalidity_date'] is not None else None, 'invalidity_date_critical': entry['invalidity_date_critical'], } def get_info(self): result = { 'changed': False, 'last_update': None, 'next_update': None, 'digest': None, 'issuer_ordered': None, 'issuer': None, 'revoked_certificates': [], } result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) try: result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) except AttributeError: # Older cryptography versions don't have signature_algorithm_oid yet dotted = crypto_utils._obj2txt( self.crl._backend._lib, self.crl._backend._ffi, self.crl._x509_crl.sig_alg.algorithm ) oid = x509.oid.ObjectIdentifier(dotted) result['digest'] = crypto_utils.cryptography_oid_to_name(oid) issuer = [] for attribute in self.crl.issuer: issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) result['issuer_ordered'] = issuer result['issuer'] = {} for k, v in issuer: result['issuer'][k] = v result['revoked_certificates'] = [] for cert in self.crl: entry = crypto_utils.cryptography_decode_revoked_certificate(cert) result['revoked_certificates'].append(self._dump_revoked(entry)) return result def generate(self): # Empty method because crypto_utils.OpenSSLObject wants this pass def dump(self): # Empty method because crypto_utils.OpenSSLObject wants this pass def main(): module = AnsibleModule( argument_spec=dict( path=dict(type='path'), content=dict(type='str'), ), required_one_of=( ['path', 'content'], ), mutually_exclusive=( ['path', 'content'], ), supports_check_mode=True, ) if not CRYPTOGRAPHY_FOUND: module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), exception=CRYPTOGRAPHY_IMP_ERR) try: crl = CRLInfo(module) result = crl.get_info() module.exit_json(**result) except crypto_utils.OpenSSLObjectError as e: module.fail_json(msg=to_native(e)) if __name__ == "__main__": main()