Repository: pyca/cryptography

Issue:
_key_identifier_from_public_key is pretty slow
Sure, okay, it even has a comment warning about this: https://github.com/pyca/cryptography/blob/master/src/cryptography/x509/extensions.py#L35

I just ran into this in the context of doing some testing; the bulk of the runtime in the test is issuing certificates in my fake CA, as part of that I'm using `SubjectKeyIdentifier.from_public_key` and `AuthorityKeyIdentifier.from_issuer_public_key`; I can cache the latter, but not the former.



Discussion:
Can you construct a benchmark for this issue that we can work towards optimizing? (A simple script that creates a public key, and then calls `AuthorityKeyIdentifier.from_issuer_public_key` is probably sufficient.

I couldn't resist getting _slightly_ fancy, here's a self-calibrating benchmark::

``` python
from __future__ import print_function

import time

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa


def main():
    key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend())

    public_key = key.public_key()
    start = time.time()
    n = 0
    while time.time() < start + 0.1:
        x509.SubjectKeyIdentifier.from_public_key(public_key)
        n += 1
    n *= 50

    start = time.time()
    for _ in xrange(n):
        x509.SubjectKeyIdentifier.from_public_key(public_key)
    end = time.time()
    print((end - start) / n, n)


if __name__ == '__main__':
    main()
```

Results on my desktop machine: ~1.8 ms/call on CPython 2.7.11. (Output values are ms/call, and calls/sec)

FWIW, it's about 3x faster under PyPy. I'm starting to profile now.

On PyPy (with the lovely `vmprof` :-)), it's about 50% the stuff in `_key_identifier_from_public_key` and maybe 33% in the `pyasn1` decoding. Very little time in `public_bytes`.

The best solution might be to have a `public_bytes` that goes directly to SPKI as bits, that way we skip the decode phase, and get bytes directly out which can be fed to SHA1. Downside is that it involves exposing vaguely internal details in the public API. What do you think @reaperhulk ?

http://vmprof.com/#/68579e518cab75db05b7bbdce6fbbc40?id=1,2,0&view=flames is the `vmprof` for pypy btw.


Resolving patch:
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -17,6 +17,8 @@
 
 from cryptography import utils
 from cryptography.hazmat.primitives import constant_time, serialization
+from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
 from cryptography.x509.general_name import GeneralName, IPAddress, OtherName
 from cryptography.x509.name import Name
 from cryptography.x509.oid import (
@@ -32,23 +34,32 @@ class _SubjectPublicKeyInfo(univ.Sequence):
 
 
 def _key_identifier_from_public_key(public_key):
-    # This is a very slow way to do this.
-    serialized = public_key.public_bytes(
-        serialization.Encoding.DER,
-        serialization.PublicFormat.SubjectPublicKeyInfo
-    )
-    spki, remaining = decoder.decode(
-        serialized, asn1Spec=_SubjectPublicKeyInfo()
-    )
-    assert not remaining
-    # the univ.BitString object is a tuple of bits. We need bytes and
-    # pyasn1 really doesn't want to give them to us. To get it we'll
-    # build an integer and convert that to bytes.
-    bits = 0
-    for bit in spki.getComponentByName("subjectPublicKey"):
-        bits = bits << 1 | bit
-
-    data = utils.int_to_bytes(bits)
+    if isinstance(public_key, RSAPublicKey):
+        data = public_key.public_bytes(
+            serialization.Encoding.DER,
+            serialization.PublicFormat.PKCS1,
+        )
+    elif isinstance(public_key, EllipticCurvePublicKey):
+        data = public_key.public_numbers().encode_point()
+    else:
+        # This is a very slow way to do this.
+        serialized = public_key.public_bytes(
+            serialization.Encoding.DER,
+            serialization.PublicFormat.SubjectPublicKeyInfo
+        )
+        spki, remaining = decoder.decode(
+            serialized, asn1Spec=_SubjectPublicKeyInfo()
+        )
+        assert not remaining
+        # the univ.BitString object is a tuple of bits. We need bytes and
+        # pyasn1 really doesn't want to give them to us. To get it we'll
+        # build an integer and convert that to bytes.
+        bits = 0
+        for bit in spki.getComponentByName("subjectPublicKey"):
+            bits = bits << 1 | bit
+
+        data = utils.int_to_bytes(bits)
+
     return hashlib.sha1(data).digest()
 
 


Test patch: