diff options
-rw-r--r-- | docs/x509.rst | 4 | ||||
-rw-r--r-- | src/_cffi_src/openssl/ssl.py | 1 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 56 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/x509.py | 54 | ||||
-rw-r--r-- | src/cryptography/x509.py | 18 | ||||
-rw-r--r-- | tests/test_x509.py | 97 |
6 files changed, 206 insertions, 24 deletions
diff --git a/docs/x509.rst b/docs/x509.rst index 2dac33bc..bcb6ee66 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -328,6 +328,8 @@ X.509 Certificate Object .. method:: public_bytes(encoding) + .. versionadded:: 1.0 + :param encoding: The :class:`~cryptography.hazmat.primitives.serialization.Encoding` that will be used to serialize the certificate. @@ -435,6 +437,8 @@ X.509 CSR (Certificate Signing Request) Object .. method:: public_bytes(encoding) + .. versionadded:: 1.0 + :param encoding: The :class:`~cryptography.hazmat.primitives.serialization.Encoding` that will be used to serialize the certificate request. diff --git a/src/_cffi_src/openssl/ssl.py b/src/_cffi_src/openssl/ssl.py index fa0aefc8..5841ee2f 100644 --- a/src/_cffi_src/openssl/ssl.py +++ b/src/_cffi_src/openssl/ssl.py @@ -182,6 +182,7 @@ int SSL_get_shutdown(const SSL *); int SSL_pending(const SSL *); int SSL_write(SSL *, const void *, int); int SSL_read(SSL *, void *, int); +int SSL_peek(SSL *, void *, int); X509 *SSL_get_peer_certificate(const SSL *); int SSL_get_ex_data_X509_STORE_CTX_idx(void); diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 73a58637..d6493778 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -8,6 +8,8 @@ import collections import itertools from contextlib import contextmanager +import idna + import six from cryptography import utils, x509 @@ -136,6 +138,56 @@ def _encode_basic_constraints(backend, basic_constraints): return pp, r +def _encode_subject_alt_name(backend, san): + general_names = backend._lib.GENERAL_NAMES_new() + assert general_names != backend._ffi.NULL + general_names = backend._ffi.gc( + general_names, backend._lib.GENERAL_NAMES_free + ) + + for alt_name in san: + if isinstance(alt_name, x509.DNSName): + gn = backend._lib.GENERAL_NAME_new() + assert gn != backend._ffi.NULL + gn.type = backend._lib.GEN_DNS + + ia5 = backend._lib.ASN1_IA5STRING_new() + assert ia5 != backend._ffi.NULL + + if alt_name.value.startswith(u"*."): + value = b"*." + idna.encode(alt_name.value[2:]) + else: + value = idna.encode(alt_name.value) + + res = backend._lib.ASN1_STRING_set(ia5, value, len(value)) + assert res == 1 + gn.d.dNSName = ia5 + elif isinstance(alt_name, x509.RegisteredID): + gn = backend._lib.GENERAL_NAME_new() + assert gn != backend._ffi.NULL + gn.type = backend._lib.GEN_RID + obj = backend._lib.OBJ_txt2obj( + alt_name.value.dotted_string.encode('ascii'), 1 + ) + assert obj != backend._ffi.NULL + gn.d.registeredID = obj + else: + raise NotImplementedError( + "Only DNSName and RegisteredID supported right now" + ) + + res = backend._lib.sk_GENERAL_NAME_push(general_names, gn) + assert res != 0 + + pp = backend._ffi.new("unsigned char **") + r = backend._lib.i2d_GENERAL_NAMES(general_names, pp) + assert r > 0 + pp = backend._ffi.gc( + pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + ) + return pp, r + + @utils.register_interface(CipherBackend) @utils.register_interface(CMACBackend) @utils.register_interface(DERSerializationBackend) @@ -841,12 +893,14 @@ class Backend(object): self._lib.sk_X509_EXTENSION_free, ) for extension in builder._extensions: - obj = _txt2obj(self, extension.oid.dotted_string) if isinstance(extension.value, x509.BasicConstraints): pp, r = _encode_basic_constraints(self, extension.value) + elif isinstance(extension.value, x509.SubjectAlternativeName): + pp, r = _encode_subject_alt_name(self, extension.value) else: raise NotImplementedError('Extension not yet supported.') + obj = _txt2obj(self, extension.oid.dotted_string) extension = self._lib.X509_EXTENSION_create_by_OBJ( self._ffi.NULL, obj, diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py index e720bfdb..399e6a6e 100644 --- a/src/cryptography/hazmat/backends/openssl/x509.py +++ b/src/cryptography/hazmat/backends/openssl/x509.py @@ -36,6 +36,14 @@ def _asn1_integer_to_int(backend, asn1_int): return backend._bn_to_int(bn) +def _asn1_string_to_bytes(backend, asn1_string): + return backend._ffi.buffer(asn1_string.data, asn1_string.length)[:] + + +def _asn1_string_to_ascii(backend, asn1_string): + return _asn1_string_to_bytes(backend, asn1_string).decode("ascii") + + def _asn1_string_to_utf8(backend, asn1_string): buf = backend._ffi.new("unsigned char **") res = backend._lib.ASN1_STRING_to_UTF8(buf, asn1_string) @@ -92,7 +100,7 @@ def _decode_general_names(backend, gns): def _decode_general_name(backend, gn): if gn.type == backend._lib.GEN_DNS: - data = backend._ffi.buffer(gn.d.dNSName.data, gn.d.dNSName.length)[:] + data = _asn1_string_to_bytes(backend, gn.d.dNSName) if data.startswith(b"*."): # This is a wildcard name. We need to remove the leading wildcard, # IDNA decode, then re-add the wildcard. Wildcard characters should @@ -109,10 +117,7 @@ def _decode_general_name(backend, gn): return x509.DNSName(decoded) elif gn.type == backend._lib.GEN_URI: - data = backend._ffi.buffer( - gn.d.uniformResourceIdentifier.data, - gn.d.uniformResourceIdentifier.length - )[:].decode("ascii") + data = _asn1_string_to_ascii(backend, gn.d.uniformResourceIdentifier) parsed = urllib_parse.urlparse(data) hostname = idna.decode(parsed.hostname) if parsed.port: @@ -138,9 +143,7 @@ def _decode_general_name(backend, gn): elif gn.type == backend._lib.GEN_IPADD: return x509.IPAddress( ipaddress.ip_address( - backend._ffi.buffer( - gn.d.iPAddress.data, gn.d.iPAddress.length - )[:] + _asn1_string_to_bytes(backend, gn.d.iPAddress) ) ) elif gn.type == backend._lib.GEN_DIRNAME: @@ -148,9 +151,7 @@ def _decode_general_name(backend, gn): _decode_x509_name(backend, gn.d.directoryName) ) elif gn.type == backend._lib.GEN_EMAIL: - data = backend._ffi.buffer( - gn.d.rfc822Name.data, gn.d.rfc822Name.length - )[:].decode("ascii") + data = _asn1_string_to_ascii(backend, gn.d.rfc822Name) name, address = parseaddr(data) parts = address.split(u"@") if name or len(parts) > 2 or not address: @@ -240,15 +241,12 @@ class _Certificate(object): def __ne__(self, other): return not self == other + def __hash__(self): + return hash(self.public_bytes(serialization.Encoding.DER)) + def fingerprint(self, algorithm): h = hashes.Hash(algorithm, self._backend) - bio = self._backend._create_mem_bio() - res = self._backend._lib.i2d_X509_bio( - bio, self._x509 - ) - assert res == 1 - der = self._backend._read_mem_bio(bio) - h.update(der) + h.update(self.public_bytes(serialization.Encoding.DER)) return h.finalize() @property @@ -295,11 +293,10 @@ class _Certificate(object): generalized_time = self._backend._ffi.gc( generalized_time, self._backend._lib.ASN1_GENERALIZEDTIME_free ) - time = self._backend._ffi.string( - self._backend._lib.ASN1_STRING_data( - self._backend._ffi.cast("ASN1_STRING *", generalized_time) - ) - ).decode("ascii") + time = _asn1_string_to_ascii( + self._backend, + self._backend._ffi.cast("ASN1_STRING *", generalized_time) + ) return datetime.datetime.strptime(time, "%Y%m%d%H%M%SZ") @property @@ -716,6 +713,17 @@ class _CertificateSigningRequest(object): self._backend = backend self._x509_req = x509_req + def __eq__(self, other): + if not isinstance(other, _CertificateSigningRequest): + return NotImplemented + + self_bytes = self.public_bytes(serialization.Encoding.DER) + other_bytes = other.public_bytes(serialization.Encoding.DER) + return self_bytes == other_bytes + + def __ne__(self, other): + return not self == other + def public_key(self): pkey = self._backend._lib.X509_REQ_get_pubkey(self._x509_req) assert pkey != self._backend._ffi.NULL diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index c9d0c260..d9d6db4a 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -1358,6 +1358,12 @@ class Certificate(object): """ @abc.abstractmethod + def __hash__(self): + """ + Computes a hash. + """ + + @abc.abstractmethod def public_bytes(self, encoding): """ Serializes the certificate to PEM or DER format. @@ -1426,6 +1432,18 @@ class CertificateRevocationList(object): @six.add_metaclass(abc.ABCMeta) class CertificateSigningRequest(object): @abc.abstractmethod + def __eq__(self, other): + """ + Checks equality. + """ + + @abc.abstractmethod + def __ne__(self, other): + """ + Checks not equal. + """ + + @abc.abstractmethod def public_key(self): """ Returns the public key diff --git a/tests/test_x509.py b/tests/test_x509.py index 1e0c9cdc..ccb24d7f 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -347,6 +347,29 @@ class TestRSACertificate(object): assert cert != cert2 assert cert != object() + def test_hash(self, backend): + cert1 = _load_cert( + os.path.join("x509", "custom", "post2000utctime.pem"), + x509.load_pem_x509_certificate, + backend + ) + cert2 = _load_cert( + os.path.join("x509", "custom", "post2000utctime.pem"), + x509.load_pem_x509_certificate, + backend + ) + cert3 = _load_cert( + os.path.join( + "x509", "PKITS_data", "certs", + "ValidGeneralizedTimenotAfterDateTest8EE.crt" + ), + x509.load_der_x509_certificate, + backend + ) + + assert hash(cert1) == hash(cert2) + assert hash(cert1) != hash(cert3) + def test_version_1_cert(self, backend): cert = _load_cert( os.path.join("x509", "v1_cert.pem"), @@ -694,6 +717,35 @@ class TestRSACertificateRequest(object): serialized = request.public_bytes(encoding) assert serialized == request_bytes + def test_eq(self, backend): + request1 = _load_cert( + os.path.join("x509", "requests", "rsa_sha1.pem"), + x509.load_pem_x509_csr, + backend + ) + request2 = _load_cert( + os.path.join("x509", "requests", "rsa_sha1.pem"), + x509.load_pem_x509_csr, + backend + ) + + assert request1 == request2 + + def test_ne(self, backend): + request1 = _load_cert( + os.path.join("x509", "requests", "rsa_sha1.pem"), + x509.load_pem_x509_csr, + backend + ) + request2 = _load_cert( + os.path.join("x509", "requests", "san_rsa_sha1.pem"), + x509.load_pem_x509_csr, + backend + ) + + assert request1 != request2 + assert request1 != object() + @pytest.mark.requires_backend_interface(interface=X509Backend) class TestCertificateSigningRequestBuilder(object): @@ -911,6 +963,51 @@ class TestCertificateSigningRequestBuilder(object): ]) ) + def test_subject_alt_names(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + csr = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COMMON_NAME, u"SAN"), + ]) + ).add_extension( + x509.SubjectAlternativeName([ + x509.DNSName(u"example.com"), + x509.DNSName(u"*.example.com"), + x509.RegisteredID(x509.ObjectIdentifier("1.2.3.4.5.6.7")), + ]), + critical=False, + ).sign(private_key, hashes.SHA256(), backend) + + assert len(csr.extensions) == 1 + ext = csr.extensions.get_extension_for_oid( + x509.OID_SUBJECT_ALTERNATIVE_NAME + ) + assert not ext.critical + assert ext.oid == x509.OID_SUBJECT_ALTERNATIVE_NAME + assert list(ext.value) == [ + x509.DNSName(u"example.com"), + x509.DNSName(u"*.example.com"), + x509.RegisteredID(x509.ObjectIdentifier("1.2.3.4.5.6.7")), + ] + + def test_subject_alt_name_unsupported_general_name(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.RFC822Name(u"test@example.com"), + ]), + critical=False, + ) + + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA256(), backend) + @pytest.mark.requires_backend_interface(interface=DSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) |