django_ca.utils - utility functions

Central functions to load CA key and cert as PKey/X509 objects.

django_ca.utils.GENERAL_NAME_RE = re.compile('^(email|URI|IP|DNS|RID|dirName|otherName):(.*)', re.IGNORECASE)

Regular expression to match general names.

class django_ca.utils.GeneralNameList(iterable=())[source]

List that holds GeneralName instances and parses str when added.

A GeneralNameList is a list subclass that will always only hold GeneralName instances, but any str passed to it will be passed to parse_general_name():

>>> from cryptography import x509
>>> l = GeneralNameList(['example.com'])
>>> l += ['DNS:example.net', x509.DNSName('example.org')]
>>> print(l)
<GeneralNameList: ['DNS:example.com', 'DNS:example.net', 'DNS:example.org']>
>>> 'example.com' in l, 'DNS:example.com' in l, x509.DNSName('example.com') in l
(True, True, True)
>>> l == ['example.com', 'example.net', 'example.org']
True
>>> l == [x509.DNSName('example.com'), 'example.net', 'DNS:example.org']
True
serialize()[source]

Generate a list of formatted names.

class django_ca.utils.LazyEncoder(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)[source]

Encoder that also encodes strings translated with gettext_lazy.

django_ca.utils.NAME_RE = re.compile('(?:/+|\\A)\\s*(?P<field>[^\\s]*?)\\s*=(?P<quote>[\\\'"])?\\s*(?P<content>(?(quote).*?|[^/]*))\\s*(?(quote)(?<!\\\\)(?P=quote))', re.IGNORECASE)

Regular expression to match RDNs out of a full x509 name.

django_ca.utils.OID_NAME_MAPPINGS = {<ObjectIdentifier(oid=2.5.4.6, name=countryName)>: 'C', <ObjectIdentifier(oid=2.5.4.8, name=stateOrProvinceName)>: 'ST', <ObjectIdentifier(oid=2.5.4.7, name=localityName)>: 'L', <ObjectIdentifier(oid=2.5.4.10, name=organizationName)>: 'O', <ObjectIdentifier(oid=2.5.4.11, name=organizationalUnitName)>: 'OU', <ObjectIdentifier(oid=2.5.4.3, name=commonName)>: 'CN', <ObjectIdentifier(oid=1.2.840.113549.1.9.1, name=emailAddress)>: 'emailAddress', <ObjectIdentifier(oid=2.5.4.5, name=serialNumber)>: 'serialNumber', <ObjectIdentifier(oid=1.3.6.1.4.1.311.60.2.1.3, name=jurisdictionCountryName)>: 'jurisdictionCountryName', <ObjectIdentifier(oid=1.3.6.1.4.1.311.60.2.1.2, name=jurisdictionStateOrProvinceName)>: 'jurisdictionStateOrProvinceName', <ObjectIdentifier(oid=2.5.4.15, name=businessCategory)>: 'businessCategory', <ObjectIdentifier(oid=2.5.4.17, name=postalCode)>: 'postalCode', <ObjectIdentifier(oid=2.5.4.9, name=streetAddress)>: 'streetAddress'}

Map OID objects to IDs used in subject strings

django_ca.utils.SERIAL_RE = re.compile('^([0-9A-F][0-9A-F]:?)+[0-9A-F][0-9A-F]?$')

Regular expression matching hexlified certificate serials

django_ca.utils.add_colons(value, pad='0')[source]

Add colons after every second digit.

This function is used in functions to prettify serials.

>>> add_colons('teststring')
'te:st:st:ri:ng'
Parameters
sstr

The string to add colons to

padstr, default

If not None, pad the string so that the last element always has two characters. The default is "0".

django_ca.utils.bytes_to_hex(value)[source]

Convert a bytes array to hex.

>>> bytes_to_hex(b'test')
'74:65:73:74'
django_ca.utils.encode_dns(name)[source]

IDNA encoding for domains.

Examples:

