Skip to content

Commit 5d695c9

Browse files
alexclaude
andauthored
Fix loading PKCS#3 DH parameters with privateValueLength (#15045)
A PKCS#3 "DH PARAMETERS" structure may carry an optional trailing INTEGER, privateValueLength. The loader parsed every DH parameters blob with the X9.42-shaped struct (p, g, q?), so it misread privateValueLength as the subprime q. Since #15016 added a check_key() validation, this now fails with "Invalid DH parameters". Route the PEM loader by tag: "DH PARAMETERS" (PKCS#3) ignores privateValueLength, while "X9.42 DH PARAMETERS" keeps q. The DER loader stays X9.42-permissive since DER carries no tag to disambiguate and the existing rfc5114 DER vectors require q to be parsed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 61b250a commit 5d695c9

4 files changed

Lines changed: 57 additions & 13 deletions

File tree

docs/development/test-vectors.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ Key exchange
353353
* ``vectors/cryptography_vectors/asymmetric/DH/dh_key_256.pem`` contains
354354
a PEM PKCS8 encoded DH key with a 256-bit key size.
355355

356+
* ``vectors/cryptography_vectors/asymmetric/DH/dhp_privatevaluelength.pem``
357+
contains PKCS#3 ``DH PARAMETERS`` that include the optional
358+
``privateValueLength`` field, which must not be confused with an X9.42
359+
subprime ``q``.
360+
356361
* ``vectors/cryptoraphy_vectors/asymmetric/ECDH/brainpool.txt`` contains
357362
Brainpool vectors from :rfc:`7027`.
358363

src/rust/src/backend/dh.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,42 @@ pub(crate) fn public_key_from_pkey(
7171
})
7272
}
7373

74+
// Build DHParameters from the DER encoding of a parameter structure. When
75+
// `x942` is true the optional trailing INTEGER is the X9.42 subprime `q`;
76+
// otherwise the structure is PKCS#3 and the optional trailing INTEGER is
77+
// `privateValueLength`, which we ignore.
78+
fn load_dh_parameters(data: &[u8], x942: bool) -> CryptographyResult<DHParameters> {
79+
let (p, q, g) = if x942 {
80+
let asn1_params = asn1::parse_single::<common::DHParams<'_>>(data)?;
81+
let p = openssl::bn::BigNum::from_slice(asn1_params.p.as_bytes())?;
82+
let q = asn1_params
83+
.q
84+
.map(|q| openssl::bn::BigNum::from_slice(q.as_bytes()))
85+
.transpose()?;
86+
let g = openssl::bn::BigNum::from_slice(asn1_params.g.as_bytes())?;
87+
(p, q, g)
88+
} else {
89+
let asn1_params = asn1::parse_single::<common::BasicDHParams<'_>>(data)?;
90+
let p = openssl::bn::BigNum::from_slice(asn1_params.p.as_bytes())?;
91+
let g = openssl::bn::BigNum::from_slice(asn1_params.g.as_bytes())?;
92+
(p, None, g)
93+
};
94+
95+
let dh = openssl::dh::Dh::from_pqg(p, q, g)?;
96+
check_dh_parameters(&dh)?;
97+
Ok(DHParameters { dh })
98+
}
99+
74100
#[pyo3::pyfunction]
75101
#[pyo3(signature = (data, backend=None))]
76102
fn from_der_parameters(
77103
data: &[u8],
78104
backend: Option<pyo3::Bound<'_, pyo3::PyAny>>,
79105
) -> CryptographyResult<DHParameters> {
80106
let _ = backend;
81-
let asn1_params = asn1::parse_single::<common::DHParams<'_>>(data)?;
82-
83-
let p = openssl::bn::BigNum::from_slice(asn1_params.p.as_bytes())?;
84-
let q = asn1_params
85-
.q
86-
.map(|q| openssl::bn::BigNum::from_slice(q.as_bytes()))
87-
.transpose()?;
88-
let g = openssl::bn::BigNum::from_slice(asn1_params.g.as_bytes())?;
89-
90-
let dh = openssl::dh::Dh::from_pqg(p, q, g)?;
91-
check_dh_parameters(&dh)?;
92-
Ok(DHParameters { dh })
107+
// DER carries no tag distinguishing PKCS#3 from X9.42, so we permissively
108+
// accept an optional trailing `q` for backwards compatibility.
109+
load_dh_parameters(data, true)
93110
}
94111

95112
#[pyo3::pyfunction]
@@ -105,7 +122,7 @@ fn from_pem_parameters(
105122
"Valid PEM but no BEGIN DH PARAMETERS/END DH PARAMETERS delimiters. Are you sure this is a DH parameters?",
106123
)?;
107124

108-
from_der_parameters(parsed.contents(), None)
125+
load_dh_parameters(parsed.contents(), parsed.tag() == "X9.42 DH PARAMETERS")
109126
}
110127

111128
fn dh_parameters_from_numbers(

tests/hazmat/primitives/test_dh.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,20 @@ def test_public_bytes_values(
979979
else:
980980
assert parameter_numbers.q is None
981981

982+
def test_load_pkcs3_with_private_value_length(self):
983+
# A PKCS#3 "DH PARAMETERS" structure may carry an optional trailing
984+
# INTEGER, privateValueLength. It must not be confused with the X9.42
985+
# subprime q (which only appears in "X9.42 DH PARAMETERS").
986+
param_bytes = load_vectors_from_file(
987+
os.path.join("asymmetric", "DH", "dhp_privatevaluelength.pem"),
988+
lambda pemfile: pemfile.read(),
989+
mode="rb",
990+
)
991+
parameters = serialization.load_pem_parameters(param_bytes)
992+
parameter_numbers = parameters.parameter_numbers()
993+
assert parameter_numbers.g == 2
994+
assert parameter_numbers.q is None
995+
982996
@pytest.mark.parametrize(
983997
("encoding", "fmt"),
984998
[
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN DH PARAMETERS-----
2+
MIIBDAKCAQEAlIp1fYr3ZNIqhxf5Ekoxi3eeGHtmXjuOXQ6F8cUjnqOCDeel6igI
3+
r00KTHnv3zTiRAdfK8+doLuBmUwHuE4ahtNi/FIbAbThaR6y2xYTGboTqLO8Jj6Z
4+
cnFyGRx4qMyhuYW98GDkbRt3MWDTCbKNtPT+W2UrVQhkDQpq+O5qZ5SOnxzlI9b6
5+
dyesAsWbeCV8aoMS9hxStBujSp1UD7Vbej1frZw1RwWuFY+6EsLXXeWFfZ4AaSJk
6+
h0TzTXeeUj5sl6xrctWK3noYypRzgidt2D3OxobO3Vh8PvbbXz5Qi/h8dqexZnRE
7+
Qf3k+DYfQsp5Mcx4ENuppHZoZXIh9+qZDwIBAgICAOE=
8+
-----END DH PARAMETERS-----

0 commit comments

Comments
 (0)