From 2ee5e3c6240721120146d6d81c70e1b900029ee0 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 4 Jul 2015 20:09:46 +0000 Subject: parse SAN otherNames into OtherName instances rather than raising an exception Test added. --- docs/x509.rst | 14 ++++++++++++ src/_cffi_src/openssl/asn1.py | 2 ++ src/cryptography/hazmat/backends/openssl/x509.py | 17 +++++++++++++- src/cryptography/x509.py | 28 ++++++++++++++++++++++++ tests/test_x509_ext.py | 24 ++++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/docs/x509.rst b/docs/x509.rst index f94f50eb..eab1f53c 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -695,6 +695,20 @@ General Name Classes :type: :class:`ObjectIdentifier` +.. class:: OtherName + + .. versionadded:: 1.0 + + This corresponds to an "otherName." An otherName has a type identifier and a value represented in binary DER format. + + .. attribute:: type_id + + :type: :class:`ObjectIdentifier` + + .. attribute:: value + + :type: `bytes` + X.509 Extensions ~~~~~~~~~~~~~~~~ diff --git a/src/_cffi_src/openssl/asn1.py b/src/_cffi_src/openssl/asn1.py index 5210c7c9..01d6f4c2 100644 --- a/src/_cffi_src/openssl/asn1.py +++ b/src/_cffi_src/openssl/asn1.py @@ -155,6 +155,8 @@ int ASN1_UTCTIME_check(ASN1_UTCTIME *); /* Not a macro, const on openssl 1.0 */ int ASN1_STRING_set_default_mask_asc(char *); + +int i2d_ASN1_TYPE(ASN1_TYPE *, unsigned char **); """ CUSTOMIZATIONS = """ diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py index 80e5f2b1..e720bfdb 100644 --- a/src/cryptography/hazmat/backends/openssl/x509.py +++ b/src/cryptography/hazmat/backends/openssl/x509.py @@ -47,6 +47,17 @@ def _asn1_string_to_utf8(backend, asn1_string): return backend._ffi.buffer(buf[0], res)[:].decode('utf8') +def _asn1_to_der(backend, asn1_type): + buf = backend._ffi.new("unsigned char **") + res = backend._lib.i2d_ASN1_TYPE(asn1_type, buf) + assert res >= 0 + assert buf[0] != backend._ffi.NULL + buf = backend._ffi.gc( + buf, lambda buffer: backend._lib.OPENSSL_free(buffer[0]) + ) + return backend._ffi.buffer(buf[0], res)[:] + + def _decode_x509_name_entry(backend, x509_name_entry): obj = backend._lib.X509_NAME_ENTRY_get_object(x509_name_entry) assert obj != backend._ffi.NULL @@ -157,8 +168,12 @@ def _decode_general_name(backend, gn): 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: - # otherName, x400Address or ediPartyName + # x400Address or ediPartyName raise x509.UnsupportedGeneralNameType( "{0} is not a supported type".format( x509._GENERAL_NAMES.get(gn.type, gn.type) diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index afd28f20..48949b61 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -1045,6 +1045,34 @@ class IPAddress(object): return not self == other +@utils.register_interface(GeneralName) +class OtherName(object): + def __init__(self, type_id, value): + if not isinstance(type_id, ObjectIdentifier): + raise TypeError("type_id must be an ObjectIdentifier") + if not isinstance(value, bytes): + raise TypeError("value must be a binary string") + + self._type_id = type_id + self._value = value + + type_id = utils.read_only_property("_type_id") + value = utils.read_only_property("_value") + + def __repr__(self): + return "".format( + self.type_id, self.value) + + def __eq__(self, other): + if not isinstance(other, OtherName): + return NotImplemented + + return self.type_id == other.type_id and self.value == other.value + + def __ne__(self, other): + return not self == other + + class GeneralNames(object): def __init__(self, general_names): if not all(isinstance(x, GeneralName) for x in general_names): diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index d15d6669..06adaa37 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -1578,6 +1578,30 @@ class TestRSASubjectAlternativeNameExtension(object): assert 'Invalid rfc822name value' in str(exc.value) + def test_other_name(self, backend): + cert = _load_cert( + os.path.join( + "x509", "custom", "san_other_name.pem" + ), + x509.load_pem_x509_certificate, + backend + ) + + ext = cert.extensions.get_extension_for_oid( + x509.OID_SUBJECT_ALTERNATIVE_NAME + ) + assert ext is not None + assert ext.critical is False + + assert len(ext.value) == 1 + assert list(ext.value)[0] == \ + x509.OtherName( + x509.ObjectIdentifier("1.2.3.4"), + b'\x16\x0bHello World') + + othernames = ext.value.get_values_for_type(x509.OtherName) + assert othernames == [b'\x16\x0bHello World'] + @pytest.mark.requires_backend_interface(interface=RSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) -- cgit v1.2.3 From 18b6fc84fcb671412aaaf453f623a44a30a1a2a3 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 5 Jul 2015 21:44:51 +0000 Subject: additional tests and doc spelling error fix for OtherName --- docs/x509.rst | 2 +- tests/test_x509_ext.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/x509.rst b/docs/x509.rst index eab1f53c..7ee4516d 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -699,7 +699,7 @@ General Name Classes .. versionadded:: 1.0 - This corresponds to an "otherName." An otherName has a type identifier and a value represented in binary DER format. + This corresponds to an ``otherName.`` An ``otherName`` has a type identifier and a value represented in binary DER format. .. attribute:: type_id diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index 06adaa37..e6ee7d66 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -1147,6 +1147,55 @@ class TestIPAddress(object): assert gn != object() +class TestOtherName(object): + def test_invalid_args(self): + with pytest.raises(TypeError): + x509.OtherName(b"notanobjectidentifier", b"derdata") + + with pytest.raises(TypeError): + x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), u"notderdata") + + def test_repr(self): + gn = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), b"derdata") + if six.PY3: + assert repr(gn) == ( + ", value=b'derdata')>" + ) + else: + assert repr(gn) == ( + ", value='derdata')>" + ) + + gn = x509.OtherName(x509.ObjectIdentifier("2.5.4.65"), b"derdata") + if six.PY3: + assert repr(gn) == ( + ", value=b'derdata')>" + ) + else: + assert repr(gn) == ( + ", value='derdata')>" + ) + + def test_eq(self): + gn = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), b"derdata") + gn2 = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), b"derdata") + assert gn == gn2 + + def test_ne(self): + gn = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), b"derdata") + assert gn != object() + + gn2 = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), b"derdata2") + assert gn != gn2 + + gn2 = x509.OtherName(x509.ObjectIdentifier("1.2.3.5"), b"derdata") + assert gn != gn2 + + class TestGeneralNames(object): def test_get_values_for_type(self): gns = x509.GeneralNames( -- cgit v1.2.3 From d2afad325e2e9c52765b4a696d6f6b646c4e855b Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 6 Jul 2015 22:37:53 +0000 Subject: special-case GeneralNames.get_values_for_type to return OtherName instances directly rather than their value properties; tests updated --- docs/x509.rst | 1 + src/cryptography/x509.py | 8 +++++++- tests/test_x509_ext.py | 9 ++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/x509.rst b/docs/x509.rst index 7ee4516d..2dac33bc 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -981,6 +981,7 @@ X.509 Extensions :ref:`general name classes `. :returns: A list of values extracted from the matched general names. + The type of the returned values depends on the :class:`GeneralName`. .. doctest:: diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index 48949b61..c9d0c260 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -1090,7 +1090,13 @@ class GeneralNames(object): return len(self._general_names) def get_values_for_type(self, type): - return [i.value for i in self if isinstance(i, type)] + # Return the value of each GeneralName, except for OtherName instances + # which we return directly because it has two important properties not + # just one value. + objs = (i for i in self if isinstance(i, type)) + if type != OtherName: + objs = (i.value for i in objs) + return list(objs) def __repr__(self): return "".format(self._general_names) diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index e6ee7d66..993802b8 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -1642,14 +1642,13 @@ class TestRSASubjectAlternativeNameExtension(object): assert ext is not None assert ext.critical is False + expected = x509.OtherName(x509.ObjectIdentifier("1.2.3.4"), + b'\x16\x0bHello World') assert len(ext.value) == 1 - assert list(ext.value)[0] == \ - x509.OtherName( - x509.ObjectIdentifier("1.2.3.4"), - b'\x16\x0bHello World') + assert list(ext.value)[0] == expected othernames = ext.value.get_values_for_type(x509.OtherName) - assert othernames == [b'\x16\x0bHello World'] + assert othernames == [expected] @pytest.mark.requires_backend_interface(interface=RSABackend) -- cgit v1.2.3