Repository: pyca/cryptography

Issue:
x509.Certificate: equality should imply equality of hashes
According to the [Python data model](https://docs.python.org/2/reference/datamodel.html#object.__hash__), `objects which compare equal have the same hash value`.

```
(venv)jakub@iks:~/dev/letsencrypt/letsencrypt/acme/jose/testdata$ pip show cryptography

---
Name: cryptography
Version: 0.9.2
Location: /home/jakub/dev/letsencrypt/letsencrypt/venv/lib/python2.7/site-packages
Requires: idna, pyasn1, six, setuptools, enum34, ipaddress, cffi
(venv)jakub@iks:~/dev/letsencrypt/letsencrypt/acme/jose/testdata$ cat t1.py
from cryptography import x509
from cryptography.hazmat.backends import default_backend

with open("cert.der") as cert_file:
    data = cert_file.read()
    cert1 = x509.load_der_x509_certificate(data, default_backend())
    cert2 = x509.load_der_x509_certificate(data, default_backend())

print repr(cert1)
print cert1 == cert2, hash(cert1) == hash(cert2)
(venv)jakub@iks:~/dev/letsencrypt/letsencrypt/acme/jose/testdata$ python t1.py 
<cryptography.hazmat.backends.openssl.x509._Certificate object at 0x7f92c76bd550>
True False
(venv)jakub@iks:~/dev/letsencrypt/letsencrypt/acme/jose/testdata$ openssl x509 -inform DER -in cert.der
-----BEGIN CERTIFICATE-----
MIIBdTCCAR+gAwIBAgIJAK3Clm9L2zFcMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
BAMMC2V4YW1wbGUuY29tMB4XDTE1MDYyMjA0MDAzN1oXDTE1MDcyMjA0MDAzN1ow
FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEA
rHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJ
S86ObRz6lUTor4R0T+3C5QIDAQABo1AwTjAdBgNVHQ4EFgQUJs/I7UvXlLLkJQNY
JI8EwHTVl4owHwYDVR0jBBgwFoAUJs/I7UvXlLLkJQNYJI8EwHTVl4owDAYDVR0T
BAUwAwEB/zANBgkqhkiG9w0BAQsFAANBAAyL/xKAnkyQvCawliCrdgxkcdIVSKUz
9kfkA992Xg/N4Rte0U3CH424Yy/JfW5cO8vNo9DYJ3Rmo3YGpfuBOrY=
-----END CERTIFICATE-----
```



Discussion:
On python3 this fails with a `TypeError`.


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
@@ -226,15 +226,12 @@ def __eq__(self, other):
     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
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -1323,6 +1323,12 @@ def __ne__(self, other):
         Checks not equal.
         """
 
+    @abc.abstractmethod
+    def __hash__(self):
+        """
+        Computes a hash.
+        """
+
     @abc.abstractmethod
     def public_bytes(self, encoding):
         """


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -347,6 +347,29 @@ def test_ne(self, backend):
         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"),