diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rwxr-xr-x | .travis/upload_coverage.sh | 10 | ||||
-rw-r--r-- | CHANGELOG.rst | 6 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 4 | ||||
-rw-r--r-- | docs/index.rst | 2 | ||||
-rw-r--r-- | docs/x509/index.rst | 14 | ||||
-rw-r--r-- | docs/x509/reference.rst (renamed from docs/x509.rst) | 10 | ||||
-rw-r--r-- | src/_cffi_src/openssl/asn1.py | 1 | ||||
-rw-r--r-- | src/_cffi_src/openssl/x509v3.py | 3 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 22 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/x509.py | 31 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/asymmetric/rsa.py | 14 | ||||
-rw-r--r-- | src/cryptography/x509.py | 19 | ||||
-rw-r--r-- | tests/conftest.py | 6 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_rsa.py | 16 | ||||
-rw-r--r-- | tests/test_x509.py | 27 | ||||
-rw-r--r-- | tests/test_x509_ext.py | 72 |
17 files changed, 236 insertions, 23 deletions
diff --git a/.travis.yml b/.travis.yml index 8ad514fd..b8206e03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -120,7 +120,7 @@ script: - ./.travis/run.sh after_success: - - source ~/.venv/bin/activate && bash <(curl -s https://codecov.io/bash) -e TRAVIS_OS_NAME,TOXENV,OPENSSL + - ./.travis/upload_coverage.sh notifications: irc: diff --git a/.travis/upload_coverage.sh b/.travis/upload_coverage.sh new file mode 100755 index 00000000..554116f7 --- /dev/null +++ b/.travis/upload_coverage.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e +set -x + +NO_COVERAGE_TOXENVS=(pypy pypy3 pep8 py3pep8 docs) +if ! [[ "${NO_COVERAGE_TOXENVS[*]}" =~ "${TOXENV}" ]]; then + source ~/.venv/bin/activate + bash <(curl -s https://codecov.io/bash) -e TRAVIS_OS_NAME,TOXENV,OPENSSL +fi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 475a2a35..85f84477 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -93,7 +93,7 @@ Changelog Note that unsupported extensions with the critical flag raise :class:`~cryptography.x509.UnsupportedExtension` while unsupported extensions set to non-critical are silently ignored. Read the - :doc:`X.509 documentation</x509>` for more information. + :doc:`X.509 documentation</x509/index>` for more information. 0.8.2 - 2015-04-10 ~~~~~~~~~~~~~~~~~~ @@ -120,7 +120,7 @@ Changelog from :mod:`~cryptography.hazmat.primitives.interfaces` to :mod:`~cryptography.hazmat.primitives.kdf`. * Added support for parsing X.509 names. See the - :doc:`X.509 documentation</x509>` for more information. + :doc:`X.509 documentation</x509/index>` for more information. * Added :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key` to support loading of DER encoded private keys and @@ -256,7 +256,7 @@ Changelog support the loading of OpenSSH public keys (:rfc:`4253`). Only RSA and DSA keys are currently supported. * Added initial support for X.509 certificate parsing. See the - :doc:`X.509 documentation</x509>` for more information. + :doc:`X.509 documentation</x509/index>` for more information. 0.6.1 - 2014-10-15 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 7839f346..8d51f0d7 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -97,8 +97,8 @@ all begin with ``-----BEGIN {format}-----`` and end with ``-----END .. note:: A PEM block which starts with ``-----BEGIN CERTIFICATE-----`` is not a - public or private key, it's an :doc:`X.509 Certificate </x509>`. You can - load it using :func:`~cryptography.x509.load_pem_x509_certificate` and + public or private key, it's an :doc:`X.509 Certificate </x509/index>`. You + can load it using :func:`~cryptography.x509.load_pem_x509_certificate` and extract the public key with :meth:`Certificate.public_key <cryptography.x509.Certificate.public_key>`. diff --git a/docs/index.rst b/docs/index.rst index 35f80a2d..5c26a754 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,7 +63,7 @@ The recipes layer :maxdepth: 2 fernet - x509 + x509/index random-numbers exceptions faq diff --git a/docs/x509/index.rst b/docs/x509/index.rst new file mode 100644 index 00000000..c3fa1ed2 --- /dev/null +++ b/docs/x509/index.rst @@ -0,0 +1,14 @@ +X.509 +===== + +X.509 is an ITU-T standard for a `public key infrastructure`_. X.509v3 is +defined in :rfc:`5280` (which obsoletes :rfc:`2459` and :rfc:`3280`). X.509 +certificates are commonly used in protocols like `TLS`_. + +.. toctree:: + :maxdepth: 2 + + reference + +.. _`public key infrastructure`: https://en.wikipedia.org/wiki/Public_key_infrastructure +.. _`TLS`: https://en.wikipedia.org/wiki/Transport_Layer_Security diff --git a/docs/x509.rst b/docs/x509/reference.rst index bcb6ee66..9179468f 100644 --- a/docs/x509.rst +++ b/docs/x509/reference.rst @@ -1,5 +1,5 @@ -X.509 -===== +X.509 Reference +=============== .. currentmodule:: cryptography.x509 @@ -86,10 +86,6 @@ X.509 -----END CERTIFICATE----- """.strip() -X.509 is an ITU-T standard for a `public key infrastructure`_. X.509v3 is -defined in :rfc:`5280` (which obsoletes :rfc:`2459` and :rfc:`3280`). X.509 -certificates are commonly used in protocols like `TLS`_. - Loading Certificates ~~~~~~~~~~~~~~~~~~~~ @@ -1582,7 +1578,5 @@ Exceptions types can be found in `RFC 5280 section 4.2.1.6`_. -.. _`public key infrastructure`: https://en.wikipedia.org/wiki/Public_key_infrastructure -.. _`TLS`: https://en.wikipedia.org/wiki/Transport_Layer_Security .. _`RFC 5280 section 4.2.1.1`: https://tools.ietf.org/html/rfc5280#section-4.2.1.1 .. _`RFC 5280 section 4.2.1.6`: https://tools.ietf.org/html/rfc5280#section-4.2.1.6 diff --git a/src/_cffi_src/openssl/asn1.py b/src/_cffi_src/openssl/asn1.py index 01d6f4c2..5f8ca697 100644 --- a/src/_cffi_src/openssl/asn1.py +++ b/src/_cffi_src/openssl/asn1.py @@ -157,6 +157,7 @@ int ASN1_UTCTIME_check(ASN1_UTCTIME *); int ASN1_STRING_set_default_mask_asc(char *); int i2d_ASN1_TYPE(ASN1_TYPE *, unsigned char **); +ASN1_TYPE *d2i_ASN1_TYPE(ASN1_TYPE **, const unsigned char **, long); """ CUSTOMIZATIONS = """ diff --git a/src/_cffi_src/openssl/x509v3.py b/src/_cffi_src/openssl/x509v3.py index 0f5306d0..8e42b65d 100644 --- a/src/_cffi_src/openssl/x509v3.py +++ b/src/_cffi_src/openssl/x509v3.py @@ -193,6 +193,9 @@ void AUTHORITY_KEYID_free(AUTHORITY_KEYID *); NAME_CONSTRAINTS *NAME_CONSTRAINTS_new(void); void NAME_CONSTRAINTS_free(NAME_CONSTRAINTS *); +OTHERNAME *OTHERNAME_new(void); +void OTHERNAME_free(OTHERNAME *); + void *X509V3_set_ctx_nodb(X509V3_CTX *); int i2d_GENERAL_NAMES(GENERAL_NAMES *, unsigned char **); diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 7255b470..637b28cc 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -199,6 +199,28 @@ def _encode_subject_alt_name(backend, san): ) gn.type = backend._lib.GEN_IPADD gn.d.iPAddress = ipaddr + elif isinstance(alt_name, x509.OtherName): + gn = backend._lib.GENERAL_NAME_new() + assert gn != backend._ffi.NULL + other_name = backend._lib.OTHERNAME_new() + assert other_name != backend._ffi.NULL + + type_id = backend._lib.OBJ_txt2obj( + alt_name.type_id.dotted_string.encode('ascii'), 1 + ) + assert type_id != backend._ffi.NULL + data = backend._ffi.new("unsigned char[]", alt_name.value) + data_ptr_ptr = backend._ffi.new("unsigned char **") + data_ptr_ptr[0] = data + value = backend._lib.d2i_ASN1_TYPE( + backend._ffi.NULL, data_ptr_ptr, len(alt_name.value) + ) + if value == backend._ffi.NULL: + raise ValueError("Invalid ASN.1 data") + other_name.type_id = type_id + other_name.value = value + gn.type = backend._lib.GEN_OTHERNAME + gn.d.otherName = other_name else: raise NotImplementedError( "Only DNSName and RegisteredID supported right now" diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py index d78c60fa..096cbc9e 100644 --- a/src/cryptography/hazmat/backends/openssl/x509.py +++ b/src/cryptography/hazmat/backends/openssl/x509.py @@ -141,11 +141,32 @@ def _decode_general_name(backend, gn): oid = _obj2txt(backend, gn.d.registeredID) return x509.RegisteredID(x509.ObjectIdentifier(oid)) elif gn.type == backend._lib.GEN_IPADD: - return x509.IPAddress( - ipaddress.ip_address( - _asn1_string_to_bytes(backend, gn.d.iPAddress) - ) - ) + data = _asn1_string_to_bytes(backend, gn.d.iPAddress) + data_len = len(data) + if data_len == 8 or data_len == 32: + # This is an IPv4 or IPv6 Network and not a single IP. This + # type of data appears in Name Constraints. Unfortunately, + # ipaddress doesn't support packed bytes + netmask. Additionally, + # IPv6Network can only handle CIDR rather than the full 16 byte + # netmask. To handle this we convert the netmask to integer, then + # find the first 0 bit, which will be the prefix. If another 1 + # bit is present after that the netmask is invalid. + base = ipaddress.ip_address(data[:data_len // 2]) + netmask = ipaddress.ip_address(data[data_len // 2:]) + bits = bin(int(netmask))[2:] + prefix = bits.find('0') + # If no 0 bits are found it is a /32 or /128 + if prefix == -1: + prefix = len(bits) + + if "1" in bits[prefix:]: + raise ValueError("Invalid netmask") + + ip = ipaddress.ip_network(base.exploded + u"/{0}".format(prefix)) + else: + ip = ipaddress.ip_address(data) + + return x509.IPAddress(ip) elif gn.type == backend._lib.GEN_DIRNAME: return x509.DirectoryName( _decode_x509_name(backend, gn.d.directoryName) diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py index 89eac4d4..41b0089e 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -307,6 +307,17 @@ class RSAPrivateNumbers(object): def __ne__(self, other): return not self == other + def __hash__(self): + return hash(( + self.p, + self.q, + self.d, + self.dmp1, + self.dmq1, + self.iqmp, + self.public_numbers, + )) + class RSAPublicNumbers(object): def __init__(self, e, n): @@ -336,3 +347,6 @@ class RSAPublicNumbers(object): def __ne__(self, other): return not self == other + + def __hash__(self): + return hash((self.e, self.n)) diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index 33c64168..8bed79e2 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -6,8 +6,11 @@ from __future__ import absolute_import, division, print_function import abc import ipaddress +from email.utils import parseaddr from enum import Enum +import idna + import six from cryptography import utils @@ -901,7 +904,23 @@ class RFC822Name(object): if not isinstance(value, six.text_type): raise TypeError("value must be a unicode string") + name, address = parseaddr(value) + parts = address.split(u"@") + if name or not address: + # parseaddr has found a name (e.g. Name <email>) or the entire + # value is an empty string. + raise ValueError("Invalid rfc822name value") + elif len(parts) == 1: + # Single label email name. This is valid for local delivery. + # No IDNA encoding needed since there is no domain component. + encoded = address.encode("ascii") + else: + # A normal email of the form user@domain.com. Let's attempt to + # encode the domain component and reconstruct the address. + encoded = parts[0].encode("ascii") + b"@" + idna.encode(parts[1]) + self._value = value + self._encoded = encoded value = utils.read_only_property("_value") diff --git a/tests/conftest.py b/tests/conftest.py index 6599a643..bdd17fb7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,10 +12,10 @@ from .utils import check_backend_support, select_backends, skip_if_empty def pytest_generate_tests(metafunc): - names = metafunc.config.getoption("--backend") - selected_backends = select_backends(names, _available_backends()) - if "backend" in metafunc.fixturenames: + names = metafunc.config.getoption("--backend") + selected_backends = select_backends(names, _available_backends()) + filtered_backends = [] required = metafunc.function.requires_backend_interface required_interfaces = [ diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index bfeab8dd..0c5f7042 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -1705,6 +1705,22 @@ class TestRSANumbersEquality(object): ) assert num != object() + def test_public_numbers_hash(self): + pub1 = RSAPublicNumbers(3, 17) + pub2 = RSAPublicNumbers(3, 17) + pub3 = RSAPublicNumbers(7, 21) + + assert hash(pub1) == hash(pub2) + assert hash(pub1) != hash(pub3) + + def test_private_numbers_hash(self): + priv1 = RSAPrivateNumbers(1, 2, 3, 4, 5, 6, RSAPublicNumbers(1, 2)) + priv2 = RSAPrivateNumbers(1, 2, 3, 4, 5, 6, RSAPublicNumbers(1, 2)) + priv3 = RSAPrivateNumbers(1, 2, 3, 4, 5, 6, RSAPublicNumbers(1, 3)) + + assert hash(priv1) == hash(priv2) + assert hash(priv1) != hash(priv3) + class TestRSAPrimeFactorRecovery(object): @pytest.mark.parametrize( diff --git a/tests/test_x509.py b/tests/test_x509.py index 9b6b8826..cb617268 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1004,6 +1004,10 @@ class TestCertificateSigningRequestBuilder(object): ])), x509.IPAddress(ipaddress.ip_address(u"127.0.0.1")), x509.IPAddress(ipaddress.ip_address(u"ff::")), + x509.OtherName( + type_id=x509.ObjectIdentifier("1.2.3.3.3.3"), + value=b"0\x03\x02\x01\x05" + ), ]), critical=False, ).sign(private_key, hashes.SHA256(), backend) @@ -1026,8 +1030,31 @@ class TestCertificateSigningRequestBuilder(object): ])), x509.IPAddress(ipaddress.ip_address(u"127.0.0.1")), x509.IPAddress(ipaddress.ip_address(u"ff::")), + x509.OtherName( + type_id=x509.ObjectIdentifier("1.2.3.3.3.3"), + value=b"0\x03\x02\x01\x05" + ), ] + def test_invalid_asn1_othername(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + builder = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COMMON_NAME, u"SAN"), + ]) + ).add_extension( + x509.SubjectAlternativeName([ + x509.OtherName( + type_id=x509.ObjectIdentifier("1.2.3.3.3.3"), + value=b"\x01\x02\x01\x05" + ), + ]), + critical=False, + ) + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA256(), backend) + def test_subject_alt_name_unsupported_general_name(self, backend): private_key = RSA_KEY_2048.private_key(backend) diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index 993802b8..84a40995 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -1087,6 +1087,24 @@ class TestDirectoryName(object): assert gn != object() +class TestRFC822Name(object): + def test_invalid_email(self): + with pytest.raises(ValueError): + x509.RFC822Name(u"Name <email>") + + with pytest.raises(ValueError): + x509.RFC822Name(u"") + + def test_single_label(self): + gn = x509.RFC822Name(u"administrator") + assert gn.value == u"administrator" + + def test_idna(self): + gn = x509.RFC822Name(u"email@em\xe5\xefl.com") + assert gn.value == u"email@em\xe5\xefl.com" + assert gn._encoded == b"email@xn--eml-vla4c.com" + + class TestRegisteredID(object): def test_not_oid(self): with pytest.raises(TypeError): @@ -2184,6 +2202,60 @@ class TestNameConstraintsExtension(object): ] ) + def test_permitted_excluded_with_ips(self, backend): + cert = _load_cert( + os.path.join( + "x509", "custom", "nc_permitted_excluded.pem" + ), + x509.load_pem_x509_certificate, + backend + ) + nc = cert.extensions.get_extension_for_oid( + x509.OID_NAME_CONSTRAINTS + ).value + assert nc == x509.NameConstraints( + permitted_subtrees=[ + x509.IPAddress(ipaddress.IPv4Network(u"192.168.0.0/24")), + x509.IPAddress(ipaddress.IPv6Network(u"FF:0:0:0:0:0:0:0/96")), + ], + excluded_subtrees=[ + x509.DNSName(u".domain.com"), + x509.UniformResourceIdentifier(u"http://test.local"), + ] + ) + + def test_single_ip_netmask(self, backend): + cert = _load_cert( + os.path.join( + "x509", "custom", "nc_single_ip_netmask.pem" + ), + x509.load_pem_x509_certificate, + backend + ) + nc = cert.extensions.get_extension_for_oid( + x509.OID_NAME_CONSTRAINTS + ).value + assert nc == x509.NameConstraints( + permitted_subtrees=[ + x509.IPAddress(ipaddress.IPv6Network(u"FF:0:0:0:0:0:0:0/128")), + x509.IPAddress(ipaddress.IPv4Network(u"192.168.0.1/32")), + ], + excluded_subtrees=None + ) + + def test_invalid_netmask(self, backend): + cert = _load_cert( + os.path.join( + "x509", "custom", "nc_invalid_ip_netmask.pem" + ), + x509.load_pem_x509_certificate, + backend + ) + with pytest.raises(ValueError): + cert.extensions.get_extension_for_oid( + x509.OID_NAME_CONSTRAINTS + ) + class TestDistributionPoint(object): def test_distribution_point_full_name_not_general_names(self): |