>>> encode_dns('example.com')
'example.com'
>>> encode_dns('exämple.com')
'xn--exmple-cua.com'
>>> encode_dns('.exämple.com')
'.xn--exmple-cua.com'
>>> encode_dns('*.exämple.com')
'*.xn--exmple-cua.com'
django_ca.utils.encode_url(url)[source]

IDNA encoding for domains in URLs.

Examples:

>>> encode_url('https://example.com')
'https://example.com'
>>> encode_url('https://exämple.com/foobar')
'https://xn--exmple-cua.com/foobar'
>>> encode_url('https://exämple.com:8000/foobar')
'https://xn--exmple-cua.com:8000/foobar'
django_ca.utils.format_general_name(name)[source]

Format a single general name.

>>> import ipaddress
>>> format_general_name(x509.DNSName('example.com'))
'DNS:example.com'
>>> format_general_name(x509.IPAddress(ipaddress.IPv4Address('127.0.0.1')))
'IP:127.0.0.1'
django_ca.utils.format_name(subject)[source]

Convert a subject into the canonical form for distinguished names.

This function does not take care of sorting the subject in any meaningful order.

Examples:

>>> format_name([('CN', 'example.com'), ])
'/CN=example.com'
>>> format_name([('CN', 'example.com'), ('O', "My Organization"), ])
'/CN=example.com/O=My Organization'
django_ca.utils.format_relative_name(name)[source]

Convert a relative name (RDN) into a canonical form.

Examples:

>>> format_relative_name([('C', 'AT'), ('CN', 'example.com')])
'/C=AT/CN=example.com'
>>> format_relative_name(x509.RelativeDistinguishedName([
...     x509.NameAttribute(NameOID.COMMON_NAME, 'example.com')
... ]))
'/CN=example.com'
django_ca.utils.generate_private_key(key_size, key_type, ecc_curve)[source]

Generate a private key.

This function assumes that you called validate_key_parameters() on the input values and does not do any sanity checks on its own.

Parameters
key_sizeint

The size of the private key (not used for ECC keys).

key_type{‘RSA’, ‘DSA’, ‘ECC’}

The type of the private key.

ecc_curveEllipticCurve

The ECC curve to use for an ECC key.

Returns
key

A private key of the appropriate type.

django_ca.utils.get_cert_builder(expires, serial=None)[source]

Get a basic X509 cert builder object.

Parameters
expiresdatetime or timedelta

When this certificate will expire.

serialint, optional

Serial number to set for this certificate. Use random_serial_number() to generate such a value. By default, a value will be generated.

django_ca.utils.get_crl_cache_key(serial, algorithm=<class 'cryptography.hazmat.primitives.hashes.SHA512'>, encoding=<Encoding.DER: 'DER'>, scope=None)[source]

Function to get a cache key for a CRL with the given parameters.

django_ca.utils.hex_to_bytes(value)[source]

Convert a hex number to bytes.

This should be the inverse of bytes_to_hex().

>>> hex_to_bytes('74:65:73:74')
b'test'
django_ca.utils.int_to_hex(i)[source]

Create a hex-representation of the given serial.

>>> int_to_hex(12345678)
'BC614E'
django_ca.utils.is_power2(num)[source]

Return True if num is a power of 2.

>>> is_power2(4)
True
>>> is_power2(3)
False
django_ca.utils.multiline_url_validator(value)[source]

Validate that a TextField contains one valid URL per line.

django_ca.utils.parse_encoding(value=None)[source]

Parse a value to a valid encoding.

This function accepts either a member of Encoding or a string describing a member. If no value is passed, it will assume PEM as a default value. Note that "ASN1" is treated as an alias for "DER".

>>> parse_encoding()
<Encoding.PEM: 'PEM'>
>>> parse_encoding('DER')
<Encoding.DER: 'DER'>
>>> parse_encoding(Encoding.PEM)
<Encoding.PEM: 'PEM'>
django_ca.utils.parse_general_name(name)[source]

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('user@example.com')
<RFC822Name(value='user@example.com')>
>>> 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 DNSName. If this doesn’t work, an exception will be raised:

