# -*- 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 six
from cryptography import x509
from django.utils.encoding import force_text
from .utils import MULTIPLE_OIDS
from .utils import NAME_OID_MAPPINGS
from .utils import OID_NAME_MAPPINGS
from .utils import SUBJECT_FIELDS
from .utils import parse_name
from .utils import sort_name
[docs]@six.python_2_unicode_compatible
class Subject(object):
"""Convenience class to handle X509 Subjects.
This class accepts a variety of values and intelligently parses them:
>>> Subject('/CN=example.com')
Subject("/CN=example.com")
>>> Subject({'CN': 'example.com'})
Subject("/CN=example.com")
>>> Subject([('CN', 'example.com'), ])
Subject("/CN=example.com")
In most respects, this class handles like a ``dict``:
>>> s = Subject('/CN=example.com')
>>> 'CN' in s
True
>>> s.get('OU', 'Default OU')
'Default OU'
>>> s.setdefault('C', 'AT')
>>> s['C'], s['CN']
('AT', 'example.com')
"""
def __init__(self, subject=None):
self._data = {}
# Normalize input data to a list
if subject is None:
subject = []
elif isinstance(subject, six.string_types):
subject = parse_name(subject)
elif isinstance(subject, dict):
subject = subject.items()
elif not isinstance(subject, (list, tuple)):
raise ValueError('subject: not a list/tuple.')
for oid, value in subject:
if isinstance(oid, six.string_types):
oid = NAME_OID_MAPPINGS[oid]
if not value:
continue
if oid not in self._data:
self._data[oid] = [value]
elif oid not in MULTIPLE_OIDS:
raise ValueError('%s: Must not occur multiple times' % OID_NAME_MAPPINGS[oid])
else:
self._data[oid].append(value)
def __contains__(self, oid):
if isinstance(oid, six.string_types):
oid = NAME_OID_MAPPINGS[oid]
return oid in self._data
def __eq__(self, other):
return isinstance(other, Subject) and self._data == other._data
def __getitem__(self, key):
if isinstance(key, six.string_types):
key = NAME_OID_MAPPINGS[key]
try:
if key in MULTIPLE_OIDS:
return self._data[key]
return self._data[key][0]
except KeyError:
raise KeyError(OID_NAME_MAPPINGS[key])
def __iter__(self):
#return (OID_NAME_MAPPINGS[t[0]] for t in self._iter)
for key, value in self._iter:
for val in value:
yield OID_NAME_MAPPINGS[key]
def __len__(self):
return len(self._data)
def __setitem__(self, key, value):
if isinstance(key, six.string_types):
key = NAME_OID_MAPPINGS[key]
if not value and key in self._data:
del self._data[key]
return
elif isinstance(value, six.string_types):
value = [value]
elif not isinstance(value, list):
raise ValueError('Value must be str or list')
if len(value) > 1 and key not in MULTIPLE_OIDS:
raise ValueError('%s: Must not occur multiple times' % OID_NAME_MAPPINGS[key])
self._data[key] = value
def __repr__(self):
return 'Subject("%s")' % str(self)
def __str__(self):
data = []
for oid, values in self._data.items():
for val in values:
data.append((OID_NAME_MAPPINGS[oid], val))
data = ['%s=%s' % (k, v) for k, v in sort_name(data)]
return '/%s' % '/'.join(data)
@property
def _iter(self):
return sorted(self._data.items(), key=lambda t: SUBJECT_FIELDS.index(OID_NAME_MAPPINGS[t[0]]))
def clear(self):
self._data.clear()
def copy(self):
return Subject(list(self.items()))
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def items(self):
for key, value in self._iter:
key = OID_NAME_MAPPINGS[key]
for val in value:
yield key, val
def keys(self):
for key in self:
yield key
def setdefault(self, oid, value):
if isinstance(oid, six.string_types):
oid = NAME_OID_MAPPINGS[oid]
if oid in self._data: # already set
return
if isinstance(value, six.string_types):
value = [value]
elif not isinstance(value, list):
raise ValueError('Value must be str or list')
if len(value) > 1 and oid not in MULTIPLE_OIDS:
raise ValueError('%s: Must not occur multiple times' % OID_NAME_MAPPINGS[oid])
self._data[oid] = value
def update(self, e=None, **f):
if e is None:
e = {}
if isinstance(e, Subject):
self._data.update(e._data)
elif hasattr(e, 'keys'):
for k in e.keys():
self[k] = e[k]
else:
for k, v in e:
self[k] = v
for k in f:
self[k] = f[k]
def values(self):
for key, value in self._iter:
for val in value:
yield val
####################
# Actual functions #
####################
@property
def fields(self):
"""This subject as a list of :py:class:`~cg:cryptography.x509.oid.NameOID` instances.
>>> list(Subject('/C=AT/CN=example.com').fields) # doctest: +NORMALIZE_WHITESPACE
[(<ObjectIdentifier(oid=2.5.4.6, name=countryName)>, 'AT'),
(<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, 'example.com')]
"""
for oid, values in self._iter:
for val in values:
yield oid, force_text(val)
@property
def name(self):
"""This subject as :py:class:`x509.Name <cg:cryptography.x509.Name>`.
>>> Subject('/C=AT/CN=example.com').name
<Name(C=AT,CN=example.com)>
"""
return x509.Name([x509.NameAttribute(k, v) for k, v in self.fields])