Repository: pyca/cryptography

Issue:
Add support for parsing distinguished names in X.509
We need to be able to parse subject and issuer DNs!



Discussion:
So, here's the ASN.1 structure of issuer and subject:

```
   Name ::= CHOICE { -- only one possibility for now --
     rdnSequence  RDNSequence }

   RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

   RelativeDistinguishedName ::=
     SET SIZE (1..MAX) OF AttributeTypeAndValue

   AttributeTypeAndValue ::= SEQUENCE {
     type     AttributeType,
     value    AttributeValue }

   AttributeType ::= OBJECT IDENTIFIER

   AttributeValue ::= ANY -- DEFINED BY AttributeType

```

So, this is a SEQUENCE OF SETs of AttributeTypeAndValue.

I'm not sure I actually understand what that data model looks like. @reaperhulk can you show what an average issuer or subject looks like in that data model?

AttributeType will be an OID like 2.5.4.3 (commonName), but apparently the normative reference for this is X.520 (http://www.itu.int/ITU-T/formal-language/itu-t/x/x520/2012/SelectedAttributeTypes.html) which would imply we need to write our declarative parser with a huge number of OID cases... In practice 99.9% of all certs are just going to have a small subset of common OIDs and they'll all be DirectoryString.

If we continue to pursue pyasn1 parsing of the X509 name here's some code that can be added to the OpenSSL `_Certificate` class to build some ASN.1 bytes to parse:

``` python
    def issuer(self):
        issuer = self._backend._lib.X509_get_issuer_name(self._x509)
        assert issuer != self._backend._ffi.NULL
        buf = self._backend._ffi.new("unsigned char **")
        # TODO free the memory allocated here.
        length = self._backend._lib.i2d_X509_NAME(issuer, buf)
        assert length > 0
        asn1der = self._backend._ffi.buffer(buf[0], length)[:]
```

@reaperhulk Do you have a thought about what the API for this should look like?

So I reviewed pyOpenSSL and Go's APIs for issuer. A few notes on this data model:
- It turns out `RelativeDistinguishedName`, despite being a `SET`, only ever contains a single item.
- Multiple OUs and things like that are a reality we have to live with
- I think our API should only ever give you a list of OUs, there shouldn't be a way to get a single one, you should have to unpack it yourself.

@reaperhulk you've mentioned doing this by letting OpenSSL extract the asn.1 and doing the parsing with pyasn.1, any reason to prefer that to doing all the processing with OpenSSL?

Not really. I suppose it would normalize behavior across backends, but that's unlikely to be a real issue.

@reaperhulk surely tests will let us do that :-)

If we want to do this with OpenSSL do we want to have it parse it out and dump it into a generic `Name` class? That class would need to have properties that allow access to each of the subject OIDs we're supporting + some way of giving a precise order (conceptually: [['CN', 'myCN'], ['O', 'My Org'], ['O', 'Another Org']]). The latter is not typically important when parsing but definitely matters if we ever intend to use this same object to generate certificate subject/issuer DNs.

Iterating over an `X509_NAME` in OpenSSL looks something like this:

``` python
        name = self._backend._lib.X509_get_issuer_name(self._x509)
        count = self._backend._lib.X509_NAME_entry_count(name)
        for x in range(0, count):
            entry = self._backend._lib.X509_NAME_get_entry(name, x)
            obj = self._backend._lib.X509_NAME_ENTRY_get_object(entry)
            assert obj != self._backend._ffi.NULL
            data = self._backend._lib.X509_NAME_ENTRY_get_data(entry)
            assert data != self._backend._ffi.NULL
            nid = self._backend._lib.OBJ_obj2nid(obj)
            if nid == self._backend._lib.NID_undef:
                raise UnknownAttribute(
                    "Not a recognized OID"
                )
            if nid == self._backend._lib.NID_commonName:
                # do some things
```


Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -91,3 +91,48 @@ def _parse_asn1_time(self, asn1_time):
             )
         ).decode("ascii")
         return datetime.datetime.strptime(time, "%Y%m%d%H%M%SZ")
+
+    @property
+    def issuer(self):
+        issuer = self._backend._lib.X509_get_issuer_name(self._x509)
+        assert issuer != self._backend._ffi.NULL
+        return self._build_x509_name(issuer)
+
+    @property
+    def subject(self):
+        subject = self._backend._lib.X509_get_subject_name(self._x509)
+        assert subject != self._backend._ffi.NULL
+        return self._build_x509_name(subject)
+
+    def _build_x509_name(self, x509_name):
+        count = self._backend._lib.X509_NAME_entry_count(x509_name)
+        attributes = []
+        for x in range(count):
+            entry = self._backend._lib.X509_NAME_get_entry(x509_name, x)
+            obj = self._backend._lib.X509_NAME_ENTRY_get_object(entry)
+            assert obj != self._backend._ffi.NULL
+            data = self._backend._lib.X509_NAME_ENTRY_get_data(entry)
+            assert data != self._backend._ffi.NULL
+            buf = self._backend._ffi.new("unsigned char **")
+            res = self._backend._lib.ASN1_STRING_to_UTF8(buf, data)
+            assert res >= 0
+            assert buf[0] != self._backend._ffi.NULL
+            buf = self._backend._ffi.gc(
+                buf, lambda buf: self._backend._lib.OPENSSL_free(buf[0])
+            )
+            value = self._backend._ffi.buffer(buf[0], res)[:].decode('utf8')
+            # Set to 80 on the recommendation of
+            # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html
+            buf_len = 80
+            buf = self._backend._ffi.new("char[]", buf_len)
+            res = self._backend._lib.OBJ_obj2txt(buf, buf_len, obj, 1)
+            assert res > 0
+            oid = self._backend._ffi.buffer(buf, res)[:].decode()
+
+            attributes.append(
+                x509.NameAttribute(
+                    x509.ObjectIdentifier(oid), value
+                )
+            )
+
+        return x509.Name(attributes)
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -104,6 +104,29 @@ def __repr__(self):
     dotted_string = utils.read_only_property("_dotted_string")
 
 