>>> parse_general_name('foo..bar`*123')  
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 GENERAL_NAME_RE:

>>> parse_general_name('email:user@example.com')
<RFC822Name(value='user@example.com')>
>>> 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
django_ca.utils.parse_hash_algorithm(value=None)[source]

Parse a hash algorithm value.

The most common use case is to pass a str naming a class in hashes.

For convenience, passing None will return the value of CA_DIGEST_ALGORITHM, and passing an HashAlgorithm will return that instance unchanged.

Example usage:

>>> parse_hash_algorithm()  
<cryptography.hazmat.primitives.hashes.SHA512 object at ...>
>>> parse_hash_algorithm('SHA512')  
<cryptography.hazmat.primitives.hashes.SHA512 object at ...>
>>> parse_hash_algorithm(' SHA512 ')  
<cryptography.hazmat.primitives.hashes.SHA512 object at ...>
>>> parse_hash_algorithm(hashes.SHA512)  
<cryptography.hazmat.primitives.hashes.SHA512 object at ...>
>>> parse_hash_algorithm(hashes.SHA512())  
<cryptography.hazmat.primitives.hashes.SHA512 object at ...>
>>> parse_hash_algorithm('Wrong')  
Traceback (most recent call last):
    ...
ValueError: Unknown hash algorithm: Wrong
>>> parse_hash_algorithm(object())  
Traceback (most recent call last):
    ...
ValueError: Unknown type passed: object
Parameters
valuestr or HashAlgorithm, optional

The value to parse, the function description on how possible values are used.

Returns
algorithm

A HashAlgorithm instance.

Raises
ValueError

If an unknown object is passed or if value does not name a known algorithm.

django_ca.utils.parse_key_curve(value=None)[source]

Parse an elliptic curve value.

This function uses a value identifying an elliptic curve to return an EllipticCurve instance. The name must match a class name of one of the classes named under “Elliptic Curves” in Elliptic curve cryptography.

For convenience, passing None will return the value of CA_DEFAULT_ECC_CURVE, and passing an EllipticCurve will return that instance unchanged.

Example usage:

>>> parse_key_curve('SECP256R1')  
<cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at ...>
>>> parse_key_curve('SECP384R1')  
<cryptography.hazmat.primitives.asymmetric.ec.SECP384R1 object at ...>
>>> parse_key_curve(ec.SECP256R1())  
<cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at ...>
>>> parse_key_curve()  
<cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at ...>
Parameters
valuestr, otional

The name of the curve or None to return the default curve.

Returns
curve

An EllipticCurve instance.

Raises
ValueError

If the named curve is not supported.

django_ca.utils.parse_name(name)[source]

Parses a subject string as used in OpenSSLs command line utilities.

