def _pyasn1_to_cryptography_othername(on): return crypto_x509.OtherName(_pyasn1_to_cryptography_oid(on['type-id']), bytes(on['value']))
def _decode_general_name(backend, gn): if gn.type == backend._lib.GEN_DNS: # Convert to bytes and then decode to utf8. We don't use # asn1_string_to_utf8 here because it doesn't properly convert # utf8 from ia5strings. data = _asn1_string_to_bytes(backend, gn.d.dNSName).decode("utf8") # We don't use the constructor for DNSName so we can bypass validation # This allows us to create DNSName objects that have unicode chars # when a certificate (against the RFC) contains them. return x509.DNSName._init_without_validation(data) elif gn.type == backend._lib.GEN_URI: # Convert to bytes and then decode to utf8. We don't use # asn1_string_to_utf8 here because it doesn't properly convert # utf8 from ia5strings. data = _asn1_string_to_bytes( backend, gn.d.uniformResourceIdentifier).decode("utf8") # We don't use the constructor for URI so we can bypass validation # This allows us to create URI objects that have unicode chars # when a certificate (against the RFC) contains them. return x509.UniformResourceIdentifier._init_without_validation(data) elif gn.type == backend._lib.GEN_RID: oid = _obj2txt(backend, gn.d.registeredID) return x509.RegisteredID(x509.ObjectIdentifier(oid)) elif gn.type == backend._lib.GEN_IPADD: data = _asn1_string_to_bytes(backend, gn.d.iPAddress) data_len = len(data) if data_len == 8 or data_len == 32: # This is an IPv4 or IPv6 Network and not a single IP. This # type of data appears in Name Constraints. Unfortunately, # ipaddress doesn't support packed bytes + netmask. Additionally, # IPv6Network can only handle CIDR rather than the full 16 byte # netmask. To handle this we convert the netmask to integer, then # find the first 0 bit, which will be the prefix. If another 1 # bit is present after that the netmask is invalid. base = ipaddress.ip_address(data[:data_len // 2]) netmask = ipaddress.ip_address(data[data_len // 2:]) bits = bin(int(netmask))[2:] prefix = bits.find("0") # If no 0 bits are found it is a /32 or /128 if prefix == -1: prefix = len(bits) if "1" in bits[prefix:]: raise ValueError("Invalid netmask") ip = ipaddress.ip_network(base.exploded + "/{}".format(prefix)) else: ip = ipaddress.ip_address(data) return x509.IPAddress(ip) elif gn.type == backend._lib.GEN_DIRNAME: return x509.DirectoryName( _decode_x509_name(backend, gn.d.directoryName)) elif gn.type == backend._lib.GEN_EMAIL: # Convert to bytes and then decode to utf8. We don't use # asn1_string_to_utf8 here because it doesn't properly convert # utf8 from ia5strings. data = _asn1_string_to_bytes(backend, gn.d.rfc822Name).decode("utf8") # We don't use the constructor for RFC822Name so we can bypass # validation. This allows us to create RFC822Name objects that have # unicode chars when a certificate (against the RFC) contains them. return x509.RFC822Name._init_without_validation(data) elif gn.type == backend._lib.GEN_OTHERNAME: type_id = _obj2txt(backend, gn.d.otherName.type_id) value = _asn1_to_der(backend, gn.d.otherName.value) return x509.OtherName(x509.ObjectIdentifier(type_id), value) else: # x400Address or ediPartyName raise x509.UnsupportedGeneralNameType( "{} is not a supported type".format( x509._GENERAL_NAMES.get(gn.type, gn.type)), gn.type, )
async def amain(url, service, template, altname, onbehalf, cn = None, pfx_file = None, pfx_password = None, enroll_cert = None, enroll_password = None): try: if pfx_file is None: pfx_file = 'cert_%s.pfx' % os.urandom(4).hex() if pfx_password is None: pfx_password = '******' print('[+] Parsing connection parameters...') su = SMBConnectionURL(url) ip = su.get_target().get_hostname_or_ip() if cn is None: cn = '%s@%s' % (su.username, su.domain) print('[*] Using CN: %s' % cn) print('[+] Generating RSA privat key...') key = rsa.generate_private_key(0x10001, 2048) print('[+] Building certificate request...') attributes = { "CertificateTemplate": template, } csr = x509.CertificateSigningRequestBuilder() csr = csr.subject_name( x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, cn), ] ) ) if altname: altname = core.UTF8String(altname).dump() csr = csr.add_extension( x509.SubjectAlternativeName( [ x509.OtherName(PRINCIPAL_NAME, altname), ] ), critical=False, ) csr = csr.sign(key, hashes.SHA256()) if onbehalf is not None: agent_key = None agent_cert = None with open(enroll_cert, 'rb') as f: agent_key, agent_cert, _ = pkcs12.load_key_and_certificates(f.read(), enroll_password) pkcs7builder = pkcs7.PKCS7SignatureBuilder().set_data(csr).add_signer(agent_key, agent_cert, hashes.SHA1()) csr = pkcs7builder.sign(Encoding.DER, options=[pkcs7.PKCS7Options.Binary]) else: csr = csr.public_bytes(Encoding.DER) print('[+] Connecting to EPM...') target, err = await EPM.create_target(ip, ICPRRPC().service_uuid, dc_ip = su.get_target().dc_ip, domain = su.get_target().domain) if err is not None: raise err print('[+] Connecting to ICRPR service...') gssapi = AuthenticatorBuilder.to_spnego_cred(su.get_credential(), target) auth = DCERPCAuth.from_smb_gssapi(gssapi) connection = DCERPC5Connection(auth, target) rpc, err = await ICPRRPC.from_rpcconnection(connection, perform_dummy=True) if err is not None: raise err logger.debug('DCE Connected!') print('[+] Requesting certificate from the service...') res, err = await rpc.request_certificate(service, csr, attributes) if err is not None: print('[-] Request failed!') raise err if res['encodedcert'] in [None, b'']: raise Exception('No certificate was returned from server!. Full message: %s' % res) print('[+] Got certificate!') cert = x509.load_der_x509_certificate(res['encodedcert']) print("[*] Cert subject: {}".format(cert.subject.rfc4514_string())) print("[*] Cert issuer: {}".format(cert.issuer.rfc4514_string())) print("[*] Cert Serial: {:X}".format(cert.serial_number)) try: ext = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE) for oid in ext.value: print("[*] Cert Extended Key Usage: {}".format(EKUS_NAMES.get(oid.dotted_string, oid.dotted_string))) except: print('[-] Could not verify extended key usage') try: ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME) for name in ext.value.get_values_for_type(x509.OtherName): if name.type_id == x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3"): print('[*] Certificate ALT NAME: %s' % core.UTF8String.load(name.value).native) break else: print('[-] Certificate doesnt have ALT NAME') except: print('[-] Certificate doesnt have ALT NAME') print('[+] Writing certificate to disk (file:"%s" pass: "******")...' % (pfx_file, pfx_password)) # Still waiting for the day oscrypto will have a pfx serializer :( # Until that we'd need to use cryptography with open(pfx_file, 'wb') as f: data = pkcs12.serialize_key_and_certificates( name=b"", key=key, cert=cert, cas=None, encryption_algorithm=BestAvailableEncryption(pfx_password.encode()) ) f.write(data) print('[+] Finished!') return True, None except Exception as e: traceback.print_exc() return False, e
def profile_kdc(builder, ca_nick, ca, warp=datetime.timedelta(days=0), dns_name=None, badusage=False): now = datetime.datetime.utcnow() + warp builder = builder.not_valid_before(now) builder = builder.not_valid_after(now + YEAR) crl_uri = u'file://{}.crl'.format(os.path.join(cert_dir, ca_nick)) builder = builder.add_extension( x509.ExtendedKeyUsage([x509.ObjectIdentifier('1.3.6.1.5.2.3.5')]), critical=False, ) name = { 'realm': realm, 'principalName': { 'name-type': 2, 'name-string': ['krbtgt', realm], }, } name = native_decoder.decode(name, asn1Spec=KRB5PrincipalName()) name = der_encoder.encode(name) names = [x509.OtherName(x509.ObjectIdentifier('1.3.6.1.5.2.2'), name)] if dns_name is not None: names += [x509.DNSName(dns_name)] builder = builder.add_extension( x509.SubjectAlternativeName(names), critical=False, ) builder = builder.add_extension( x509.CRLDistributionPoints([ x509.DistributionPoint( full_name=[x509.UniformResourceIdentifier(crl_uri)], relative_name=None, crl_issuer=None, reasons=None, ), ]), critical=False, ) if badusage: builder = builder.add_extension(x509.KeyUsage(digital_signature=False, content_commitment=False, key_encipherment=False, data_encipherment=True, key_agreement=True, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False), critical=False) return builder
def build_csr(self): if not self.private_key: self._gen_key() csr_builder = x509.CertificateSigningRequestBuilder() subject = [ x509.NameAttribute( NameOID.COMMON_NAME, self.common_name, ) ] if self.locality: subject.append( x509.NameAttribute(NameOID.LOCALITY_NAME, self.locality)) if self.province: subject.append( x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, self.province)) if self.country: subject.append( x509.NameAttribute(NameOID.COUNTRY_NAME, self.country)) if self.organization: subject.append( x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.organization)) if self.organizational_unit: if isinstance(self.organizational_unit, string_types): subject.append( x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, self.organizational_unit)) elif isinstance(self.organizational_unit, list): for u in self.organizational_unit: subject.append( x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u)) csr_builder = csr_builder.subject_name(x509.Name(subject)) alt_names = [] if self.ip_addresses: for ip in self.ip_addresses: alt_names.append(x509.IPAddress(ipaddress.IPv4Address(ip))) if self.san_dns: for ns in self.san_dns: alt_names.append(x509.DNSName(ns)) if self.email_addresses: for mail in self.email_addresses: alt_names.append(x509.RFC822Name(mail)) if self.uniform_resource_identifiers: for uri in self.uniform_resource_identifiers: alt_names.append(x509.UniformResourceIdentifier(uri)) if self.user_principal_names: for upn in self.user_principal_names: # Python cryptography library doesn't include # OID for UPN in any of the OID constants # See http://www.oid-info.com/get/1.3.6.1.4.1.311.20.2.3 # and https://docs.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-certificate-requirements-and-enumeration UPNOID = x509.ObjectIdentifier('1.3.6.1.4.1.311.20.2.3') # x509 library expects DER encoded string, so encode UPN into bytes # with ASN1 syntax for UTF8String, which is a type/length/value encoding # per https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-utf8string # Construct the array of bytes (note bytes are strings in python2) # by inserting the header consisting of the tag '0x0C' followed by the length of the upn if sys.version_info > (3, 0): # For python3, since we have native bytes available we'll convert the string into bytes. # However, for cases when this method is called with inputs extracted from existing certificates # & csrs, the input will already be encoded as bytes. So we'll check the input, if it is a # string we'll convert it into a bytes object then insert our header. Otherwise, we'll just # insert the header in the passed in bytes. if isinstance(upn, str): bupn = bytes(upn, 'utf-8') else: bupn = upn values = [12, len(upn)] header = bytes(values) data = header + bupn alt_names.append(x509.OtherName(UPNOID, data)) else: # For python2, since there is no native bytes method, we'll use the bytes method # in the future module to convert the string we constructed into bytes. bupn = ''.join(chr(x) for x in [12, len(upn)]) bupn = bupn + str(upn) alt_names.append( x509.OtherName(UPNOID, bytes(bupn, 'utf-8'))) csr_builder = csr_builder.add_extension( x509.SubjectAlternativeName(alt_names), critical=False) csr_builder = csr_builder.sign(self.private_key, hashes.SHA256(), default_backend()) self.csr = csr_builder.public_bytes( serialization.Encoding.PEM).decode() return
def _decode_general_name(backend, gn): if gn.type == backend._lib.GEN_DNS: data = _asn1_string_to_bytes(backend, gn.d.dNSName) if not data: decoded = u"" elif data.startswith(b"*."): # This is a wildcard name. We need to remove the leading wildcard, # IDNA decode, then re-add the wildcard. Wildcard characters should # always be left-most (RFC 2595 section 2.4). decoded = u"*." + idna.decode(data[2:]) else: # Not a wildcard, decode away. If the string has a * in it anywhere # invalid this will raise an InvalidCodePoint decoded = idna.decode(data) if data.startswith(b"."): # idna strips leading periods. Name constraints can have that # so we need to re-add it. Sigh. decoded = u"." + decoded return x509.DNSName(decoded) elif gn.type == backend._lib.GEN_URI: data = _asn1_string_to_ascii(backend, gn.d.uniformResourceIdentifier) parsed = urllib_parse.urlparse(data) if parsed.hostname: hostname = idna.decode(parsed.hostname) else: hostname = "" if parsed.port: netloc = hostname + u":" + six.text_type(parsed.port) else: netloc = hostname # Note that building a URL in this fashion means it should be # semantically indistinguishable from the original but is not # guaranteed to be exactly the same. uri = urllib_parse.urlunparse(( parsed.scheme, netloc, parsed.path, parsed.params, parsed.query, parsed.fragment )) return x509.UniformResourceIdentifier(uri) elif gn.type == backend._lib.GEN_RID: oid = _obj2txt(backend, gn.d.registeredID) return x509.RegisteredID(x509.ObjectIdentifier(oid)) elif gn.type == backend._lib.GEN_IPADD: data = _asn1_string_to_bytes(backend, gn.d.iPAddress) data_len = len(data) if data_len == 8 or data_len == 32: # This is an IPv4 or IPv6 Network and not a single IP. This # type of data appears in Name Constraints. Unfortunately, # ipaddress doesn't support packed bytes + netmask. Additionally, # IPv6Network can only handle CIDR rather than the full 16 byte # netmask. To handle this we convert the netmask to integer, then # find the first 0 bit, which will be the prefix. If another 1 # bit is present after that the netmask is invalid. base = ipaddress.ip_address(data[:data_len // 2]) netmask = ipaddress.ip_address(data[data_len // 2:]) bits = bin(int(netmask))[2:] prefix = bits.find('0') # If no 0 bits are found it is a /32 or /128 if prefix == -1: prefix = len(bits) if "1" in bits[prefix:]: raise ValueError("Invalid netmask") ip = ipaddress.ip_network(base.exploded + u"/{0}".format(prefix)) else: ip = ipaddress.ip_address(data) return x509.IPAddress(ip) elif gn.type == backend._lib.GEN_DIRNAME: return x509.DirectoryName( _decode_x509_name(backend, gn.d.directoryName) ) elif gn.type == backend._lib.GEN_EMAIL: data = _asn1_string_to_ascii(backend, gn.d.rfc822Name) name, address = parseaddr(data) parts = address.split(u"@") if name or not address: # parseaddr has found a name (e.g. Name <email>) or the entire # value is an empty string. raise ValueError("Invalid rfc822name value") elif len(parts) == 1: # Single label email name. This is valid for local delivery. No # IDNA decoding can be done since there is no domain component. return x509.RFC822Name(address) else: # A normal email of the form [email protected]. Let's attempt to # decode the domain component and return the entire address. return x509.RFC822Name( parts[0] + u"@" + idna.decode(parts[1]) ) elif gn.type == backend._lib.GEN_OTHERNAME: type_id = _obj2txt(backend, gn.d.otherName.type_id) value = _asn1_to_der(backend, gn.d.otherName.value) return x509.OtherName(x509.ObjectIdentifier(type_id), value) else: # x400Address or ediPartyName raise x509.UnsupportedGeneralNameType( "{0} is not a supported type".format( x509._GENERAL_NAMES.get(gn.type, gn.type) ), gn.type )
def parse_general_name(name): """Parse a general name from user input. This function will do its best to detect the intended type of any value passed to it: >>> parse_general_name('example.com') <DNSName(value='example.com')> >>> parse_general_name('*.example.com') <DNSName(value='*.example.com')> >>> parse_general_name('.example.com') # Syntax used e.g. for NameConstraints: All levels of subdomains <DNSName(value='.example.com')> >>> parse_general_name('*****@*****.**') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('1.2.3.4') <IPAddress(value=1.2.3.4)> >>> parse_general_name('fd00::1') <IPAddress(value=fd00::1)> >>> parse_general_name('/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> The default fallback is to assume a :py:class:`~cg:cryptography.x509.DNSName`. If this doesn't work, an exception will be raised: >>> parse_general_name('foo..bar`*123') # doctest: +ELLIPSIS Traceback (most recent call last): ... idna.core.IDNAError: ... If you want to override detection, you can prefix the name to match :py:const:`GENERAL_NAME_RE`: >>> parse_general_name('email:[email protected]') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('URI:https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('dirname:/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> Some more exotic values can only be generated by using this prefix: >>> parse_general_name('rid:2.5.4.3') <RegisteredID(value=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>)> >>> parse_general_name('otherName:2.5.4.3;UTF8:example.com') <OtherName(type_id=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value=b'example.com')> If you give a prefixed value, this function is less forgiving of any typos and does not catch any exceptions: >>> parse_general_name('email:foo@bar com') Traceback (most recent call last): ... ValueError: Invalid domain: bar com """ name = force_text(name) typ = None match = GENERAL_NAME_RE.match(name) if match is not None: typ, name = match.groups() typ = typ.lower() if typ is None: if re.match('[a-z0-9]{2,}://', name): # Looks like a URI try: return x509.UniformResourceIdentifier(name) except Exception: # pragma: no cover - this really accepts anything pass if '@' in name: # Looks like an Email address try: return x509.RFC822Name(validate_email(name)) except Exception: pass if name.strip().startswith('/'): # maybe it's a dirname? return x509.DirectoryName(x509_name(name)) # Try to parse this as IPAddress/Network try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass # Try to encode as domain name. DNSName() does not validate the domain name, but this check will fail. if name.startswith('*.'): idna.encode(name[2:]) elif name.startswith('.'): idna.encode(name[1:]) else: idna.encode(name) # Almost anything passes as DNS name, so this is our default fallback return x509.DNSName(name) if typ == 'uri': return x509.UniformResourceIdentifier(name) elif typ == 'email': return x509.RFC822Name(validate_email(name)) elif typ == 'ip': try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass raise ValueError('Could not parse IP address.') elif typ == 'rid': return x509.RegisteredID(x509.ObjectIdentifier(name)) elif typ == 'othername': regex = "(.*);(.*):(.*)" if re.match(regex, name) is not None: oid, asn_typ, val = re.match(regex, name).groups() oid = x509.ObjectIdentifier(oid) if asn_typ == 'UTF8': val = val.encode('utf-8') elif asn_typ == 'OctetString': val = bytes(bytearray.fromhex(val)) val = OctetString(val).dump() else: raise ValueError('Unsupported ASN type in otherName: %s' % asn_typ) val = force_bytes(val) return x509.OtherName(oid, val) else: raise ValueError('Incorrect otherName format: %s' % name) elif typ == 'dirname': return x509.DirectoryName(x509_name(name)) else: # Try to encode the domain name. DNSName() does not validate the domain name, but this # check will fail. if name.startswith('*.'): idna.encode(name[2:]) elif name.startswith('.'): idna.encode(name[1:]) else: idna.encode(name) return x509.DNSName(name)
def test_othername(self): self.assertEqual( parse_general_name('otherName:2.5.4.3;UTF8:example.com'), x509.OtherName(NameOID.COMMON_NAME, b'example.com'))
def parse_general_name(name): """Parse a general name from user input. This function will do its best to detect the intended type of any value passed to it: >>> parse_general_name('example.com') <DNSName(value='example.com')> >>> parse_general_name('*.example.com') <DNSName(value='*.example.com')> >>> parse_general_name('*****@*****.**') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('1.2.3.4') <IPAddress(value=1.2.3.4)> >>> parse_general_name('fd00::1') <IPAddress(value=fd00::1)> >>> parse_general_name('/CN=example.com') # doctest: +NORMALIZE_WHITESPACE <DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')>])>)> The default fallback is to assume a :py:class:`~cryptography:cryptography.x509.DNSName`. If this doesn't work, an exception will be raised: >>> parse_general_name('foo..bar`*123') Traceback (most recent call last): ... idna.core.IDNAError: The label b'' is not a valid A-label >>> parse_general_name('foo bar') Traceback (most recent call last): ... idna.core.IDNAError: The label b'foo bar' is not a valid A-label If you want to override detection, you can prefix the name to match :py:const:`GENERAL_NAME_RE`: >>> parse_general_name('email:[email protected]') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('URI:https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('dirname:/CN=example.com') # doctest: +NORMALIZE_WHITESPACE <DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')>])>)> Some more exotic values can only be generated by using this prefix: >>> parse_general_name('rid:2.5.4.3') <RegisteredID(value=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>)> >>> parse_general_name('otherName:2.5.4.3,example.com') <OtherName(type_id=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value=b'example.com')> If you give a prefixed value, this function is less forgiving of any typos and does not catch any exceptions: >>> parse_general_name('email:foo@bar com') Traceback (most recent call last): ... ValueError: Invalid domain: bar com """ name = force_text(name) typ = None match = GENERAL_NAME_RE.match(name) if match is not None: typ, name = match.groups() typ = typ.lower() if typ is None: if re.match('[a-z0-9]{2,}://', name): # Looks like a URI try: return x509.UniformResourceIdentifier(name) except Exception: # pragma: no cover - this really accepts anything pass if '@' in name: # Looks like an Email address try: return x509.RFC822Name(validate_email(name)) except Exception: pass if name.strip().startswith('/'): # maybe it's a dirname? return x509.DirectoryName(x509_name(name)) # Try to parse this as IPAddress/Network try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass # Try to encode the domain name. DNSName() does not validate the domain name, but this # check will fail. if name.startswith('*.'): idna.encode(name[2:]) else: idna.encode(name) # Almost anything passes as DNS name, so this is our default fallback return x509.DNSName(name) if typ == 'uri': return x509.UniformResourceIdentifier(name) elif typ == 'email': return x509.RFC822Name(validate_email(name)) elif typ == 'ip': try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass raise ValueError('Could not parse IP address.') elif typ == 'rid': return x509.RegisteredID(x509.ObjectIdentifier(name)) elif typ == 'othername': type_id, value = name.split(',', 1) type_id = x509.ObjectIdentifier(type_id) value = force_bytes(value) return x509.OtherName(type_id, value) elif typ == 'dirname': return x509.DirectoryName(x509_name(name)) else: # Try to encode the domain name. DNSName() does not validate the domain name, but this # check will fail. if name.startswith('*.'): idna.encode(name[2:]) else: idna.encode(name) return x509.DNSName(name)
def parse_general_name(name: ParsableGeneralName) -> x509.GeneralName: """Parse a general name from user input. This function will do its best to detect the intended type of any value passed to it: >>> parse_general_name('example.com') <DNSName(value='example.com')> >>> parse_general_name('*.example.com') <DNSName(value='*.example.com')> >>> parse_general_name('.example.com') # Syntax used e.g. for NameConstraints: All levels of subdomains <DNSName(value='.example.com')> >>> parse_general_name('*****@*****.**') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('1.2.3.4') <IPAddress(value=1.2.3.4)> >>> parse_general_name('fd00::1') <IPAddress(value=fd00::1)> >>> parse_general_name('/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> The default fallback is to assume a :py:class:`~cg:cryptography.x509.DNSName`. If this doesn't work, an exception will be raised: >>> parse_general_name('foo..bar`*123') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Could not parse name: foo..bar`*123 If you want to override detection, you can prefix the name to match :py:const:`GENERAL_NAME_RE`: >>> parse_general_name('email:[email protected]') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('URI:https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('dirname:/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> Some more exotic values can only be generated by using this prefix: >>> parse_general_name('rid:2.5.4.3') <RegisteredID(value=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>)> >>> parse_general_name('otherName:2.5.4.3;UTF8:example.com') <OtherName(type_id=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value=b'\\x0c\\x0bexample.com')> If you give a prefixed value, this function is less forgiving of any typos and does not catch any exceptions: >>> parse_general_name('email:foo@bar com') Traceback (most recent call last): ... ValueError: Invalid domain: bar com """ # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements if isinstance(name, x509.GeneralName): return name if not isinstance(name, str): raise ValueError( f"Cannot parse general name {name}: Must be of type str (was: {type(name).__name__})." ) typ = None match = GENERAL_NAME_RE.match(name) if match is not None: typ, name = match.groups() typ = typ.lower() if typ is None: if re.match("[a-z0-9]{2,}://", name): # Looks like a URI try: return x509.UniformResourceIdentifier(encode_url(name)) except idna.IDNAError: pass if "@" in name: # Looks like an Email address try: return x509.RFC822Name(validate_email(name)) except ValueError: pass if name.strip().startswith("/"): # maybe it's a dirname? return x509.DirectoryName(x509_name(name)) # Try to parse this as IPAddress/Network try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass # Almost anything passes as DNS name, so this is our default fallback try: return x509.DNSName(encode_dns(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse name: {name}") from e if typ == "uri": try: return x509.UniformResourceIdentifier(encode_url(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse DNS name in URL: {name}") from e elif typ == "email": return x509.RFC822Name(validate_email(name)) # validate_email already raises ValueError elif typ == "ip": try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass raise ValueError("Could not parse IP address.") elif typ == "rid": return x509.RegisteredID(x509.ObjectIdentifier(name)) elif typ == "othername": match = re.match("(.*?);(.*?):(.*)", name) if match is not None: oid, asn_typ, val = match.groups() # Get DER representation of the value for x509.OtherName() if asn_typ in ("UTF8", "UTF8String"): parsed_value = asn1crypto.core.UTF8String(val).dump() elif asn_typ in ("UNIV", "UNIVERSALSTRING"): parsed_value = asn1crypto.core.UniversalString(val).dump() elif asn_typ in ("IA5", "IA5STRING"): parsed_value = asn1crypto.core.IA5String(val).dump() elif asn_typ in ("BOOL", "BOOLEAN"): # nconf allows for true, y, yes, false, n and no as valid values if val.lower() in ("true", "y", "yes"): parsed_value = asn1crypto.core.Boolean(True).dump() elif val.lower() in ("false", "n", "no"): parsed_value = asn1crypto.core.Boolean(False).dump() else: raise ValueError( f"Unsupported {asn_typ} specification for otherName: {val}: Must be TRUE or FALSE" ) elif asn_typ in ("UTC", "UTCTIME"): parsed_datetime = datetime.strptime(val, "%y%m%d%H%M%SZ").replace(tzinfo=timezone.utc) parsed_value = asn1crypto.core.UTCTime(parsed_datetime).dump() elif asn_typ in ("GENTIME", "GENERALIZEDTIME"): parsed_datetime = datetime.strptime(val, "%Y%m%d%H%M%SZ").replace(tzinfo=timezone.utc) parsed_value = asn1crypto.core.GeneralizedTime(parsed_datetime).dump() elif asn_typ == "NULL": if val: raise ValueError("Invalid NULL specification for otherName: Value must not be present") parsed_value = asn1crypto.core.Null().dump() elif asn_typ in ("INT", "INTEGER"): if val.startswith("0x"): parsed_value = asn1crypto.core.Integer(int(val, 16)).dump() else: parsed_value = asn1crypto.core.Integer(int(val)).dump() elif asn_typ == "OctetString": parsed_value = asn1crypto.core.OctetString(bytes(bytearray.fromhex(val))).dump() else: raise ValueError(f"Unsupported ASN type in otherName: {asn_typ}") # NOTE: cryptography docs are not really clear on what kind of bytes x509.OtherName() expects, but # the test suite explicitly use b"derdata" as value, indicating DER encoded data. return x509.OtherName(x509.ObjectIdentifier(oid), parsed_value) raise ValueError(f"Incorrect otherName format: {name}") elif typ == "dirname": return x509.DirectoryName(x509_name(name)) else: try: return x509.DNSName(encode_dns(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse DNS name: {name}") from e
def parse_general_name(name: ParsableGeneralName) -> x509.GeneralName: """Parse a general name from user input. This function will do its best to detect the intended type of any value passed to it: >>> parse_general_name('example.com') <DNSName(value='example.com')> >>> parse_general_name('*.example.com') <DNSName(value='*.example.com')> >>> parse_general_name('.example.com') # Syntax used e.g. for NameConstraints: All levels of subdomains <DNSName(value='.example.com')> >>> parse_general_name('*****@*****.**') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('1.2.3.4') <IPAddress(value=1.2.3.4)> >>> parse_general_name('fd00::1') <IPAddress(value=fd00::1)> >>> parse_general_name('/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> The default fallback is to assume a :py:class:`~cg:cryptography.x509.DNSName`. If this doesn't work, an exception will be raised: >>> parse_general_name('foo..bar`*123') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Could not parse name: foo..bar`*123 If you want to override detection, you can prefix the name to match :py:const:`GENERAL_NAME_RE`: >>> parse_general_name('email:[email protected]') <RFC822Name(value='*****@*****.**')> >>> parse_general_name('URI:https://example.com') <UniformResourceIdentifier(value='https://example.com')> >>> parse_general_name('dirname:/CN=example.com') <DirectoryName(value=<Name(CN=example.com)>)> Some more exotic values can only be generated by using this prefix: >>> parse_general_name('rid:2.5.4.3') <RegisteredID(value=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>)> >>> parse_general_name('otherName:2.5.4.3;UTF8:example.com') <OtherName(type_id=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value=b'example.com')> If you give a prefixed value, this function is less forgiving of any typos and does not catch any exceptions: >>> parse_general_name('email:foo@bar com') Traceback (most recent call last): ... ValueError: Invalid domain: bar com """ # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements if isinstance(name, x509.GeneralName): return name if not isinstance(name, str): raise ValueError( f"Cannot parse general name {name}: Must be of type str (was: {type(name).__name__})." ) typ = None match = GENERAL_NAME_RE.match(name) if match is not None: typ, name = match.groups() typ = typ.lower() if typ is None: if re.match("[a-z0-9]{2,}://", name): # Looks like a URI try: return x509.UniformResourceIdentifier(encode_url(name)) except idna.IDNAError: pass if "@" in name: # Looks like an Email address try: return x509.RFC822Name(validate_email(name)) except ValueError: pass if name.strip().startswith("/"): # maybe it's a dirname? return x509.DirectoryName(x509_name(name)) # Try to parse this as IPAddress/Network try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass # Almost anything passes as DNS name, so this is our default fallback try: return x509.DNSName(encode_dns(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse name: {name}") from e if typ == "uri": try: return x509.UniformResourceIdentifier(encode_url(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse DNS name in URL: {name}") from e elif typ == "email": return x509.RFC822Name( validate_email(name)) # validate_email already raises ValueError elif typ == "ip": try: return x509.IPAddress(ip_address(name)) except ValueError: pass try: return x509.IPAddress(ip_network(name)) except ValueError: pass raise ValueError("Could not parse IP address.") elif typ == "rid": return x509.RegisteredID(x509.ObjectIdentifier(name)) elif typ == "othername": match = re.match("(.*);(.*):(.*)", name) if match is not None: oid, asn_typ, val = match.groups() if asn_typ == "UTF8": parsed_value = val.encode("utf-8") elif asn_typ == "OctetString": parsed_value = OctetString(bytes( bytearray.fromhex(val))).dump() else: raise ValueError( f"Unsupported ASN type in otherName: {asn_typ}") return x509.OtherName(x509.ObjectIdentifier(oid), parsed_value) raise ValueError(f"Incorrect otherName format: {name}") elif typ == "dirname": return x509.DirectoryName(x509_name(name)) else: try: return x509.DNSName(encode_dns(name)) except idna.IDNAError as e: raise ValueError(f"Could not parse DNS name: {name}") from e