+class Name(object):
+    def __init__(self, attributes):
+        self._attributes = attributes
+
+    def get_attributes_for_oid(self, oid):
+        return [i for i in self._attributes if i.oid == oid]
+
+    def __eq__(self, other):
+        if not isinstance(other, Name):
+            return NotImplemented
+
+        return self._attributes == other._attributes
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __iter__(self):
+        return iter(self._attributes)
+
+    def __len__(self):
+        return len(self._attributes)
+
+
 OID_COMMON_NAME = ObjectIdentifier("2.5.4.3")
 OID_COUNTRY_NAME = ObjectIdentifier("2.5.4.6")
 OID_LOCALITY_NAME = ObjectIdentifier("2.5.4.7")
@@ -158,3 +181,15 @@ def not_valid_after(self):
         """
         Not after time (represented as UTC datetime)
         """
+
+    @abc.abstractproperty
+    def issuer(self):
+        """
+        Returns the issuer name object.
+        """
+
+    @abc.abstractproperty
+    def subject(self):
+        """
+        Returns the subject name object.
+        """


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -10,6 +10,8 @@
 
 import pytest
 
+import six
+
 from cryptography import x509
 from cryptography.hazmat.backends.interfaces import (
     DSABackend, EllipticCurveBackend, RSABackend, X509Backend
@@ -55,6 +57,171 @@ def test_load_der_cert(self, backend):
         fingerprint = binascii.hexlify(cert.fingerprint(hashes.SHA1()))
         assert fingerprint == b"6f49779533d565e8b7c1062503eab41492c38e4d"
 
+    def test_issuer(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "PKITS_data", "certs",
+                "Validpre2000UTCnotBeforeDateTest3EE.crt"
+            ),
+            x509.load_der_x509_certificate,
+            backend
+        )
+        issuer = cert.issuer
+        assert isinstance(issuer, x509.Name)
+        assert list(issuer) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'US'),
+            x509.NameAttribute(
+                x509.OID_ORGANIZATION_NAME, 'Test Certificates 2011'
+            ),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'Good CA')
+        ]
+        assert issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) == [
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'Good CA')
+        ]
+
+    def test_all_issuer_name_types(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "custom",
+                "all_supported_names.pem"
+            ),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        issuer = cert.issuer
+
+        assert isinstance(issuer, x509.Name)
+        assert list(issuer) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'US'),
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'CA'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Texas'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Illinois'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Chicago'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'Zero, LLC'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'One, LLC'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'common name 0'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'common name 1'),
+            x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, 'OU 0'),
+            x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, 'OU 1'),
+            x509.NameAttribute(x509.OID_DN_QUALIFIER, 'dnQualifier0'),
+            x509.NameAttribute(x509.OID_DN_QUALIFIER, 'dnQualifier1'),
+            x509.NameAttribute(x509.OID_SERIAL_NUMBER, '123'),
+            x509.NameAttribute(x509.OID_SERIAL_NUMBER, '456'),
+            x509.NameAttribute(x509.OID_TITLE, 'Title 0'),
+            x509.NameAttribute(x509.OID_TITLE, 'Title 1'),
+            x509.NameAttribute(x509.OID_SURNAME, 'Surname 0'),
+            x509.NameAttribute(x509.OID_SURNAME, 'Surname 1'),
+            x509.NameAttribute(x509.OID_GIVEN_NAME, 'Given Name 0'),
+            x509.NameAttribute(x509.OID_GIVEN_NAME, 'Given Name 1'),
+            x509.NameAttribute(x509.OID_PSEUDONYM, 'Incognito 0'),
+            x509.NameAttribute(x509.OID_PSEUDONYM, 'Incognito 1'),
+            x509.NameAttribute(x509.OID_GENERATION_QUALIFIER, 'Last Gen'),
+            x509.NameAttribute(x509.OID_GENERATION_QUALIFIER, 'Next Gen'),
+            x509.NameAttribute(x509.OID_DOMAIN_COMPONENT, 'dc0'),
+            x509.NameAttribute(x509.OID_DOMAIN_COMPONENT, 'dc1'),
+            x509.NameAttribute(x509.OID_EMAIL_ADDRESS, 'test0@test.local'),
+            x509.NameAttribute(x509.OID_EMAIL_ADDRESS, 'test1@test.local'),
+        ]
+
+    def test_subject(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "PKITS_data", "certs",
+                "Validpre2000UTCnotBeforeDateTest3EE.crt"
+            ),
+            x509.load_der_x509_certificate,
+            backend
+        )
+        subject = cert.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'US'),
+            x509.NameAttribute(
+                x509.OID_ORGANIZATION_NAME, 'Test Certificates 2011'
+            ),
+            x509.NameAttribute(
+                x509.OID_COMMON_NAME,
+                'Valid pre2000 UTC notBefore Date EE Certificate Test3'
+            )
+        ]
+        assert subject.get_attributes_for_oid(x509.OID_COMMON_NAME) == [
+            x509.NameAttribute(
+                x509.OID_COMMON_NAME,
+                'Valid pre2000 UTC notBefore Date EE Certificate Test3'
+            )
+        ]
+
+    def test_unicode_name(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "custom",
+                "utf8_common_name.pem"
+            ),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        assert cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME) == [
+            x509.NameAttribute(
+                x509.OID_COMMON_NAME,
+                six.u('We heart UTF8!\u2122')
+            )
+        ]
+        assert cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) == [
+            x509.NameAttribute(
+                x509.OID_COMMON_NAME,
+                six.u('We heart UTF8!\u2122')
+            )
+        ]
+
+    def test_all_subject_name_types(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "custom",
+                "all_supported_names.pem"
+            ),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        subject = cert.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'AU'),
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, 'DE'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'California'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'New York'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'San Francisco'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Ithaca'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'Org Zero, LLC'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'Org One, LLC'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'CN 0'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'CN 1'),
+            x509.NameAttribute(
+                x509.OID_ORGANIZATIONAL_UNIT_NAME, 'Engineering 0'
+            ),
+            x509.NameAttribute(
+                x509.OID_ORGANIZATIONAL_UNIT_NAME, 'Engineering 1'
+            ),
+            x509.NameAttribute(x509.OID_DN_QUALIFIER, 'qualified0'),
+            x509.NameAttribute(x509.OID_DN_QUALIFIER, 'qualified1'),
+            x509.NameAttribute(x509.OID_SERIAL_NUMBER, '789'),
+            x509.NameAttribute(x509.OID_SERIAL_NUMBER, '012'),
+            x509.NameAttribute(x509.OID_TITLE, 'Title IX'),
+            x509.NameAttribute(x509.OID_TITLE, 'Title X'),
+            x509.NameAttribute(x509.OID_SURNAME, 'Last 0'),
+            x509.NameAttribute(x509.OID_SURNAME, 'Last 1'),
+            x509.NameAttribute(x509.OID_GIVEN_NAME, 'First 0'),
+            x509.NameAttribute(x509.OID_GIVEN_NAME, 'First 1'),
+            x509.NameAttribute(x509.OID_PSEUDONYM, 'Guy Incognito 0'),
+            x509.NameAttribute(x509.OID_PSEUDONYM, 'Guy Incognito 1'),
+            x509.NameAttribute(x509.OID_GENERATION_QUALIFIER, '32X'),
+            x509.NameAttribute(x509.OID_GENERATION_QUALIFIER, 'Dreamcast'),
+            x509.NameAttribute(x509.OID_DOMAIN_COMPONENT, 'dc2'),
+            x509.NameAttribute(x509.OID_DOMAIN_COMPONENT, 'dc3'),
+            x509.NameAttribute(x509.OID_EMAIL_ADDRESS, 'test2@test.local'),
+            x509.NameAttribute(x509.OID_EMAIL_ADDRESS, 'test3@test.local'),
+        ]
+
     def test_load_good_ca_cert(self, backend):
         cert = _load_cert(
             os.path.join("x509", "PKITS_data", "certs", "GoodCACert.crt"),
@@ -301,3 +468,28 @@ def test_repr(self):
         assert repr(oid) == "<ObjectIdentifier(oid=2.5.4.3, name=commonName)>"
         oid = x509.ObjectIdentifier("oid1")
         assert repr(oid) == "<ObjectIdentifier(oid=oid1, name=Unknown OID)>"
+
+
+class TestName(object):
+    def test_eq(self):
+        name1 = x509.Name([
+            x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1'),
+            x509.NameAttribute(x509.ObjectIdentifier('oid2'), 'value2'),
+        ])
+        name2 = x509.Name([
+            x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1'),
+            x509.NameAttribute(x509.ObjectIdentifier('oid2'), 'value2'),
+        ])
+        assert name1 == name2
+
+    def test_ne(self):
+        name1 = x509.Name([
+            x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1'),
+            x509.NameAttribute(x509.ObjectIdentifier('oid2'), 'value2'),
+        ])
+        name2 = x509.Name([
+            x509.NameAttribute(x509.ObjectIdentifier('oid2'), 'value2'),
+            x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1'),
+        ])
+        assert name1 != name2
+        assert name1 != object()