The name is expected to be close to the subject format commonly used by OpenSSL, for example /C=AT/L=Vienna/CN=example.com/emailAddress=user@example.com. The function does its best to be lenient on deviations from the format, object identifiers are case-insensitive (e.g. cn is the same as CN, whitespace at the start and end is stripped and the subject does not have to start with a slash (/).

>>> parse_name('/CN=example.com')
[('CN', 'example.com')]
>>> parse_name('c=AT/l= Vienna/o="ex org"/CN=example.com')
[('C', 'AT'), ('L', 'Vienna'), ('O', 'ex org'), ('CN', 'example.com')]

Dictionary keys are normalized to the values of OID_NAME_MAPPINGS and keys will be sorted based on x509 name specifications regardless of the given order:

>>> parse_name('L="Vienna / District"/EMAILaddress=user@example.com')
[('L', 'Vienna / District'), ('emailAddress', 'user@example.com')]
>>> parse_name('/C=AT/CN=example.com') == parse_name('/CN=example.com/C=AT')
True

Due to the magic of NAME_RE, the function even supports quoting strings and including slashes, so strings like /OU="Org / Org Unit"/CN=example.com will work as expected.

>>> parse_name('L="Vienna / District"/CN=example.com')
[('L', 'Vienna / District'), ('CN', 'example.com')]

But note that it’s still easy to trick this function, if you really want to. The following example is not a valid subject, the location is just bogus, and whatever you were expecting as output, it’s certainly different:

>>> parse_name('L="Vienna " District"/CN=example.com')
[('L', 'Vienna'), ('CN', 'example.com')]

Examples of where this string is used are:

# openssl req -new -key priv.key -out csr -utf8 -batch -sha256 -subj '/C=AT/CN=example.com'
# openssl x509 -in cert.pem -noout -subject -nameopt compat
/C=AT/L=Vienna/CN=example.com
django_ca.utils.read_file(path)[source]

Read the file from the given path.

If path is an absolute path, reads a file from the local filesystem. For relative paths, read the file using the storage backend configured using CA_FILE_STORAGE.

django_ca.utils.sanitize_serial(value)[source]

Sanitize a serial provided by user/untrusted input.

This function is intended to be used to get a serial as used internaly by django-ca from untrusted user input. Internally, serials are stored in upper case and without : and leading zeros, but user output adds at least :.

Examples:

>>> sanitize_serial('01:aB')
'1AB'
django_ca.utils.shlex_split(val, sep)[source]

Split a character on the given set of characters.

Example:

>>> shlex_split('foo,bar', ', ')
['foo', 'bar']
>>> shlex_split('foo\\,bar1', ',')  # escape a separator
['foo,bar1']
>>> shlex_split('"foo,bar", bla', ', ')
['foo,bar', 'bla']
>>> shlex_split('foo,"bar,bla"', ',')
['foo', 'bar,bla']
django_ca.utils.sort_name(subject)[source]

Returns the subject in the correct order for a x509 subject.

django_ca.utils.validate_email(addr)[source]

Validate an email address.

This function raises ValueError if the email address is not valid.

>>> validate_email('foo@bar.com')
'foo@bar.com'
>>> validate_email('foo@bar com')
Traceback (most recent call last):
    ...
ValueError: Invalid domain: bar com
django_ca.utils.validate_hostname(hostname, allow_port=False)[source]

Validate a hostname, optionally with a given port.

>>> validate_hostname('example.com')
'example.com'
>>> validate_hostname('example.com:8000', allow_port=True)
'example.com:8000'
Parameters
hostnamestr

The hostname to validate.

allow_portbool, optional

If True, the hostname can also contain an optional port number, e.g. “example.com:8000”.

Raises
ValueError

If hostname or port are not valid.

django_ca.utils.validate_key_parameters(key_size=None, key_type='RSA', ecc_curve=None)[source]

Validate parameters for private key generation and return sanitized values.

This function can be used to fail early if invalid parameters are passed, before the private key is generated.

>>> validate_key_parameters()  # defaults
(1024, 'RSA', None)
>>> validate_key_parameters(4096, 'ECC', None)  
(None, 'ECC', <cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at ...>)
>>> validate_key_parameters(4000, 'RSA', None)
Traceback (most recent call last):
    ...
ValueError: 4000: Key size must be a power of two
django_ca.utils.x509_name(name)[source]

Parses a subject into a x509.Name.

If name is a string, parse_name() is used to parse it.

>>> x509_name('/C=AT/CN=example.com')
<Name(C=AT,CN=example.com)>
>>> x509_name([('C', 'AT'), ('CN', 'example.com')])
<Name(C=AT,CN=example.com)>
django_ca.utils.x509_relative_name(name)[source]

Parse a relative name (RDN) into a RelativeDistinguishedName.

>>> x509_relative_name('/CN=example.com')
<RelativeDistinguishedName(CN=example.com)>
>>> x509_relative_name([('CN', 'example.com')])
<RelativeDistinguishedName(CN=example.com)>