PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/x509.py |
import base64 import copy import ipaddress import logging import os.path import re from datetime import datetime, timedelta, timezone from enum import Enum from urllib.parse import urlparse, urlunparse import cryptography from cryptography import x509 as cx509 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, padding, rsa from cryptography.hazmat.primitives.serialization import pkcs7, pkcs12 from cryptography.x509.oid import SubjectInformationAccessOID import salt.utils.files import salt.utils.immutabletypes as immutabletypes import salt.utils.stringutils import salt.utils.versions from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.utils.odict import OrderedDict try: import idna HAS_IDNA = True except ImportError: HAS_IDNA = False CRYPTOGRAPHY_VERSION = tuple(int(x) for x in cryptography.__version__.split(".")) log = logging.getLogger(__name__) NAME_ATTRS_ALT_NAMES = immutabletypes.freeze( { "CN": ("commonName",), "L": ("localityName",), "ST": ("stateOrProvinceName",), "O": ("organizationName",), "OU": ("organizationUnitName",), "GN": ("givenName",), "SN": ("surname",), "MAIL": ("Email", "emailAddress"), "SERIALNUMBER": ("serialNumber",), } ) NAME_ATTRS_OID = immutabletypes.freeze( OrderedDict( [ ("C", cx509.NameOID.COUNTRY_NAME), ("ST", cx509.NameOID.STATE_OR_PROVINCE_NAME), ("L", cx509.NameOID.LOCALITY_NAME), ("STREET", cx509.NameOID.STREET_ADDRESS), ("O", cx509.NameOID.ORGANIZATION_NAME), ("OU", cx509.NameOID.ORGANIZATIONAL_UNIT_NAME), ("CN", cx509.NameOID.COMMON_NAME), ("MAIL", cx509.NameOID.EMAIL_ADDRESS), ("SN", cx509.NameOID.SURNAME), ("GN", cx509.NameOID.GIVEN_NAME), ("UID", cx509.NameOID.USER_ID), ("SERIALNUMBER", cx509.NameOID.SERIAL_NUMBER), ] ) ) EXTENSIONS_ALT_NAMES = immutabletypes.freeze( { "basicConstraints": ("X509v3 Basic Constraints",), "keyUsage": ("X509v3 Key Usage",), "extendedKeyUsage": ("X509v3 Extended Key Usage",), "subjectKeyIdentifier": ("X509v3 Subject Key Identifier",), "authorityKeyIdentifier": ("X509v3 Authority Key Identifier",), # issuserAltName was a typo in the old x509 modules "issuerAltName": ("X509v3 Issuer Alternative Name", "issuserAltName"), "authorityInfoAccess": ("Authority Information Access",), "subjectAltName": ("X509v3 Subject Alternative Name",), "crlDistributionPoints": ("X509v3 CRL Distribution Points",), "issuingDistributionPoint": ("X509v3 Issuing Distribution Point",), "certificatePolicies": ("X509v3 Certificate Policies",), "policyConstraints": ("X509v3 Policy Constraints",), "inhibitAnyPolicy": ("X509v3 Inhibit Any Policy",), "nameConstraints": ("X509v3 Name Constraints",), "noCheck": ("OCSP No Check",), "tlsfeature": ("TLS Feature",), "nsComment": ("Netscape Comment",), "nsCertType": ("Netscape Certificate Type",), "cRLNumber": ("X509v3 CRLNumber",), "deltaCRLIndicator": ("X509v3 Delta CRL Indicator",), "freshestCRL": ("x509v3 Freshest CRL",), } ) EXTENSIONS_OID = immutabletypes.freeze( { "basicConstraints": cx509.ExtensionOID.BASIC_CONSTRAINTS, "keyUsage": cx509.ExtensionOID.KEY_USAGE, "extendedKeyUsage": cx509.ExtensionOID.EXTENDED_KEY_USAGE, "subjectKeyIdentifier": cx509.ExtensionOID.SUBJECT_KEY_IDENTIFIER, "authorityKeyIdentifier": cx509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER, "issuerAltName": cx509.ExtensionOID.ISSUER_ALTERNATIVE_NAME, "authorityInfoAccess": cx509.ExtensionOID.AUTHORITY_INFORMATION_ACCESS, "subjectAltName": cx509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME, "crlDistributionPoints": cx509.ExtensionOID.CRL_DISTRIBUTION_POINTS, "issuingDistributionPoint": cx509.ExtensionOID.ISSUING_DISTRIBUTION_POINT, "certificatePolicies": cx509.ExtensionOID.CERTIFICATE_POLICIES, "policyConstraints": cx509.ExtensionOID.POLICY_CONSTRAINTS, "inhibitAnyPolicy": cx509.ExtensionOID.INHIBIT_ANY_POLICY, "nameConstraints": cx509.ExtensionOID.NAME_CONSTRAINTS, "noCheck": cx509.ExtensionOID.OCSP_NO_CHECK, "tlsfeature": cx509.ExtensionOID.TLS_FEATURE, "nsComment": None, "nsCertType": None, "cRLNumber": cx509.ExtensionOID.CRL_NUMBER, "deltaCRLIndicator": cx509.ExtensionOID.DELTA_CRL_INDICATOR, "freshestCRL": cx509.ExtensionOID.FRESHEST_CRL, } ) EXTENSIONS_CRL_ENTRY_OID = immutabletypes.freeze( { "certificateIssuer": cx509.CRLEntryExtensionOID.CERTIFICATE_ISSUER, "CRLReason": cx509.CRLEntryExtensionOID.CRL_REASON, "invalidityDate": cx509.CRLEntryExtensionOID.INVALIDITY_DATE, } ) EXTENDED_KEY_USAGE_OID = immutabletypes.freeze( { "serverAuth": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.1"), "clientAuth": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.2"), "codeSigning": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.3"), "emailProtection": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.4"), "timeStamping": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.8"), "OCSPSigning": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.9"), "msSmartcardLogin": cx509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.2"), "pkInitKDC": cx509.ObjectIdentifier("1.3.6.1.5.2.3.5"), "ipsecIKE": cx509.ObjectIdentifier("1.3.6.1.5.5.7.3.17"), "msCodeInd": cx509.ObjectIdentifier("1.3.6.1.4.1.311.2.1.21"), "msCodeCom": cx509.ObjectIdentifier("1.3.6.1.4.1.311.2.1.22"), "msCTLSign": cx509.ObjectIdentifier("1.3.6.1.4.1.311.10.3.1"), "msEFS": cx509.ObjectIdentifier("1.3.6.1.4.1.311.10.3.4"), } ) ACCESS_OID = immutabletypes.freeze( { "OCSP": cx509.AuthorityInformationAccessOID.OCSP, "caIssuers": cx509.AuthorityInformationAccessOID.CA_ISSUERS, "caRepository": SubjectInformationAccessOID.CA_REPOSITORY, } ) CERT_EXTS = ( "basicConstraints", "keyUsage", "extendedKeyUsage", "subjectKeyIdentifier", "authorityKeyIdentifier", "issuerAltName", "authorityInfoAccess", "subjectAltName", "crlDistributionPoints", "certificatePolicies", "policyConstraints", "inhibitAnyPolicy", "nameConstraints", "noCheck", "tlsfeature", "nsComment", "nsCertType", ) CRL_EXTS = ( "authorityKeyIdentifier", "authorityInfoAccess", "cRLNumber", "deltaCRLIndicator", "freshestCRL", "issuerAltName", "issuingDistributionPoint", ) CSR_FORBIDDEN = ( "authorityInfoAccess", "authorityKeyIdentifier", "issuerAltName", "crlDistributionPoints", ) class KEY_TYPE(Enum): RSA = 1 EC = 2 ED25519 = 3 ED448 = 4 PEM_BEGIN = b"-----BEGIN" PEM_END = b"-----END" TIME_FMT = "%Y-%m-%d %H:%M:%S" def ensure_cert_kwargs_compat(kwargs): """ Ensures the deprecated long form of Name Attribute and extension definitions is still recognized, but warned about. """ for name, long_names in NAME_ATTRS_ALT_NAMES.items(): for long_name in long_names: if long_name in kwargs: salt.utils.versions.warn_until( 3009, f"Found {long_name} in keyword args. Please migrate to the short name: {name}", ) kwargs[name] = kwargs.pop(long_name) for extname, long_names in EXTENSIONS_ALT_NAMES.items(): for long_name in long_names: if long_name in kwargs: salt.utils.versions.warn_until( 3009, f"Found {long_name} in keyword args. Please migrate to the short name: {extname}", ) kwargs[extname] = kwargs.pop(long_name) return kwargs def build_crt( signing_private_key, skip_load_signing_private_key=False, signing_private_key_passphrase=None, signing_cert=None, public_key=None, private_key=None, private_key_passphrase=None, csr=None, subject=None, serial_number=None, not_before=None, not_after=None, days_valid=30, **kwargs, ): """ Parse the input into a CertificateBuilder, which can be used to sign the certificate or be inspected for changes. Also returns signing private key (if available), associated private key and associated signing certificate. """ ca_pub = None self_signed = False if not skip_load_signing_private_key: # skipping this is necessary for the state module to be able to check # for changes when issuing certs from remote signing_private_key = load_privkey( signing_private_key, passphrase=signing_private_key_passphrase ) else: signing_private_key = None serial_number = _get_serial_number(serial_number) # optionally holds cert-associated private key, needed for pkcs12 serialization later private_key_loaded = None if csr: csr = load_csr(csr) if signing_cert: signing_cert = load_cert(signing_cert) else: self_signed = True public_key = signing_private_key.public_key() ca_pub = public_key if self_signed: pass elif private_key: private_key_loaded = load_privkey( private_key, passphrase=private_key_passphrase ) public_key = private_key_loaded.public_key() elif public_key: public_key = load_pubkey(public_key) elif csr: public_key = csr.public_key() else: raise SaltInvocationError( "This certificate is not self-signed (signing_cert is set) and thus " "needs public_key, private_key or csr to derive public key to sign" ) if ( not skip_load_signing_private_key and not self_signed and not is_pair(signing_cert.public_key(), signing_private_key) ): raise SaltInvocationError( "Signing private key does not match the certificate's public key" ) builder = cx509.CertificateBuilder( serial_number=serial_number, public_key=public_key ) subject_name = _get_dn(subject or kwargs) builder = builder.subject_name(subject_name).issuer_name( signing_cert.subject if not self_signed else subject_name ) not_before = ( datetime.strptime(not_before, TIME_FMT).replace(tzinfo=timezone.utc) if not_before else datetime.now(tz=timezone.utc) ) not_after = ( datetime.strptime(not_after, TIME_FMT).replace(tzinfo=timezone.utc) if not_after else datetime.now(tz=timezone.utc) + timedelta(days=days_valid) ) builder = builder.not_valid_before(not_before).not_valid_after(not_after) ext_present = [] for extname in EXTENSIONS_OID: if extname not in CERT_EXTS: continue if extname in kwargs: val = kwargs[extname] # signing policies need to be able to force-unset extensions if val is None: continue ext, critical = _create_extension( extname, val, ca_crt=signing_cert, subject_pubkey=public_key, ca_pub=ca_pub, ) builder = builder.add_extension( ext, critical=critical, ) ext_present.append(extname) if csr: for extname, oid in EXTENSIONS_OID.items(): if any( ( extname in ext_present, extname not in CERT_EXTS, extname in CSR_FORBIDDEN, ) ): continue try: ext = csr.extensions.get_extension_for_oid(oid) builder = builder.add_extension(ext.value, ext.critical) except cx509.ExtensionNotFound: pass return builder, signing_private_key, private_key_loaded, signing_cert def build_csr(private_key, private_key_passphrase=None, subject=None, **kwargs): """ Parse the input into a CertificateSigningRequestBuilder, which can be used to sign the CSR or be inspected for changes. Also returns associated private key. """ private_key = load_privkey(private_key, passphrase=private_key_passphrase) public_key = private_key.public_key() builder = cx509.CertificateSigningRequestBuilder() subject_name = _get_dn(subject or kwargs) builder = builder.subject_name(subject_name) for extname, oid in EXTENSIONS_OID.items(): if any( ( extname not in CERT_EXTS, extname in CSR_FORBIDDEN, ) ): continue if extname in kwargs: ext, critical = _create_extension( extname, kwargs[extname], subject_pubkey=public_key ) builder = builder.add_extension( ext, critical=critical, ) return builder, private_key def build_crl( signing_private_key, revoked, signing_cert=None, signing_private_key_passphrase=None, include_expired=False, days_valid=100, extensions=None, ): """ Parse the input into a CertificateRevocationListBuilder, which can be used to sign the CRL or be inspected for changes. Also returns signing private key. """ extensions = extensions or {} if signing_cert: signing_cert = load_cert(signing_cert) signing_private_key = load_privkey( signing_private_key, passphrase=signing_private_key_passphrase ) if signing_cert and not is_pair(signing_cert.public_key(), signing_private_key): raise SaltInvocationError( "Signing private key does not match the certificate's public key" ) builder = cx509.CertificateRevocationListBuilder() if signing_cert: builder = builder.issuer_name(signing_cert.subject) builder = builder.last_update(datetime.now(tz=timezone.utc)) builder = builder.next_update( datetime.now(tz=timezone.utc) + timedelta(days=days_valid) ) for rev in revoked: serial_number = not_after = revocation_date = None if "not_after" in rev: not_after = datetime.strptime(rev["not_after"], TIME_FMT).replace( tzinfo=timezone.utc ) if "serial_number" in rev: serial_number = rev["serial_number"] if "certificate" in rev: rev_cert = load_cert(rev["certificate"]) serial_number = rev_cert.serial_number try: not_after = rev_cert.not_valid_after_utc except AttributeError: # naive datetime object, release <42 (it's always UTC) not_after = rev_cert.not_valid_after.replace(tzinfo=timezone.utc) if not serial_number: raise SaltInvocationError("Need serial_number or certificate") serial_number = _get_serial_number(serial_number) if not_after and not include_expired: if datetime.now(tz=timezone.utc) > not_after: continue if "revocation_date" in rev: revocation_date = datetime.strptime( rev["revocation_date"], TIME_FMT ).replace(tzinfo=timezone.utc) else: revocation_date = datetime.now(tz=timezone.utc) revoked_cert = cx509.RevokedCertificateBuilder( serial_number=serial_number, revocation_date=revocation_date ) for extname, val in rev.get("extensions", {}).items(): if extname not in EXTENSIONS_CRL_ENTRY_OID: log.warning("Ignoring invalid CRL entry extension: %s", extname) continue ext, critical = _create_extension(extname, val) revoked_cert = revoked_cert.add_extension(ext, critical=critical) builder = builder.add_revoked_certificate(revoked_cert.build()) for extname, val in extensions.items(): if extname not in CRL_EXTS: log.warning("Ignoring invalid CRL extension: %s", extname) continue ext, critical = _create_extension(extname, val, ca_crt=signing_cert) builder = builder.add_extension( ext, critical=critical, ) return builder, signing_private_key def generate_rsa_privkey(keysize=2048): """ Generate an RSA private key """ if keysize not in [2048, 3072, 4096]: raise SaltInvocationError( "RSA key size must be either 2048, 3072 or 4096 bits." ) return rsa.generate_private_key(public_exponent=65537, key_size=keysize) def generate_ec_privkey(keysize=256): """ Generate an elliptic curve private key """ if keysize not in [256, 384, 521]: raise SaltInvocationError("EC key size must be either 256, 384, 521 bits.") return ec.generate_private_key(getattr(ec, f"SECP{keysize}R1")()) def generate_ed25519_privkey(): """ Generate an ed25519 private key """ return ed25519.Ed25519PrivateKey.generate() def generate_ed448_privkey(): """ Generate an ed448 private key """ return ed448.Ed448PrivateKey.generate() def get_hashing_algorithm(digest): """ Returns an instance of a hashing algorithm, if available """ try: return getattr(hashes, digest.upper())() except AttributeError as err: raise CommandExecutionError( "The selected hashing algorithm does not exist in the cryptography " "python library" ) from err def get_key_type(key, as_string=False): """ Checks which type of private/public key a class instance is. Returns None if it is not a valid key. """ key_type = None if isinstance(key, (rsa.RSAPrivateKey, rsa.RSAPublicKey)): key_type = "rsa" if isinstance(key, (ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey)): key_type = "ec" if isinstance(key, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey)): key_type = "ed25519" if isinstance(key, (ed448.Ed448PrivateKey, ed448.Ed448PublicKey)): key_type = "ed448" if key_type is None or as_string: return key_type return getattr(KEY_TYPE, key_type.upper()) def is_pair(pubkey, privkey): """ Checks whether a public key belongs to a private key """ privkey_type = get_key_type(privkey) if privkey_type is None: raise SaltInvocationError("Did not recognize key type") return match_pubkey(pubkey, privkey.public_key()) def match_pubkey(pubkey_a, pubkey_b): """ Checks whether two public keys are the same """ pubkey_a_type = get_key_type(pubkey_a) pubkey_b_type = get_key_type(pubkey_b) if pubkey_a_type is None or pubkey_b_type is None: raise SaltInvocationError("Did not recognize key type") if pubkey_a_type != pubkey_b_type: return False if pubkey_a_type in [KEY_TYPE.RSA, KEY_TYPE.EC]: return pubkey_a.public_numbers() == pubkey_b.public_numbers() return to_pem(pubkey_a) == to_pem(pubkey_b) def merge_signing_policy(policy, kwargs): """ Merge a signing policy, taking care that the different methods of specifying RDN do not lead to unexpected results. This is found in utils since the state module needs access as well to check for expected changes. """ if not policy: return kwargs # ensure we don't modify data that is used elsewhere policy = copy.deepcopy(policy) # Don't immediately break for the long form of name attributes for name, long_names in NAME_ATTRS_ALT_NAMES.items(): for long_name in long_names: if long_name in kwargs: salt.utils.versions.warn_until( 3009, f"Found {long_name} in keyword args. Please migrate to the short name: {name}", ) kwargs[name] = kwargs.pop(long_name) # Don't immediately break for the long form of extensions for extname, long_names in EXTENSIONS_ALT_NAMES.items(): for long_name in long_names: if long_name in kwargs: salt.utils.versions.warn_until( 3009, f"Found {long_name} in keyword args. Please migrate to the short name: {extname}", ) kwargs[extname] = kwargs.pop(long_name) if "subject" in kwargs: # a) ensure subject in kwargs does not override CN etc from signing policy if any(x in policy for x in NAME_ATTRS_OID): kwargs.pop("subject") # b) subject is not enforced or if it is a string, it cannot be overridden elif "subject" not in policy or not isinstance(policy["subject"], (dict, list)): pass # c) if both subject sources are of the same time, update dicts or merge lists else: try: kwargs["subject"].update(policy["subject"]) policy.pop("subject") except (AttributeError, ValueError): try: kwargs["subject"] = policy["subject"] + kwargs["subject"] policy.pop("subject") except TypeError: pass # d) otherwise enforce subject from signing policy kwargs.update(policy) return kwargs def to_pem(pub_or_cert): """ Returns the PEM-encoded serialization of a public key, certificate, certificate signing request or certificate revocation list. This does not work for private keys. """ try: return pub_or_cert.public_bytes( serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) except (AttributeError, TypeError): pass try: return pub_or_cert.public_bytes(serialization.Encoding.PEM) except (AttributeError, TypeError): pass raise SaltInvocationError("Could not serialize parameter to PEM") def to_der(pub_or_cert): """ Returns the DER-encoded serialization of a public key, certificate, certificate signing request or certificate revocation list. This does not work for private keys. """ try: return pub_or_cert.public_bytes( serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) except (AttributeError, TypeError): pass try: return pub_or_cert.public_bytes(serialization.Encoding.DER) except (AttributeError, TypeError): pass raise SaltInvocationError("Could not serialize parameter to DER") def load_privkey(pk, passphrase=None, get_encoding=False): """ Return a private key instance from * a class instance * a file path on the local system * a string (PEM) * bytes (hex, base64, raw) Valid encodings are PEM, DER and PKCS12. """ if hasattr(pk, "private_bytes"): if isinstance( pk, ( rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey, ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey, ), ): if get_encoding: return pk, None, None return pk raise SaltInvocationError( f"Passed object is not a known private key, but {pk.__class__.__name__}" ) pk = load_file_or_bytes(pk) passphrase = passphrase.encode() if passphrase is not None else None # PEM if PEM_BEGIN in pk: try: pk = serialization.load_pem_private_key(pk, password=passphrase) if get_encoding: return pk, "pem", None return pk except ValueError as err: if "Bad decrypt" in str(err): raise SaltInvocationError( "Bad decrypt - is the password correct?" ) from err raise CommandExecutionError( "Could not load PEM-encoded private key" ) from err except TypeError as err: if "private key is encrypted" in str(err): raise SaltInvocationError( "Private key is encrypted. Please provide a password." ) from err if "but private key is not encrypted" in str(err): raise SaltInvocationError("Private key is unencrypted") from err raise CommandExecutionError( "Could not load PEM-encoded private key" ) from err # DER try: pk = serialization.load_der_private_key(pk, password=passphrase) if get_encoding: return pk, "der", None return pk except ValueError as err: if "Bad decrypt" in str(err): raise SaltInvocationError("Bad decrypt - is the password correct?") from err except TypeError as err: if "private key is encrypted" in str(err): raise SaltInvocationError( "Private key is encrypted. Please provide a password." ) from err # PKCS12 try: # v36+ - there is pkcs12.load_key_and_certificates in 2.5+ @TODO? loaded = pkcs12.load_pkcs12(pk, password=passphrase) if not loaded.key: raise CommandExecutionError( "PKCS12-encoded blob does not contain a private key." ) if get_encoding: return loaded.key, "pkcs12", loaded return loaded.key except ValueError as err: if "Bad decrypt" in str(err): raise SaltInvocationError("Bad decrypt - is the password correct?") from err except TypeError as err: if "private key is encrypted" in str(err): raise SaltInvocationError( "Private key is encrypted. Please provide a password." ) from err except AttributeError: pass # nothing worked raise SaltInvocationError( "Could not deserialize binary data, neither as DER nor PKCS#12." ) def load_pubkey(pk, get_encoding=False): """ Return a public key instance from * a class instance * a file path on the local system * a string (PEM) * bytes (hex, base64, raw) Valid encodings are PEM and DER. """ if hasattr(pk, "public_bytes"): if isinstance( pk, ( rsa.RSAPublicKey, ec.EllipticCurvePublicKey, ed25519.Ed25519PublicKey, ed448.Ed448PublicKey, ), ): return pk raise SaltInvocationError( f"Passed object is not a public key, but {pk.__class__.__name__}" ) pk = load_file_or_bytes(pk) if PEM_BEGIN in pk: try: return serialization.load_pem_public_key(pk) except ValueError as err: raise CommandExecutionError( "Could not load PEM-encoded public key." ) from err try: return serialization.load_der_public_key(pk) except ValueError as err: raise CommandExecutionError("Could not load DER-encoded public key.") from err def load_cert(cert, passphrase=None, load_chain=False, get_encoding=False): """ Return a certificate instance from * a class instance * a file path on the local system * a string (PEM) * bytes (hex, base64, raw) Valid encodings are PEM, DER, PKCS7 (as PEM and DER) and PKCS12. """ if isinstance(cert, cx509.Certificate): if load_chain: return cert, [] if get_encoding: return cert, None, None return cert cert = load_file_or_bytes(cert) # PEM or PKCS7 in PEM if PEM_BEGIN in cert: pems = split_pems(cert) if b"-----BEGIN PKCS7" not in pems[0]: try: loaded = cx509.load_pem_x509_certificate(pems.pop(0)) if load_chain or get_encoding: chain = [] for pem in pems: try: chain.append(cx509.load_pem_x509_certificate(pem)) except ValueError: pass if load_chain: return loaded, chain if get_encoding: return loaded, "pem", chain, None return loaded except (ValueError, IndexError) as err: raise CommandExecutionError( "Could not load PEM-encoded certificate." ) from err else: try: loaded = pkcs7.load_pem_pkcs7_certificates(pems[0]) if load_chain: return loaded.pop(0), loaded if get_encoding: return loaded.pop(0), "pkcs7_pem", loaded, None return loaded.pop(0) except ValueError as err: raise CommandExecutionError( "Could not load PEM-encoded PKCS#7 blob" ) from err # DER try: loaded = cx509.load_der_x509_certificate(cert) if get_encoding: return loaded, "der", None, None if load_chain: return loaded, [] return loaded except ValueError: pass # PKCS12 try: if passphrase is not None and not isinstance(passphrase, bytes): passphrase = passphrase.encode() # v36+ loaded = pkcs12.load_pkcs12(cert, passphrase) if load_chain or get_encoding: chain = [x.certificate for x in loaded.additional_certs] if load_chain: return loaded.cert.certificate, chain if get_encoding: return loaded.cert.certificate, "pkcs12", chain, loaded return loaded.cert.certificate except (AttributeError, ValueError): pass # PKCS7 try: # v37+ loaded = pkcs7.load_der_pkcs7_certificates(cert) if load_chain: return loaded.pop(0), loaded if get_encoding: return loaded.pop(0), "pkcs7_der", loaded, None return loaded[0] except ValueError: pass # nothing worked raise SaltInvocationError( "Could not deserialize binary data, neither as DER nor PKCS#7, PKCS#12." ) def load_crl(crl, get_encoding=False): """ Return a CRL instance from * a class instance * a file path on the local system * a string (PEM) * bytes (hex, base64, raw) Valid encodings are PEM and DER. """ if isinstance(crl, cx509.CertificateRevocationList): if get_encoding: return crl, None, None return crl crl = load_file_or_bytes(crl) if PEM_BEGIN in crl: try: loaded = cx509.load_pem_x509_crl(crl) if get_encoding: return loaded, "pem" return loaded except ValueError as err: raise SaltInvocationError( "Could not load PEM-encoded certificate revocation list." ) from err try: loaded = cx509.load_der_x509_crl(crl) if get_encoding: return loaded, "der" return loaded except ValueError as err: raise SaltInvocationError( "Could not load DER-encoded certificate revocation list." ) from err def load_csr(csr, get_encoding=False): """ Return a CSR instance from * a class instance * a file path on the local system * a string (PEM) * bytes (hex, base64, raw) Valid encodings are PEM and DER. """ if isinstance(csr, cx509.CertificateSigningRequest): if get_encoding: return csr, None, None return csr csr = load_file_or_bytes(csr) if PEM_BEGIN in csr: try: loaded = cx509.load_pem_x509_csr(csr) if get_encoding: return loaded, "pem" return loaded except ValueError as err: raise SaltInvocationError( "Could not load PEM-encoded certificate signing request." ) from err try: loaded = cx509.load_der_x509_csr(csr) if get_encoding: return loaded, "der" return loaded except ValueError as err: raise SaltInvocationError( "Could not load DER-encoded certificate signing request." ) from err def verify_signature(cert, pubkey): """ Verifies that the signature on a certificate was made by a public key. This functionality is currently not exposed by cryptography since it does not imply the certificate chain is valid. """ key_type = get_key_type(pubkey) if key_type == KEY_TYPE.RSA: try: # SignatureAlgorithmOID is not present in older versions, # otherwise cx509.SignatureAlgorithmOID.RSASSA_PSS could be used if cert.signature_algorithm_oid.dotted_string == "1.2.840.113549.1.1.10": pubkey.verify( cert.signature, cert.tbs_certificate_bytes, padding.PSS( padding.MGF1(cert.signature_hash_algorithm), padding.PSS.AUTO ), cert.signature_hash_algorithm, ) else: pubkey.verify( cert.signature, cert.tbs_certificate_bytes, padding.PKCS1v15(), cert.signature_hash_algorithm, ) return True except InvalidSignature: return False if key_type == KEY_TYPE.EC: try: pubkey.verify( cert.signature, cert.tbs_certificate_bytes, ec.ECDSA(cert.signature_hash_algorithm), ) return True except InvalidSignature: return False if key_type in [KEY_TYPE.ED25519, KEY_TYPE.ED448]: try: pubkey.verify(cert.signature, cert.tbs_certificate_bytes) return True except InvalidSignature: return False raise SaltInvocationError( "Invalid public key type, can only process rsa, ec, ed25519, ed448" ) def isfile(path): """ A wrapper around os.path.isfile that ignores ValueError exceptions which can be raised if the input to isfile is too long. """ try: return os.path.isfile(path) except (TypeError, ValueError): pass return False def split_pems(pems): """ Returns a list of PEM strings from a possibly concatenated one """ pems = salt.utils.stringutils.to_bytes(pems) splits = [] cur = [] for line in pems.splitlines(True): if not line.strip(): continue if line.startswith(PEM_BEGIN): cur = [line] continue cur.append(line) if line.startswith(PEM_END): splits.append(b"".join(cur)) cur = [] return splits def load_file_or_bytes(fob): """ Tries to load a reference and return its bytes. Can be a file path on the local system, a string and bytes (hex/base64-encoded, raw) """ if isfile(fob): with salt.utils.files.fopen(fob, "rb") as f: fob = f.read() if isinstance(fob, str): if fob.startswith("b64:"): fob = base64.b64decode(fob[4:]) elif PEM_BEGIN.decode() in fob: fob = fob.encode() else: try: fob = bytes.fromhex(fob) except ValueError: try: fob = base64.b64decode(fob) except ValueError: pass if not isinstance(fob, bytes): raise SaltInvocationError( "Could not load provided source. You need to pass an existing file, " "(PEM|hex|base64)-encoded string or raw bytes." ) return fob def _create_extension(name, val, subject_pubkey=None, ca_crt=None, ca_pub=None): if name not in EXTENSION_BUILDERS: raise CommandExecutionError(f"Unknown extension {name}") return EXTENSION_BUILDERS[name]( val, subject_pubkey=subject_pubkey, ca_crt=ca_crt, ca_pub=ca_pub ) def _create_basic_constraints(val, **kwargs): try: critical = val.get("critical", False) except AttributeError: critical = False if isinstance(val, str): try: val, critical = _deserialize_openssl_confstring(val.lower()) val["ca"] = val["ca"] == "true" if "pathlen" in val: val["pathlen"] = int(val["pathlen"]) except (KeyError, ValueError) as err: raise SaltInvocationError( f"Invalid configuration for basicContraints: {err}" ) from err try: return ( cx509.BasicConstraints(val["ca"], val.get("pathlen")), critical, ) except KeyError as err: raise SaltInvocationError( f"Undefined required key for basicConstraints: {err}" ) from err except (TypeError, ValueError) as err: raise SaltInvocationError(err) from err def _create_key_usage(val, **kwargs): critical = "critical" in val args = { "digital_signature": "digitalSignature" in val, "content_commitment": "nonRepudiation" in val, "key_encipherment": "keyEncipherment" in val, "data_encipherment": "dataEncipherment" in val, "key_agreement": "keyAgreement" in val, "key_cert_sign": "keyCertSign" in val, "crl_sign": "cRLSign" in val, "encipher_only": "encipherOnly" in val, "decipher_only": "decipherOnly" in val, } try: return cx509.KeyUsage(**args), critical except ValueError as err: raise SaltInvocationError(err) from err def _create_extended_key_usage(val, **kwargs): critical = "critical" in val if isinstance(val, str): val, critical = _deserialize_openssl_confstring(val) val = list(val) if not isinstance(val, list): val = [val] usages = [] for usage in val: if usage == "critical": continue usages.append(EXTENDED_KEY_USAGE_OID.get(usage) or _get_oid(str(usage))) return cx509.ExtendedKeyUsage(usages), critical def _create_subject_key_identifier(val, subject_pubkey, **kwargs): if "critical" in val: raise SaltInvocationError("subjectKeyIdentifier must be marked as non-critical") if val == "hash": if not subject_pubkey: raise RuntimeError( "Cannot calculate digest for subjectKeyIdentifier: missing pubkey" ) try: return ( cx509.SubjectKeyIdentifier.from_public_key(subject_pubkey), False, ) except AttributeError as err: raise RuntimeError( "subjectKeyIdentifier: subject_pubkey was not a pubkey" ) from err if isinstance(val, str): try: val = bytes.fromhex(val.replace(":", "")) except ValueError as err: raise SaltInvocationError("Value must be precomputed hash") from err if not isinstance(val, bytes): raise SaltInvocationError("Value must be a (hex-)digest or pubkey") # this must be marked as non-critical return cx509.SubjectKeyIdentifier(val), False def _create_authority_key_identifier(val, ca_crt, ca_pub, **kwargs): if "critical" in val: raise SaltInvocationError( "authorityKeyIdentifier must be marked as non-critical" ) if not (ca_crt or ca_pub): raise RuntimeError( "Need CA certificate or CA pubkey to calculate authorityKeyIdentifier" ) if isinstance(val, str): val, _ = _deserialize_openssl_confstring(val) args = { "key_identifier": None, "authority_cert_issuer": None, "authority_cert_serial_number": None, } if "keyid" in val: if ca_crt: try: args["key_identifier"] = ca_crt.extensions.get_extension_for_class( cx509.SubjectKeyIdentifier ).value.digest except cx509.ExtensionNotFound: args["key_identifier"] = ( cx509.AuthorityKeyIdentifier.from_issuer_public_key( ca_crt.public_key() ).key_identifier ) except Exception: # pylint: disable=broad-except pass if not args["key_identifier"] and ca_pub: # this should happen for self-signed certificates try: args["key_identifier"] = ( cx509.AuthorityKeyIdentifier.from_issuer_public_key( ca_pub ).key_identifier ) except Exception: # pylint: disable=broad-except pass if val["keyid"] == "always" and args["key_identifier"] is None: raise CommandExecutionError( "Could not retrieve authorityKeyIdentifier keyid, but it was set to always" ) if val.get("issuer") == "always" or ( "issuer" in val and args["key_identifier"] is None ): try: args["authority_cert_issuer"] = [cx509.DirectoryName(ca_crt.issuer)] args["authority_cert_serial_number"] = ca_crt.serial_number except (AttributeError, ValueError) as err: # this will not work for self-signed certificates currently args["authority_cert_issuer"] = args["authority_cert_serial_number"] = None if val["issuer"] == "always": raise CommandExecutionError( "Could not add authority_cert_issuer and " "authority_cert_serial_number, but was set to always." ) from err if not args: raise SaltInvocationError("authorityKeyIdentifier cannot be empty.") # this must be marked as non-critical return cx509.AuthorityKeyIdentifier(**args), False def _create_issuer_alt_name(val, ca_crt, **kwargs): parsed, critical = _parse_issuer_general_name(val, ca_crt) return cx509.IssuerAlternativeName(parsed), critical def _create_certificate_issuer(val, ca_crt, **kwargs): parsed, critical = _parse_issuer_general_name(val, ca_crt) return cx509.CertificateIssuer(parsed), critical def _parse_issuer_general_name(val, ca_crt): critical = "critical" in val if isinstance(val, list): list_ = [] for x in val: if isinstance(x, str) and ":" in x: k, v = x.split(":", maxsplit=1) list_.append((k.strip(), v.strip())) elif isinstance(x, dict): list_.extend(list(x.items())) # since this is accessed twice, the generator needs to be cast to a tuple val = tuple(list_) elif isinstance(val, str): val, critical = _deserialize_openssl_confstring(val, multiple=True) val = tuple(val) parsed = [] if any(x == ("issuer", "copy") for x in val): if not ca_crt: raise RuntimeError("Need CA certificate to copy to issuerAltName") try: # eeh, there's no public API to get the list of general names parsed.extend( copy.deepcopy( ca_crt.extensions.get_extension_for_class( cx509.SubjectAlternativeName )._general_names._general_names ) ) val = tuple(x for x in val if x[0] != "issuer") except cx509.ExtensionNotFound as err: raise CommandExecutionError(err) from err except AttributeError as err: raise CommandExecutionError( "It seems your version of cryptography does not have an " "internal API that the issuer:copy functionality relies on" ) from err parsed.extend(_parse_general_names(val)) return parsed, critical def _create_authority_info_access(val, **kwargs): if isinstance(val, str): val = (x.strip().split(";") for x in val.split(",") if x.strip() != "critical") elif isinstance(val, dict): val = ((k, v) for k, v in val.items() if k != "critical") elif isinstance(val, list): val = ((k, v) for x in val for k, v in x.items() if x != "critical") parsed = [] for oid, general_name in val: try: oid = ACCESS_OID.get(oid) or _get_oid(str(oid)) except CommandExecutionError as err: raise CommandExecutionError(f"Unknown access OID: {oid}") from err general_name = _get_gn(general_name) parsed.append(cx509.AccessDescription(oid, general_name)) return cx509.AuthorityInformationAccess(parsed), False # always noncritical def _create_subject_alt_name(val, **kwargs): # Note: subjectAltName must be marked as critical if subject is empty. # This is not checked. critical = "critical" in val if isinstance(val, list): list_ = [] for x in val: if isinstance(x, str) and ":" in x: k, v = x.split(":", maxsplit=1) list_.append((k.strip(), v.strip())) elif isinstance(x, dict): list_.extend(list(x.items())) val = tuple(list_) elif isinstance(val, str): val, critical = _deserialize_openssl_confstring(val, multiple=True) parsed = _parse_general_names(val) return cx509.SubjectAlternativeName(parsed), critical def _create_crl_distribution_points(val, **kwargs): parsed, critical = _parse_distribution_points(val) return cx509.CRLDistributionPoints(parsed), critical def _create_freshest_crl(val, **kwargs): parsed, _ = _parse_distribution_points(val) return cx509.FreshestCRL(parsed), False # must be non-critical def _parse_distribution_points(val): critical = "critical" in val if isinstance(val, str): val, critical = _deserialize_openssl_confstring(val, multiple=True) elif isinstance(val, list): list_ = [] for x in val: if isinstance(x, str) and ":" in x: k, v = x.split(":", maxsplit=1) list_.append((k.strip(), v.strip())) elif x != "critical": list_.append(x) val = tuple(list_) parsed = [] for dpoint in val: fullname = relativename = crlissuer = reasons = None if isinstance(dpoint, dict): fullname = dpoint.get("fullname") relativename = dpoint.get("relativename") crlissuer = dpoint.get("crlissuer") reasons = dpoint.get("reasons") if fullname: if not isinstance(fullname, list): fullname = [fullname] fullname = (x.split(":", maxsplit=1) for x in fullname) if relativename: relativename = _get_rdn(relativename) if crlissuer: if not isinstance(crlissuer, list): crlissuer = [crlissuer] crlissuer = _parse_general_names( x.split(":", maxsplit=1) for x in crlissuer ) if reasons: try: reasons = frozenset(cx509.ReasonFlags(x) for x in reasons) except ValueError as err: raise SaltInvocationError(err) from err else: fullname = (dpoint,) if fullname: fullname = _parse_general_names(fullname) try: parsed.append( cx509.DistributionPoint( full_name=fullname, relative_name=relativename, reasons=reasons, crl_issuer=crlissuer, ) ) except (ValueError, TypeError) as err: raise SaltInvocationError(err) from err return parsed, critical def _create_issuing_distribution_point(val, **kwargs): if not isinstance(val, dict): raise SaltInvocationError("issuingDistributionPoint must be a dictionary") critical = val.get("critical", False) onlyuser = val.get("onlyuser", False) onlyca = val.get("onlyCA", False) onlyaa = val.get("onlyAA", False) indirectcrl = val.get("indirectCRL", False) fullname = val.get("fullname") relativename = val.get("relativename") onlysomereasons = val.get("onlysomereasons") if fullname: if not isinstance(fullname, list): fullname = [fullname] fullname = (x.split(":", maxsplit=1) for x in fullname) fullname = _parse_general_names(fullname) if relativename: relativename = _get_rdn(relativename) if onlysomereasons: try: onlysomereasons = frozenset(cx509.ReasonFlags(x) for x in onlysomereasons) except ValueError as err: raise SaltInvocationError(err) from err try: return ( cx509.IssuingDistributionPoint( full_name=fullname, relative_name=relativename, only_contains_user_certs=onlyuser, only_contains_ca_certs=onlyca, only_some_reasons=onlysomereasons, indirect_crl=indirectcrl, only_contains_attribute_certs=onlyaa, ), critical, ) except (ValueError, TypeError) as err: raise SaltInvocationError(err) from err def _create_certificate_policies(val, **kwargs): if isinstance(val, str): try: critical = val.startswith("critical") policy_identifiers = ( _get_oid(x.strip()) for x in val.split(",") if x.strip() != "critical" ) policy_information = [ cx509.PolicyInformation(policy_identifier=p, policy_qualifiers=None) for p in policy_identifiers ] return cx509.CertificatePolicies(policy_information), critical except CommandExecutionError as err: raise SaltInvocationError( "certificatePolicies defined as string must be a " "comma-separated list of OID." ) from err critical = val.get("critical", False) parsed = [] for polid, qualifiers in val.items(): if polid == "critical": continue parsed_qualifiers = [] for qual in qualifiers: if isinstance(qual, str): # pointer to the practice statement published by the certificate authority parsed_qualifiers.append(qual) continue notice = None organization = qual.get("organization") notice_numbers = qual.get("noticeNumbers") text = qual.get("text") if notice_numbers: try: notice = cx509.NoticeReference( organization=organization, notice_numbers=notice_numbers ) except TypeError as err: raise CommandExecutionError(err) from err parsed_qualifiers.append( cx509.UserNotice(notice_reference=notice, explicit_text=text) ) parsed.append( cx509.PolicyInformation( policy_identifier=_get_oid(polid), policy_qualifiers=parsed_qualifiers, ) ) return cx509.CertificatePolicies(parsed), critical def _create_policy_constraints(val, **kwargs): critical = "critical" in val if isinstance(val, str): val, critical = _deserialize_openssl_confstring(val) args = { "require_explicit_policy": ( int(val["requireExplicitPolicy"]) if "requireExplicitPolicy" in val else None ), "inhibit_policy_mapping": ( int(val["inhibitPolicyMapping"]) if "inhibitPolicyMapping" in val else None ), } try: # not sure why pylint complains about this line having kwargs from keyUsage # pylint: disable=unexpected-keyword-arg return cx509.PolicyConstraints(**args), critical except (TypeError, ValueError) as err: raise SaltInvocationError(err) from err def _create_inhibit_any_policy(val, **kwargs): critical = "critical" in val if not isinstance(val, int) else False if isinstance(val, str): val, critical = _deserialize_openssl_confstring(val) val = int(next(iter(val))) try: return ( cx509.InhibitAnyPolicy(val if isinstance(val, int) else val["value"]), critical, ) except KeyError as err: raise SaltInvocationError( f"Undefined required key for inhibitAnyPolicy: {err}" ) from err except (TypeError, ValueError) as err: raise SaltInvocationError(err) from err def _create_name_constraints(val, **kwargs): critical = "critical" in val if isinstance(val, dict): parsed = {} for scope, constraints in val.items(): if scope not in ("permitted", "excluded"): continue list_ = [] for x in constraints: if isinstance(x, str) and ":" in x: k, v = x.split(":", maxsplit=1) list_.append((k.strip(), v.strip())) elif x != "critical": list_.append(x) parsed[scope] = tuple(list_) val = parsed elif isinstance(val, str): items = tuple(x.strip().split(";") for x in val.split(",")) val = { "permitted": [ x[1].split(":", maxsplit=1) for x in items if x[0] == "permitted" ], "excluded": [ x[1].split(":", maxsplit=1) for x in items if x[0] == "excluded" ], } args = { "permitted_subtrees": ( _parse_general_names(val["permitted"]) if "permitted" in val else None ), "excluded_subtrees": ( _parse_general_names(val["excluded"]) if "excluded" in val else None ), } if not any(args.values()): raise SaltInvocationError("nameConstraints needs at least one definition") return cx509.NameConstraints(**args), critical def _create_no_check(val, **kwargs): return cx509.OCSPNoCheck(), "critical" in str(val) def _create_tlsfeature(val, **kwargs): if isinstance(val, str): val = [x.strip() for x in val.split(",")] critical = "critical" in val try: types = [getattr(cx509.TLSFeatureType, x) for x in val if x != "critical"] except ValueError as err: raise SaltInvocationError(err) from err return cx509.TLSFeature(types), critical def _create_ns_comment(val, **kwargs): raise SaltInvocationError("nsComment is currently not implemented.") def _create_ns_cert_type(val, **kwargs): raise SaltInvocationError("nsCertType is currently not implemented.") def _create_crl_number(val, **kwargs): try: return cx509.CRLNumber(int(val)), False except ValueError as err: raise SaltInvocationError( "cRLNumber must be an integer and must be marked as non-critical" ) from err def _create_delta_crl_indicator(val, **kwargs): critical = "critical" in str(val) val = re.findall(r"[\d]+", str(val)) if len(val) != 1: raise SaltInvocationError( "deltaCRLIndicator must contain a single integer pointing to a cRLNumber" ) return cx509.DeltaCRLIndicator(int(val[0])), critical def _create_crl_reason(val, **kwargs): critical = False if isinstance(val, str): val, critical = _deserialize_openssl_confstring(val) else: if "critical" in val: critical = True val = [x for x in val if x != "critical"] try: return cx509.CRLReason(cx509.ReasonFlags(next(iter(val)))), critical except ValueError as err: raise SaltInvocationError(str(err)) from err def _create_invalidity_date(val, **kwargs): if not isinstance(val, str): raise SaltInvocationError("invalidityDate must be a string") critical = val.startswith("critical") if critical: val = val.split(" ", maxsplit=1)[1] try: # InvalidityDate deals in naive datetime objects only currently return ( cx509.InvalidityDate(datetime.strptime(val, TIME_FMT)), critical, ) except ValueError as err: raise SaltInvocationError(str(err)) from err # Map canonical name of extension to builder function. # This is done globally to avoid having to instantiate this # over and over. EXTENSION_BUILDERS = immutabletypes.freeze( { "basicConstraints": _create_basic_constraints, "keyUsage": _create_key_usage, "extendedKeyUsage": _create_extended_key_usage, "subjectKeyIdentifier": _create_subject_key_identifier, "authorityKeyIdentifier": _create_authority_key_identifier, "issuerAltName": _create_issuer_alt_name, "certificateIssuer": _create_certificate_issuer, "authorityInfoAccess": _create_authority_info_access, "subjectAltName": _create_subject_alt_name, "crlDistributionPoints": _create_crl_distribution_points, "freshestCRL": _create_freshest_crl, "issuingDistributionPoint": _create_issuing_distribution_point, "certificatePolicies": _create_certificate_policies, "policyConstraints": _create_policy_constraints, "inhibitAnyPolicy": _create_inhibit_any_policy, "nameConstraints": _create_name_constraints, "noCheck": _create_no_check, "tlsfeature": _create_tlsfeature, "nsComment": _create_ns_comment, "nsCertType": _create_ns_cert_type, "cRLNumber": _create_crl_number, "deltaCRLIndicator": _create_delta_crl_indicator, "CRLReason": _create_crl_reason, "invalidityDate": _create_invalidity_date, } ) def _deserialize_openssl_confstring(conf, multiple=False): critical = conf.startswith("critical") if critical: conf = conf[8:].strip(",").strip() items = (x.strip() for x in conf.split(",")) if multiple: return ( (k.strip(), v.strip()) for k, v in ( x.split(":", maxsplit=1) if ":" in x else (x, "__present__") for x in items ) ), critical return { k.strip(): v.strip() for k, v in ( x.split(":", maxsplit=1) if ":" in x else (x, "__present__") for x in items ) }, critical def _parse_general_names(val): def idna_encode(val, allow_leading_dot=False, allow_wildcard=False): # A leading dot is allowed in some values (nameConstraints). # idna complains about it not being a valid domain name try: has_dot = val.startswith(".") except AttributeError: raise SaltInvocationError( f"Expected string value, got {type(val).__name__}: `{val}`" ) if has_dot: if not allow_leading_dot: raise CommandExecutionError( "Leading dots are not allowed in this context" ) val = val.lstrip(".") has_wildcard = val.startswith("*.") if has_wildcard: if not allow_wildcard: raise CommandExecutionError("Wildcards are not allowed in this context") if has_dot: raise CommandExecutionError( "Wildcards and leading dots cannot be present together" ) val = val[2:] if val.startswith("."): raise CommandExecutionError("Empty label") if HAS_IDNA: try: ret = idna.encode(val).decode() except idna.IDNAError as err: raise CommandExecutionError(str(err)) from err else: if not val: raise CommandExecutionError("Empty domain") try: val.encode(encoding="ascii") except UnicodeEncodeError as err: raise CommandExecutionError( "Cannot encode non-ASCII strings to internationalized domain " "name format, missing library: idna" ) from err for elem in val.split("."): if not elem: raise CommandExecutionError("Empty Label") invalid = re.search(r"[^A-Za-z\d\-\.]", elem) if invalid is not None: raise CommandExecutionError( f"Codepoint U+00{hex(ord(invalid.group()))[2:]} at position {invalid.end()} of '{val}' not allowed" ) ret = val if has_dot: return f".{ret}" if has_wildcard: return f"*.{ret}" return ret valid_types = { "email": cx509.general_name.RFC822Name, "uri": cx509.general_name.UniformResourceIdentifier, "dns": cx509.general_name.DNSName, "rid": cx509.general_name.RegisteredID, "ip": cx509.general_name.IPAddress, "dirname": cx509.general_name.DirectoryName, # othername currently not implemented } parsed = [] for typ, v in val: typ = typ.lower() if typ == "dirname": v = _get_dn(v) elif typ == "rid": v = _get_oid(v) elif typ == "ip": try: v = ipaddress.ip_address(v) except ValueError: try: v = ipaddress.ip_network(v) except ValueError as err: raise CommandExecutionError( f"Provided value {v} does not seem to be an IP address or network range." ) from err elif typ == "email": splits = v.rsplit("@", maxsplit=1) if len(splits) > 1: user, domain = splits domain = idna_encode(domain) v = "@".join((user, domain)) else: # nameConstraints v = idna_encode(splits[0], allow_leading_dot=True) elif typ == "uri": url = urlparse(v) if url.netloc: domain = idna_encode(url.netloc) v = urlunparse( (url.scheme, domain, url.path, url.params, url.query, url.fragment) ) elif typ == "dns": v = idna_encode(v, allow_leading_dot=True, allow_wildcard=True) elif typ == "othername": raise SaltInvocationError("otherName is currently not implemented") if typ in valid_types: try: parsed.append(valid_types[typ](v)) continue except (ValueError, TypeError) as err: raise CommandExecutionError(err) from err raise CommandExecutionError(f"GeneralName type {typ} is invalid") return parsed def _get_oid(oid): if not str(oid).startswith(("0", "1", "2")) or str(oid).strip("0123456789."): raise CommandExecutionError(f"Invalid oid: {oid}") return cx509.ObjectIdentifier(oid) def _get_rdn(rdn): try: rdns = cx509.Name.from_rfc4514_string(rdn).rdns if len(rdns) > 1: raise SaltInvocationError( "Specified string is not a Relative Distinguished Name, " "but a Distinguished Name." ) return rdns[0] except ValueError as err: raise CommandExecutionError(f"Failed parsing rdn string: {rdn}") from err except AttributeError as err: raise CommandExecutionError( "At least cryptography v37 is required for parsing RFC4514 strings." ) from err def _get_gn(gn): return _parse_general_names((gn.split(":", maxsplit=1),))[0] def _get_serial_number(sn=None): if sn is None: return cx509.random_serial_number() if isinstance(sn, int): return sn try: sn = bytes.fromhex(sn.replace(":", "")) except (AttributeError, TypeError, ValueError): pass if isinstance(sn, bytes): return int.from_bytes(sn, "big") raise CommandExecutionError(f"Could not parse serial number {sn}") def _get_dn(dn): if isinstance(dn, str): try: parsed = cx509.Name.from_rfc4514_string(dn) if CRYPTOGRAPHY_VERSION[0] == 37: return cx509.Name(parsed.rdns[::-1]) return parsed except ValueError as err: raise CommandExecutionError( "Failed parsing rfc4514 dirName string" ) from err except AttributeError as err: raise CommandExecutionError( "At least cryptography v37 is required for parsing RFC4514 strings." ) from err elif isinstance(dn, list): return cx509.Name([_get_rdn(x) for x in dn]) elif isinstance(dn, dict): parsed = [] for name, oid in NAME_ATTRS_OID.items(): if name in dn: parsed.append(cx509.NameAttribute(oid, dn[name])) return cx509.Name(parsed) raise SaltInvocationError("Need string, list or dict to parse distinguished names") def pretty_hex(hex_str): """ Nicely formats hex strings """ if isinstance(hex_str, bytes): hex_str = hex_str.hex() if len(hex_str) % 2 != 0: hex_str = "0" + hex_str return ":".join([hex_str[i : i + 2] for i in range(0, len(hex_str), 2)]).upper() def dec2hex(decval): """ Converts decimal values to nicely formatted hex strings """ return pretty_hex(f"{decval:X}") def render_gn(gn): """ Returns a valid OpenSSL string for a GeneralName instance """ if isinstance(gn, cx509.DNSName): return f"DNS:{gn.value}" if isinstance(gn, cx509.DirectoryName): return f"dirName:{gn.value.rfc4514_string()}" if isinstance(gn, cx509.IPAddress): return f"IP:{gn.value.exploded}" if isinstance(gn, cx509.RFC822Name): return f"mail:{gn.value}" if isinstance(gn, cx509.RegisteredID): return f"RID:{gn.value.dotted_string}" if isinstance(gn, cx509.UniformResourceIdentifier): return f"URI:{gn.value}" return str(gn) def render_extension(ext): """ Render an Extension instance to a dict for informational purposes """ ret = {"critical": ext.critical} typ = type(ext.value) if typ in EXTENSION_RENDERERS: ret.update(EXTENSION_RENDERERS[typ](ext)) else: ret["value"] = str(ext.value) return ret def _render_basic_constraints(ext): return {"ca": ext.value.ca, "pathlen": ext.value.path_length} def _render_key_usage(ext): return { "cRLSign": ext.value.crl_sign, "dataEncipherment": ext.value.data_encipherment, "decipherOnly": ext.value.decipher_only if ext.value.key_agreement else False, "digitalSignature": ext.value.digital_signature, "encipherOnly": ext.value.encipher_only if ext.value.key_agreement else False, "keyAgreement": ext.value.key_agreement, "keyCertSign": ext.value.key_cert_sign, "keyEncipherment": ext.value.key_encipherment, "nonRepudiation": ext.value.content_commitment, } def _render_extended_key_usage(ext): # this does not account for unnamed OIDs try: usages = [ x._name if x._name != "Unknown OID" else x.dotted_string for x in ext.value._usages or [] ] except AttributeError: # best effort in case x._name becomes undefined at some point usages = re.findall( r"\<ObjectIdentifier\(oid=[\d\.]+, name=([\w]+)\)\>", str(ext.value) ) usages = [x[1] if "Unknown OID" != x[1] else x[0] for x in usages] return {"value": usages} def _render_subject_key_identifier(ext): return {"value": pretty_hex(ext.value.digest)} def _render_authority_key_identifier(ext): return { "keyid": ( pretty_hex(ext.value.key_identifier) if ext.value.key_identifier else None ), "issuer": [render_gn(x) for x in ext.value.authority_cert_issuer or []] or None, "issuer_sn": ( dec2hex(ext.value.authority_cert_serial_number) if ext.value.authority_cert_serial_number else None ), } def _render_general_names(ext): try: rendered = [render_gn(x) for x in ext.value._general_names._general_names or []] except AttributeError: # best effort in case ext.value._general_names._general_names # becomes undefined at some point prefixes = { "DNSName": "dns", "RFC822Name": "email", "UniformResourceIdentifier": "url", "RegisteredID": "rid", "IPAddress": "ip", "DirectoryName": "dirName", } rendered = [ f"{prefixes[typ]}:{gn}" for typ, gn in re.findall( rf"\<({'|'.join(prefixes)})\(value='([^']+)'\)\>", str(ext.value), ) ] return {"value": rendered} def _render_authority_info_access(ext): rendered = [] try: for description in ext.value._descriptions: rendered.append( { ( description.access_method._name if description.access_method._name != "Unknown OID" else description.access_method.dotted_string ): render_gn(description.access_location.value) } ) except AttributeError: rendered = str(ext.value) return {"value": rendered} def _render_distribution_points(ext): dpoints = [] try: for dpoint in ext.value._distribution_points: dpoints.append( { "crlissuer": [render_gn(x) for x in dpoint.crl_issuer or []], "fullname": [render_gn(x) for x in dpoint.full_name or []], "reasons": list(sorted(x.value for x in dpoint.reasons or [])), "relativename": ( dpoint.relative_name.rfc4514_string() if dpoint.relative_name else None ), } ) except AttributeError: dpoints = str(ext.value) return {"value": dpoints} def _render_issuing_distribution_point(ext): return { "fullname": [render_gn(x) for x in ext.value.full_name or []], "onysomereasons": list( sorted(x.value for x in ext.value.only_some_reasons or []) ), "relativename": ( ext.value.relative_name.rfc4514_string() if ext.value.relative_name else None ), "onlyuser": ext.value.only_contains_user_certs, "onlyCA": ext.value.only_contains_ca_certs, "onlyAA": ext.value.only_contains_attribute_certs, "indirectCRL": ext.value.indirect_crl, } def _render_certificate_policies(ext): policies = [] try: for policy in ext.value._policies: polid = policy.policy_identifier._name if polid == "Unknown OID": polid = policy.policy_identifier.dotted_string qualifiers = [] for notice in policy.policy_qualifiers or []: if isinstance(notice, str): qualifiers.append({"practice_statement": notice}) continue organization = notice_numbers = None if notice.notice_reference: organization = notice.notice_reference.organization notice_numbers = notice.notice_reference.notice_numbers qualifiers.append( { "organizataion": organization, "notice_numbers": notice_numbers, "explicit_text": notice.explicit_text, } ) policies.append({polid: qualifiers}) except AttributeError: policies = str(ext.value) return {"value": policies} def _render_policy_constraints(ext): return { "inhibitPolicyMapping": ext.value.inhibit_policy_mapping, "requireExplicitPolicy": ext.value.require_explicit_policy, } def _render_inhibit_any_policy(ext): return {"value": ext.value.skip_certs} def _render_name_constraints(ext): return { "excluded": [render_gn(x) for x in ext.value.excluded_subtrees or []], "permitted": [render_gn(x) for x in ext.value.permitted_subtrees or []], } def _render_no_check(ext): return {"value": True} def _render_tlsfeature(ext): try: features = [x.name for x in ext.value._features] except AttributeError: features = [] if "status_request" in str(ext.value): features.append("status_request") if "status_request_v2" in str(ext.value): features.append("status_request_v2") return {"value": features} def _render_crl_number(ext): return {"value": ext.value.crl_number} def _render_crl_reason(ext): return {"value": ext.value.reason.value} def _render_invalidity_date(ext): return {"value": ext.value.invalidity_date.strftime(TIME_FMT)} EXTENSION_RENDERERS = immutabletypes.freeze( { cx509.BasicConstraints: _render_basic_constraints, cx509.KeyUsage: _render_key_usage, cx509.ExtendedKeyUsage: _render_extended_key_usage, cx509.SubjectKeyIdentifier: _render_subject_key_identifier, cx509.AuthorityKeyIdentifier: _render_authority_key_identifier, cx509.IssuerAlternativeName: _render_general_names, cx509.CertificateIssuer: _render_general_names, cx509.AuthorityInformationAccess: _render_authority_info_access, cx509.SubjectAlternativeName: _render_general_names, cx509.CRLDistributionPoints: _render_distribution_points, cx509.FreshestCRL: _render_distribution_points, cx509.IssuingDistributionPoint: _render_issuing_distribution_point, cx509.CertificatePolicies: _render_certificate_policies, cx509.PolicyConstraints: _render_policy_constraints, cx509.InhibitAnyPolicy: _render_inhibit_any_policy, cx509.NameConstraints: _render_name_constraints, cx509.OCSPNoCheck: _render_no_check, cx509.TLSFeature: _render_tlsfeature, cx509.CRLNumber: _render_crl_number, cx509.DeltaCRLIndicator: _render_crl_number, cx509.CRLReason: _render_crl_reason, cx509.InvalidityDate: _render_invalidity_date, } )