# -*- coding: utf-8 -*- # # (c) 2019, Felix Fontein # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . from __future__ import absolute_import, division, print_function __metaclass__ = type import base64 from ansible.module_utils._text import to_bytes, to_text from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress try: import OpenSSL except ImportError: # Error handled in the calling module. pass from ._objects import ( NORMALIZE_NAMES_SHORT, NORMALIZE_NAMES, ) from ._obj2txt import obj2txt from .basic import ( OpenSSLObjectError, ) def pyopenssl_normalize_name(name, short=False): nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name)) if nid != 0: b_name = OpenSSL._util.lib.OBJ_nid2ln(nid) name = to_text(OpenSSL._util.ffi.string(b_name)) if short: return NORMALIZE_NAMES_SHORT.get(name, name) else: return NORMALIZE_NAMES.get(name, name) def pyopenssl_normalize_name_attribute(san): # apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) if san.startswith('IP Address:'): san = 'IP:' + san[len('IP Address:'):] if san.startswith('IP:'): address = san[3:] if '/' in address: ip = compat_ipaddress.ip_network(address) san = 'IP:{0}/{1}'.format(ip.network_address.compressed, ip.prefixlen) else: ip = compat_ipaddress.ip_address(address) san = 'IP:{0}'.format(ip.compressed) if san.startswith('Registered ID:'): san = 'RID:' + san[len('Registered ID:'):] # Some versions of OpenSSL apparently forgot the colon. Happens in CI with Ubuntu 16.04 and FreeBSD 11.1 if san.startswith('Registered ID'): san = 'RID:' + san[len('Registered ID'):] return san def pyopenssl_get_extensions_from_cert(cert): # While pyOpenSSL allows us to get an extension's DER value, it won't # give us the dotted string for an OID. So we have to do some magic to # get hold of it. result = dict() ext_count = cert.get_extension_count() for i in range(0, ext_count): ext = cert.get_extension(i) entry = dict( critical=bool(ext.get_critical()), value=base64.b64encode(ext.get_data()), ) oid = obj2txt( OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) ) # This could also be done a bit simpler: # # oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) # # Unfortunately this gives the wrong result in case the linked OpenSSL # doesn't know the OID. That's why we have to get the OID dotted string # similarly to how cryptography does it. result[oid] = entry return result def pyopenssl_get_extensions_from_csr(csr): # While pyOpenSSL allows us to get an extension's DER value, it won't # give us the dotted string for an OID. So we have to do some magic to # get hold of it. result = dict() for ext in csr.get_extensions(): entry = dict( critical=bool(ext.get_critical()), value=base64.b64encode(ext.get_data()), ) oid = obj2txt( OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) ) # This could also be done a bit simpler: # # oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) # # Unfortunately this gives the wrong result in case the linked OpenSSL # doesn't know the OID. That's why we have to get the OID dotted string # similarly to how cryptography does it. result[oid] = entry return result def pyopenssl_parse_name_constraints(name_constraints_extension): lines = to_text(name_constraints_extension, errors='surrogate_or_strict').splitlines() exclude = None excluded = [] permitted = [] for line in lines: if line.startswith(' ') or line.startswith('\t'): name = pyopenssl_normalize_name_attribute(line.strip()) if exclude is True: excluded.append(name) elif exclude is False: permitted.append(name) else: raise OpenSSLObjectError('Unexpected nameConstraint line: "{0}"'.format(line)) else: line_lc = line.lower() if line_lc.startswith('exclud'): exclude = True elif line_lc.startswith('includ') or line_lc.startswith('permitt'): exclude = False else: raise OpenSSLObjectError('Cannot parse nameConstraint line: "{0}"'.format(line)) return permitted, excluded