Skip to content

Commit 0eebb9d

Browse files
reaperhulkalex
andauthored
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 <[email protected]>
1 parent bedf6e1 commit 0eebb9d

File tree

5 files changed

+104
-13
lines changed

5 files changed

+104
-13
lines changed

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
Changelog
22
=========
33

4+
.. _v46-0-5:
5+
6+
46.0.5 - 2026-02-10
7+
~~~~~~~~~~~~~~~~~~~
8+
9+
* An attacker could create a malicious public key that reveals portions of your
10+
private key when using certain uncommon elliptic curves (binary curves).
11+
This version now includes additional security checks to prevent this attack.
12+
This issue only affects binary elliptic curves, which are rarely used in
13+
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab and
14+
Atuin Automated Vulnerability Discovery Engine** for reporting the issue.
15+
**CVE-2026-26007**
16+
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
17+
removed in the next release.
18+
419
.. v46-0-4:
520
621
46.0.4 - 2026-01-27

src/cryptography/hazmat/primitives/asymmetric/ec.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,26 @@ def get_curve_for_oid(oid: ObjectIdentifier) -> type[EllipticCurve]:
445445
"The provided object identifier has no matching elliptic "
446446
"curve class"
447447
)
448+
449+
450+
_SECT_CURVES: tuple[type[EllipticCurve], ...] = (
451+
SECT163K1,
452+
SECT163R2,
453+
SECT233K1,
454+
SECT233R1,
455+
SECT283K1,
456+
SECT283R1,
457+
SECT409K1,
458+
SECT409R1,
459+
SECT571K1,
460+
SECT571R1,
461+
)
462+
463+
for _curve_cls in _SECT_CURVES:
464+
utils.deprecated(
465+
_curve_cls,
466+
__name__,
467+
f"{_curve_cls.__name__} will be removed in the next release.",
468+
utils.DeprecatedIn46,
469+
name=_curve_cls.__name__,
470+
)

src/cryptography/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class CryptographyDeprecationWarning(UserWarning):
2626
DeprecatedIn41 = CryptographyDeprecationWarning
2727
DeprecatedIn42 = CryptographyDeprecationWarning
2828
DeprecatedIn43 = CryptographyDeprecationWarning
29+
DeprecatedIn46 = CryptographyDeprecationWarning
2930

3031

3132
# If you're wondering why we don't use `Buffer`, it's because `Buffer` would

src/rust/src/backend/ec.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,10 @@ pub(crate) fn public_key_from_pkey(
135135
) -> CryptographyResult<ECPublicKey> {
136136
let ec = pkey.ec_key()?;
137137
let curve = py_curve_from_curve(py, ec.group())?;
138-
check_key_infinity(&ec)?;
139-
Ok(ECPublicKey {
140-
pkey: pkey.to_owned(),
141-
curve: curve.into(),
142-
})
138+
139+
ECPublicKey::new(pkey.to_owned(), curve.into())
143140
}
141+
144142
#[pyo3::pyfunction]
145143
#[pyo3(signature = (curve, backend=None))]
146144
fn generate_private_key(
@@ -198,10 +196,7 @@ fn from_public_bytes(
198196
let ec = openssl::ec::EcKey::from_public_key(&curve, &point)?;
199197
let pkey = openssl::pkey::PKey::from_ec_key(ec)?;
200198

201-
Ok(ECPublicKey {
202-
pkey,
203-
curve: py_curve.into(),
204-
})
199+
ECPublicKey::new(pkey, py_curve.into())
205200
}
206201

207202
#[pyo3::pymethods]
@@ -367,6 +362,29 @@ impl ECPrivateKey {
367362
}
368363
}
369364

365+
impl ECPublicKey {
366+
fn new(
367+
pkey: openssl::pkey::PKey<openssl::pkey::Public>,
368+
curve: pyo3::Py<pyo3::PyAny>,
369+
) -> CryptographyResult<ECPublicKey> {
370+
let ec = pkey.ec_key()?;
371+
check_key_infinity(&ec)?;
372+
let mut bn_ctx = openssl::bn::BigNumContext::new()?;
373+
let mut cofactor = openssl::bn::BigNum::new()?;
374+
ec.group().cofactor(&mut cofactor, &mut bn_ctx)?;
375+
let one = openssl::bn::BigNum::from_u32(1)?;
376+
if cofactor != one {
377+
ec.check_key().map_err(|_| {
378+
pyo3::exceptions::PyValueError::new_err(
379+
"Invalid EC key (key out of range, infinity, etc.)",
380+
)
381+
})?;
382+
}
383+
384+
Ok(ECPublicKey { pkey, curve })
385+
}
386+
}
387+
370388
#[pyo3::pymethods]
371389
impl ECPublicKey {
372390
#[getter]
@@ -606,10 +624,7 @@ impl EllipticCurvePublicNumbers {
606624

607625
let pkey = openssl::pkey::PKey::from_ec_key(public_key)?;
608626

609-
Ok(ECPublicKey {
610-
pkey,
611-
curve: self.curve.clone_ref(py),
612-
})
627+
ECPublicKey::new(pkey, self.curve.clone_ref(py))
613628
}
614629

615630
fn __eq__(

tests/hazmat/primitives/test_ec.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,3 +1542,40 @@ def test_exchange_non_matching_curve(self, backend):
15421542

15431543
with pytest.raises(ValueError):
15441544
key.exchange(ec.ECDH(), public_key)
1545+
1546+
1547+
def test_invalid_sect_public_keys(backend):
1548+
_skip_curve_unsupported(backend, ec.SECT571K1())
1549+
public_numbers = ec.EllipticCurvePublicNumbers(1, 1, ec.SECT571K1())
1550+
with pytest.raises(ValueError):
1551+
public_numbers.public_key()
1552+
1553+
point = binascii.unhexlify(
1554+
b"0400000000000000000000000000000000000000000000000000000000000000000"
1555+
b"0000000000000000000000000000000000000000000000000000000000000000000"
1556+
b"0000000000010000000000000000000000000000000000000000000000000000000"
1557+
b"0000000000000000000000000000000000000000000000000000000000000000000"
1558+
b"0000000000000000000001"
1559+
)
1560+
with pytest.raises(ValueError):
1561+
ec.EllipticCurvePublicKey.from_encoded_point(ec.SECT571K1(), point)
1562+
1563+
der = binascii.unhexlify(
1564+
b"3081a7301006072a8648ce3d020106052b810400260381920004000000000000000"
1565+
b"0000000000000000000000000000000000000000000000000000000000000000000"
1566+
b"0000000000000000000000000000000000000000000000000000000000000100000"
1567+
b"0000000000000000000000000000000000000000000000000000000000000000000"
1568+
b"0000000000000000000000000000000000000000000000000000000000000000000"
1569+
b"00001"
1570+
)
1571+
with pytest.raises(ValueError):
1572+
serialization.load_der_public_key(der)
1573+
1574+
pem = textwrap.dedent("""-----BEGIN PUBLIC KEY-----
1575+
MIGnMBAGByqGSM49AgEGBSuBBAAmA4GSAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1576+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1577+
AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1578+
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=
1579+
-----END PUBLIC KEY-----""").encode()
1580+
with pytest.raises(ValueError):
1581+
serialization.load_pem_public_key(pem)

0 commit comments

Comments
 (0)