# -*- coding: utf-8 -*-
#
# This file is part of django-ca (https://github.com/mathiasertl/django-ca).
#
# django-ca is free software: you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# django-ca is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with django-ca. If not,
# see <http://www.gnu.org/licenses/>.
import binascii
import six
from cryptography import x509
from cryptography.x509 import TLSFeatureType
from cryptography.x509.oid import ExtendedKeyUsageOID
from cryptography.x509.oid import ExtensionOID
from cryptography.x509.oid import ObjectIdentifier
[docs]@six.python_2_unicode_compatible
class Extension(object):
"""Convenience class to handle X509 Extensions.
The class is designed to take whatever format an extension might occur, essentially providing a
convertible format for extensions that is used in many places throughout the code. It accepts ``str`` if
e.g. the value was received from the commandline::
>>> KeyUsage('keyAgreement,keyEncipherment')
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=False>
>>> KeyUsage('critical,keyAgreement,keyEncipherment')
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=True>
It also accepts a ``list``/``tuple`` of two elements, the first being the "critical" flag, the second
being a value (e.g. from a MultiValueField from a form)::
>>> KeyUsage((False, ['keyAgreement', 'keyEncipherment']))
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=False>
>>> KeyUsage((True, ['keyAgreement', 'keyEncipherment']))
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=True>
Or it can be a ``dict`` as used by the :ref:`CA_PROFILES <settings-ca-profiles>` setting::
>>> KeyUsage({'value': ['keyAgreement', 'keyEncipherment']})
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=False>
>>> KeyUsage({'critical': True, 'value': ['keyAgreement', 'keyEncipherment']})
<KeyUsage: ['keyAgreement', 'keyEncipherment'], critical=True>
... and finally it can also use a subclass of :py:class:`~cryptography:cryptography.x509.ExtensionType`
from ``cryptography``::
>>> from cryptography import x509
>>> ExtendedKeyUsage(x509.extensions.Extension(
... oid=ExtensionOID.EXTENDED_KEY_USAGE,
... critical=False,
... value=x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH])
... ))
<ExtendedKeyUsage: ['serverAuth'], critical=False>
Attributes
----------
name
value
Raw value for this extension. The type various from subclass to subclass.
Parameters
----------
value : list or tuple or dict or str or :py:class:`~cryptography:cryptography.x509.ExtensionType`
The value of the extension, the description provides further details.
"""
def __init__(self, value):
if isinstance(value, x509.extensions.Extension): # e.g. from a cert object
self.critical = value.critical
self.from_extension(value)
elif isinstance(value, (list, tuple)): # e.g. from a form
self.critical, value = value
self.from_list(value)
self._test_value()
elif isinstance(value, dict): # e.g. from settings
self.critical = value.get('critical', False)
self.from_dict(value)
self._test_value()
elif isinstance(value, six.string_types): # e.g. from commandline parser
if value.startswith('critical,'):
self.critical = True
value = value[9:]
else:
self.critical = False
value = value
self.from_str(value)
self._test_value()
else:
raise ValueError('Value is of unsupported type %s' % type(value).__name__)
if not isinstance(self.critical, bool):
raise ValueError('%s: Invalid critical value passed' % self.critical)
def __eq__(self, other):
return isinstance(other, type(self)) and self.critical == other.critical and self.value == other.value
def __repr__(self):
return '<%s: %r, critical=%r>' % (self.__class__.__name__, self.value, self.critical)
def __str__(self):
if self.critical:
return'%s/critical' % self.as_text()
return self.as_text()
def from_extension(self, value):
raise NotImplementedError
def from_str(self, value):
self.value = value
def from_dict(self, value):
self.value = value['value']
def from_list(self, value):
self.value = value
def _test_value(self):
pass
@property
def name(self):
"""A human readable name of this extension."""
return self.oid._name
[docs] def add_colons(self, s):
"""Add colons to a string.
TODO: duplicate from utils :-(
"""
return ':'.join([s[i:i + 2] for i in range(0, len(s), 2)])
@property
def extension_type(self):
"""The extension_type for this value."""
raise NotImplementedError
[docs] def as_extension(self):
"""This extension as :py:class:`~cryptography:cryptography.x509.ExtensionType`."""
return x509.extensions.Extension(oid=self.oid, critical=self.critical, value=self.extension_type)
[docs] def as_text(self):
"""Human-readable version of the *value*, not including the "critical" flag."""
return self.value
[docs] def for_builder(self):
"""Return kwargs suitable for a :py:class:`~cryptography:cryptography.x509.CertificateBuilder`.
Example::
>>> kwargs = KeyUsage('keyAgreement,keyEncipherment').for_builder()
>>> builder.add_extension(**kwargs) # doctest: +SKIP
"""
return {'extension': self.extension_type, 'critical': self.critical}
[docs]class MultiValueExtension(Extension):
"""A generic base class for extensions that have multiple values.
Instances of this class have a ``len()`` and can be used with the ``in`` operator::
>>> ku = KeyUsage((False, ['keyAgreement', 'keyEncipherment']))
>>> 'keyAgreement' in ku
True
>>> len(ku)
2
Known values are set in the ``KNOWN_VALUES`` attribute for each class. The constructor will raise
``ValueError`` if an unknown value is passed.
"""
KNOWN_VALUES = set()
def __eq__(self, other):
return isinstance(other, type(self)) and self.critical == other.critical \
and sorted(self.value) == sorted(other.value)
def __str__(self):
val = ','.join(sorted(self.value))
if self.critical:
return'%s/critical' % val
return val
def from_dict(self, value):
self.value = value['value']
if isinstance(self.value, six.string_types):
self.value = [self.value]
def from_str(self, value):
super(MultiValueExtension, self).from_str(value) # parses critical prefix
self.value = [v.strip() for v in self.value.split(',') if v.strip()]
def __contains__(self, value):
return value in self.value
def __len__(self):
return len(self.value)
def _test_value(self):
diff = set(self.value) - self.KNOWN_VALUES
if diff:
raise ValueError('Unknown value(s): %s' % ', '.join(sorted(diff)))
def as_text(self):
return '\n'.join(['* %s' % v for v in sorted(self.value)])
[docs]class KeyIdExtension(Extension):
"""Base class for extensions that contain a KeyID as value.
.. TODO::
* All subclasses are only instantiated from a cryptography extension, so other values don't work.
"""
def as_text(self):
return self.add_colons(binascii.hexlify(self.value).upper().decode('utf-8'))
[docs]class AuthorityKeyIdentifier(KeyIdExtension):
"""Class representing a AuthorityKeyIdentifier extension.
.. TODO::
* This class only supports key_identifier, should also support authority_cert_issuer and
authority_cert_serial_number (see underlying constructor).
"""
oid = ExtensionOID.AUTHORITY_KEY_IDENTIFIER
def from_extension(self, ext):
self.value = ext.value.key_identifier
def as_text(self):
return 'keyid:%s' % super(AuthorityKeyIdentifier, self).as_text()
[docs]class KeyUsage(MultiValueExtension):
"""Class representing a KeyUsage extension."""
oid = ExtensionOID.KEY_USAGE
CRYPTOGRAPHY_MAPPING = {
'cRLSign': 'crl_sign',
'dataEncipherment': 'data_encipherment',
'decipherOnly': 'decipher_only',
'digitalSignature': 'digital_signature',
'encipherOnly': 'encipher_only',
'keyAgreement': 'key_agreement',
'keyCertSign': 'key_cert_sign',
'keyEncipherment': 'key_encipherment',
'nonRepudiation': 'content_commitment', # http://marc.info/?t=107176106300005&r=1&w=2
}
KNOWN_VALUES = set(CRYPTOGRAPHY_MAPPING)
"""Known values for this extension."""
CHOICES = (
('cRLSign', 'CRL Sign'),
('dataEncipherment', 'dataEncipherment'),
('decipherOnly', 'decipherOnly'),
('digitalSignature', 'Digital Signature'),
('encipherOnly', 'encipherOnly'),
('keyAgreement', 'Key Agreement'),
('keyCertSign', 'Certificate Sign'),
('keyEncipherment', 'Key Encipherment'),
('nonRepudiation', 'nonRepudiation'),
)
def __init__(self, *args, **kwargs):
super(KeyUsage, self).__init__(*args, **kwargs)
# decipherOnly only makes sense if keyAgreement is True
if 'decipherOnly' in self.value and 'keyAgreement' not in self.value:
self.value.append('keyAgreement')
def from_extension(self, ext):
self.value = []
for k, v in self.CRYPTOGRAPHY_MAPPING.items():
try:
if getattr(ext.value, v):
self.value.append(k)
except ValueError:
# cryptography throws a ValueError if encipher_only/decipher_only is accessed and
# key_agreement is not set.
pass
@property
def extension_type(self):
kwargs = {v: (k in self.value) for k, v in self.CRYPTOGRAPHY_MAPPING.items()}
return x509.KeyUsage(**kwargs)
[docs]class ExtendedKeyUsage(MultiValueExtension):
"""Class representing a ExtendedKeyUsage extension."""
oid = ExtensionOID.EXTENDED_KEY_USAGE
CRYPTOGRAPHY_MAPPING = {
'serverAuth': ExtendedKeyUsageOID.SERVER_AUTH,
'clientAuth': ExtendedKeyUsageOID.CLIENT_AUTH,
'codeSigning': ExtendedKeyUsageOID.CODE_SIGNING,
'emailProtection': ExtendedKeyUsageOID.EMAIL_PROTECTION,
'timeStamping': ExtendedKeyUsageOID.TIME_STAMPING,
'OCSPSigning': ExtendedKeyUsageOID.OCSP_SIGNING,
'smartcardLogon': ObjectIdentifier("1.3.6.1.4.1.311.20.2.2"),
'msKDC': ObjectIdentifier("1.3.6.1.5.2.3.5"),
}
_CRYPTOGRAPHY_MAPPING_REVERSED = {v: k for k, v in CRYPTOGRAPHY_MAPPING.items()}
KNOWN_VALUES = set(CRYPTOGRAPHY_MAPPING)
"""Known values for this extension."""
CHOICES = (
('serverAuth', 'SSL/TLS Web Server Authentication'),
('clientAuth', 'SSL/TLS Web Client Authentication'),
('codeSigning', 'Code signing'),
('emailProtection', 'E-mail Protection (S/MIME)'),
('timeStamping', 'Trusted Timestamping'),
('OCSPSigning', 'OCSP Signing'),
('smartcardLogon', 'Smart card logon'),
('msKDC', 'Kerberos Domain Controller'),
)
def from_extension(self, ext):
self.value = [self._CRYPTOGRAPHY_MAPPING_REVERSED[u] for u in ext.value]
@property
def extension_type(self):
return x509.ExtendedKeyUsage([self.CRYPTOGRAPHY_MAPPING[u] for u in self.value])
[docs]class SubjectKeyIdentifier(KeyIdExtension):
"""Class representing a SubjectKeyIdentifier extension."""
oid = ExtensionOID.SUBJECT_KEY_IDENTIFIER
def from_extension(self, ext):
self.value = ext.value.digest
[docs]class TLSFeature(MultiValueExtension):
"""Class representing a TLSFeature extension."""
oid = ExtensionOID.TLS_FEATURE
CHOICES = (
('OCSPMustStaple', 'OCSP Must-Staple'),
('MultipleCertStatusRequest', 'Multiple Certificate Status Request'),
)
CRYPTOGRAPHY_MAPPING = {
# https://tools.ietf.org/html/rfc6066.html:
'OCSPMustStaple': TLSFeatureType.status_request,
# https://tools.ietf.org/html/rfc6961.html (not commonly used):
'MultipleCertStatusRequest': TLSFeatureType.status_request_v2,
}
_CRYPTOGRAPHY_MAPPING_REVERSED = {v: k for k, v in CRYPTOGRAPHY_MAPPING.items()}
KNOWN_VALUES = set(CRYPTOGRAPHY_MAPPING)
"""Known values for this extension."""
def from_extension(self, ext):
self.value = [self._CRYPTOGRAPHY_MAPPING_REVERSED[f] for f in ext.value]
@property
def extension_type(self):
return x509.TLSFeature([self.CRYPTOGRAPHY_MAPPING[f] for f in self.value])