Repository: pyca/cryptography

Issue:
Breaking change in type of a private elliptic key's "curve" from instance-of-class to class
While trying to test cryptography 42 changes related to UTC times going from "naive" to "timezone aware" in aioquic, I encountered an unrelated failure from our tests.

The problem was that for a SECP256R1 private key, we had code testing `isinstance(key, ec.SECP256R1)` and this is `True` in 41.0.7 and `False` in the development mainline.  I built from 00a94b00 this morning for testing with python 3.12, and I built with "python -m build".  I then installed the wheel into my aioquic test environment.

I did not see this change listed in the list of breaking changes and figure it might have snuck in unintentionally.

Here is a comparison between 41.0.7 and main:

```
# cryptography 41.0.7

>>> from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa
>>> key = ec.generate_private_key(ec.SECP256R1)
>>> key.curve 
<cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at 0x102da2840>
>>> isinstance(key.curve, ec.SECP256R1)
True
>>> key.curve is ec.SECP256R1  
False


# cryptography main

>>> from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa
>>> key = ec.generate_private_key(ec.SECP256R1)
>>> key.curve
<class 'cryptography.hazmat.primitives.asymmetric.ec.SECP256R1'>
>>> isinstance(key.curve, ec.SECP256R1)
False
>>> key.curve is ec.SECP256R1
True



Discussion:
Looks like a bug to me. I wonder why our tests didn't catch this.
Ahhh, the real bug is that `ec.generate_private_key(ec.SECP256R1)` should never have been accepted in the first place. The correct way to write it is: `ec.generate_private_key(ec.SECP256R1())`
Hmm.  I will go hunting to see if we miscalled somewhere.
Ahhh, still so wrong :-)
Hmm, we seem to call the generate method with the class and not an instance in one place, but things work in 41 and not in 42 so I am still unsure either of why it ever worked or why it has stopped working :),
The issue here is that in < 42 we returned an instance of the curve for `key.curve` and in main right now we return the class. We aren't entirely clear on which we return in the docs, but we should keep returning an instance since that's what we always did.
Sorry, to be a bit more accurate here: in all releases right now if you pass a class or an instance, `key.curve` will return an instance. In `main` right now if you pass an instance you get the instance back for `key.curve` but if you pass a class you'll get the class.
Sorry it looks to me like if you pass an instance (as opposed to the class), to generate key it looks right to me in 42:

```
>>> from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa
>>> key = ec.generate_private_key(ec.SECP256R1())
>>> 
>>> key.curve
<cryptography.hazmat.primitives.asymmetric.ec.SECP256R1 object at 0x104505910>
>>> isinstance(key.curve, ec.SECP256R1)
True
```

So there's just a behavioral change in if you pass the class
OK, I found that we were passing the class in one place in our test suite.  If I change that to an instance, then it works on both.  I can accept this is "our bad", but it might be worth considering doing "what you always did" on the return value in case others are having similar confusion between instances and classes.

Thanks for the super fast response.

I'll leave it to you to decide if you want to change something or just close the ticket, and will fix our test to use the instance.
I think we should fix this Alex. Maybe with a deprecation warning to note this was always incorrect, maybe with a test that just silently accepts this forever ¯\_(ツ)_/¯ 
Yeah, seems good.

Resolving patch:
diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py
--- a/src/cryptography/hazmat/primitives/asymmetric/ec.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py
@@ -173,9 +173,6 @@ def from_encoded_point(
     ) -> EllipticCurvePublicKey:
         utils._check_bytes("data", data)
 
-        if not isinstance(curve, EllipticCurve):
-            raise TypeError("curve must be an EllipticCurve instance")
-
         if len(data) == 0:
             raise ValueError("data must not be an empty byte string")
 
@@ -346,9 +343,6 @@ def derive_private_key(
     if private_value <= 0:
         raise ValueError("private_value must be a positive integer.")
 
-    if not isinstance(curve, EllipticCurve):
-        raise TypeError("curve must provide the EllipticCurve interface.")
-
     return rust_openssl.ec.derive_private_key(private_value, curve)
 
 


Test patch:
diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py
--- a/tests/hazmat/primitives/test_ec.py
+++ b/tests/hazmat/primitives/test_ec.py
@@ -12,7 +12,7 @@
 
 import pytest
 
-from cryptography import exceptions, x509
+from cryptography import exceptions, utils, x509
 from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import ec
 from cryptography.hazmat.primitives.asymmetric.utils import (
@@ -210,6 +210,16 @@ def test_ec_key_key_size(backend):
     assert key.public_key().key_size == 256
 
 
+def test_deprecated_generate_private_key_with_curve_class(backend):
+    # This test verifies that if you pass a curve _class_ instead of instance,
+    # you get a warning and then `key.curve` is still an instance.
+    _skip_curve_unsupported(backend, ec.SECP256R1())
+
+    with pytest.warns(utils.DeprecatedIn42):
+        key = ec.generate_private_key(ec.SECP256R1)  # type: ignore[arg-type]
+    assert isinstance(key.curve, ec.SECP256R1)
+
+
 class TestECWithNumbers:
     def test_with_numbers(self, backend, subtests):
         vectors = itertools.product(
diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py
--- a/tests/hazmat/primitives/test_pkcs12.py
+++ b/tests/hazmat/primitives/test_pkcs12.py
@@ -308,7 +308,7 @@ class TestPKCS12Creation:
         ]
         + [
             pytest.param(
-                ec.generate_private_key, ec.EllipticCurvePrivateKey, [curve]
+                ec.generate_private_key, ec.EllipticCurvePrivateKey, [curve()]
             )
             for curve in ec._CURVE_TYPES.values()
         ],