From 0eebb9dbb6343d9bc1d91e5a2482ed4e054a6d8c Mon Sep 17 00:00:00 2001
From: Paul Kehrer <paul.l.kehrer@gmail.com>
Date: Tue, 10 Feb 2026 12:32:06 -0600
Subject: [PATCH] EC check key on cofactor > 1 (#14287)

* Refactor EC public key construction to share more code (#14285)

* Run an EC check key if cofactor > 1

This only applies to the binary curves (ed25519 is cofactor 8 and
ed448 is cofactor 4 but we use a different code path for eddsa)

* deprecate SECT curves

* add a test

* more tests

* code review

* simplify

* update with CVE and credit

---------

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
---
 CHANGELOG.rst                                 | 15 +++++++
 .../hazmat/primitives/asymmetric/ec.py        | 23 +++++++++++
 src/cryptography/utils.py                     |  1 +
 src/rust/src/backend/ec.rs                    | 41 +++++++++++++------
 tests/hazmat/primitives/test_ec.py            | 37 +++++++++++++++++
 5 files changed, 104 insertions(+), 13 deletions(-)

Index: cryptography-44.0.3/src/cryptography/hazmat/primitives/asymmetric/ec.py
===================================================================
--- cryptography-44.0.3.orig/src/cryptography/hazmat/primitives/asymmetric/ec.py
+++ cryptography-44.0.3/src/cryptography/hazmat/primitives/asymmetric/ec.py
@@ -401,3 +401,26 @@ def get_curve_for_oid(oid: ObjectIdentif
             "The provided object identifier has no matching elliptic "
             "curve class"
         )
+
+
+_SECT_CURVES: tuple[type[EllipticCurve], ...] = (
+    SECT163K1,
+    SECT163R2,
+    SECT233K1,
+    SECT233R1,
+    SECT283K1,
+    SECT283R1,
+    SECT409K1,
+    SECT409R1,
+    SECT571K1,
+    SECT571R1,
+)
+
+for _curve_cls in _SECT_CURVES:
+    utils.deprecated(
+        _curve_cls,
+        __name__,
+        f"{_curve_cls.__name__} will be removed in the next release.",
+        utils.DeprecatedIn46,
+        name=_curve_cls.__name__,
+    )
Index: cryptography-44.0.3/src/cryptography/utils.py
===================================================================
--- cryptography-44.0.3.orig/src/cryptography/utils.py
+++ cryptography-44.0.3/src/cryptography/utils.py
@@ -26,6 +26,7 @@ DeprecatedIn40 = CryptographyDeprecation
 DeprecatedIn41 = CryptographyDeprecationWarning
 DeprecatedIn42 = CryptographyDeprecationWarning
 DeprecatedIn43 = CryptographyDeprecationWarning
+DeprecatedIn46 = CryptographyDeprecationWarning
 
 
 def _check_bytes(name: str, value: bytes) -> None:
Index: cryptography-44.0.3/src/rust/src/backend/ec.rs
===================================================================
--- cryptography-44.0.3.orig/src/rust/src/backend/ec.rs
+++ cryptography-44.0.3/src/rust/src/backend/ec.rs
@@ -149,12 +149,10 @@ pub(crate) fn public_key_from_pkey(
 ) -> CryptographyResult<ECPublicKey> {
     let ec = pkey.ec_key()?;
     let curve = py_curve_from_curve(py, ec.group())?;
-    check_key_infinity(&ec)?;
-    Ok(ECPublicKey {
-        pkey: pkey.to_owned(),
-        curve: curve.into(),
-    })
+
+    ECPublicKey::new(pkey.to_owned(), curve.into())
 }
+
 #[pyo3::pyfunction]
 #[pyo3(signature = (curve, backend=None))]
 fn generate_private_key(
@@ -212,10 +210,7 @@ fn from_public_bytes(
     let ec = openssl::ec::EcKey::from_public_key(&curve, &point)?;
     let pkey = openssl::pkey::PKey::from_ec_key(ec)?;
 
-    Ok(ECPublicKey {
-        pkey,
-        curve: py_curve.into(),
-    })
+    ECPublicKey::new(pkey, py_curve.into())
 }
 
 #[pyo3::pymethods]
@@ -377,6 +372,29 @@ impl ECPrivateKey {
     }
 }
 
+impl ECPublicKey {
+    fn new(
+        pkey: openssl::pkey::PKey<openssl::pkey::Public>,
+        curve: pyo3::Py<pyo3::PyAny>,
+    ) -> CryptographyResult<ECPublicKey> {
+        let ec = pkey.ec_key()?;
+        check_key_infinity(&ec)?;
+        let mut bn_ctx = openssl::bn::BigNumContext::new()?;
+        let mut cofactor = openssl::bn::BigNum::new()?;
+        ec.group().cofactor(&mut cofactor, &mut bn_ctx)?;
+        let one = openssl::bn::BigNum::from_u32(1)?;
+        if cofactor != one {
+            ec.check_key().map_err(|_| {
+                pyo3::exceptions::PyValueError::new_err(
+                    "Invalid EC key (key out of range, infinity, etc.)",
+                )
+            })?;
+        }
+
+        Ok(ECPublicKey { pkey, curve })
+    }
+}
+
 #[pyo3::pymethods]
 impl ECPublicKey {
     #[getter]
@@ -617,10 +635,7 @@ impl EllipticCurvePublicNumbers {
 
         let pkey = openssl::pkey::PKey::from_ec_key(public_key)?;
 
-        Ok(ECPublicKey {
-            pkey,
-            curve: self.curve.clone_ref(py),
-        })
+        ECPublicKey::new(pkey, self.curve.clone_ref(py))
     }
 
     fn __eq__(
Index: cryptography-44.0.3/tests/hazmat/primitives/test_ec.py
===================================================================
--- cryptography-44.0.3.orig/tests/hazmat/primitives/test_ec.py
+++ cryptography-44.0.3/tests/hazmat/primitives/test_ec.py
@@ -1457,3 +1457,40 @@ class TestECDH:
 
         with pytest.raises(ValueError):
             key.exchange(ec.ECDH(), public_key)
+
+
+def test_invalid_sect_public_keys(backend):
+    _skip_curve_unsupported(backend, ec.SECT571K1())
+    public_numbers = ec.EllipticCurvePublicNumbers(1, 1, ec.SECT571K1())
+    with pytest.raises(ValueError):
+        public_numbers.public_key()
+
+    point = binascii.unhexlify(
+        b"0400000000000000000000000000000000000000000000000000000000000000000"
+        b"0000000000000000000000000000000000000000000000000000000000000000000"
+        b"0000000000010000000000000000000000000000000000000000000000000000000"
+        b"0000000000000000000000000000000000000000000000000000000000000000000"
+        b"0000000000000000000001"
+    )
+    with pytest.raises(ValueError):
+        ec.EllipticCurvePublicKey.from_encoded_point(ec.SECT571K1(), point)
+
+    der = binascii.unhexlify(
+        b"3081a7301006072a8648ce3d020106052b810400260381920004000000000000000"
+        b"0000000000000000000000000000000000000000000000000000000000000000000"
+        b"0000000000000000000000000000000000000000000000000000000000000100000"
+        b"0000000000000000000000000000000000000000000000000000000000000000000"
+        b"0000000000000000000000000000000000000000000000000000000000000000000"
+        b"00001"
+    )
+    with pytest.raises(ValueError):
+        serialization.load_der_public_key(der)
+
+    pem = textwrap.dedent("""-----BEGIN PUBLIC KEY-----
+    MIGnMBAGByqGSM49AgEGBSuBBAAmA4GSAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+    AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=
+    -----END PUBLIC KEY-----""").encode()
+    with pytest.raises(ValueError):
+        serialization.load_pem_public_key(pem)
