Skip to content

Commit c2c9105

Browse files
divybotlittledivy
andauthored
fix(ext/node): support PKCS#12 MACs other than SHA-1 (#34342)
## Summary `tls.createSecureContext({ pfx })` rejected PFX files using a SHA-2-family MAC — including the default produced by OpenSSL 3 (`openssl pkcs12 -export ...` without `-legacy`/`-macalg sha1`) — with `Error: mac verify failure`, even when the passphrase was correct. Debug builds additionally hit a `debug_assert_eq!` panic in the `p12` crate. The underlying call was `p12::Pkcs12::verify_mac`, which hardcodes SHA-1 for both the PBKDF and the HMAC. This PR replaces that call with an in-Deno verifier that: - Implements the PKCS#12 PBKDF (RFC 7292 Appendix B.2) generically over the `Digest` trait. - Dispatches on the digest algorithm OID stored in `MacData`, supporting SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, and SHA-512/256. Both the wrong-result-on-release and the panic-on-debug-build symptoms are resolved by the same change. Closes denoland/orchid#217 Fixes #34336. ## Test plan - [x] `localhost_{sha1,sha256,sha384,sha512}.pfx` fixtures added under `tests/testdata/tls/`, generated from the existing `localhost` certificate/key with `openssl pkcs12 -export -macalg <alg> -passout pass:secret`. README updated with the regen command. - [x] New `tls_test.ts` cases iterate over the four MAC algorithms and assert `tls.createSecureContext({ pfx, passphrase: "secret" })` succeeds. - [x] Additional case asserts a wrong passphrase still rejects with `mac verify failure`. - [x] Verified manually against a debug build with the reproducer from the issue: SHA-256 PFX is accepted, no panic. --------- Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 1ecd4c5 commit c2c9105

10 files changed

Lines changed: 383 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ ed25519-dalek = "2.1.1"
397397
ed448-goldilocks = "0.14.0-pre.10"
398398
elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem", "jwk"] }
399399
hkdf = "0.12.3"
400+
hmac = "0.12.1"
400401
k256 = { version = "0.13.1", features = ["ecdh", "jwk", "pem"] }
401402
md-5 = "0.10.5"
402403
md4 = "0.10.2"

ext/node_crypto/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ed448-goldilocks = { workspace = true }
3737
elliptic-curve.workspace = true
3838
faster-hex.workspace = true
3939
hkdf.workspace = true
40+
hmac.workspace = true
4041
k256.workspace = true
4142
md-5 = { workspace = true, features = ["oid"] }
4243
md4.workspace = true

ext/node_crypto/keys.rs

Lines changed: 336 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ use deno_core::convert::Uint8Array;
1111
use deno_core::op2;
1212
use deno_core::unsync::spawn_blocking;
1313
use deno_error::JsErrorBox;
14+
use digest::Digest;
15+
use digest::FixedOutputReset;
1416
use ed25519_dalek::pkcs8::BitStringRef;
1517
use elliptic_curve::JwkEcKey;
18+
use hmac::Hmac;
19+
use hmac::Mac;
1620
use num_bigint::BigInt;
1721
use num_traits::FromPrimitive as _;
22+
use p12::AlgorithmIdentifier as Pkcs12AlgorithmIdentifier;
1823
use p12::PFX as Pkcs12;
1924
use pkcs8::DecodePrivateKey as _;
2025
use pkcs8::Document;
@@ -4160,6 +4165,16 @@ pub enum PfxValidationError {
41604165
MacVerifyFailure,
41614166
}
41624167

4168+
// Cap on the iteration count for the PKCS#12 MAC PBKDF, since an attacker
4169+
// who controls the PFX bytes also controls this field (`mac_data.iterations`
4170+
// is a u32, up to ~4 billion, which would tie up CPU for tens of seconds).
4171+
// Matches the limit Mozilla NSS uses for the same KDF; well above any
4172+
// realistic legitimate value — OpenSSL defaults to 2048 on creation, and
4173+
// hardened producers rarely go beyond ~100k. OpenSSL itself doesn't cap
4174+
// here, but Node trusts the caller's PFX; in Deno the PFX often comes
4175+
// from untrusted input (e.g. server config), so capping is defensive.
4176+
const PFX_MAC_ITERATIONS_CAP: u64 = 600_000;
4177+
41634178
#[op2]
41644179
pub fn op_node_validate_pfx(
41654180
#[buffer] pfx: &[u8],
@@ -4168,12 +4183,249 @@ pub fn op_node_validate_pfx(
41684183
let parsed =
41694184
Pkcs12::parse(pfx).map_err(|_| PfxValidationError::NotEnoughData)?;
41704185
let password = passphrase.as_deref().unwrap_or("");
4171-
if !parsed.verify_mac(password) {
4186+
let bmp_password = bmp_string(password);
4187+
// If no MAC is present, the file is considered valid.
4188+
let Some(mac_data) = &parsed.mac_data else {
4189+
return Ok(());
4190+
};
4191+
let iterations = u64::from(mac_data.iterations);
4192+
if iterations > PFX_MAC_ITERATIONS_CAP {
4193+
return Err(PfxValidationError::MacVerifyFailure);
4194+
}
4195+
let data = parsed
4196+
.auth_safe
4197+
.data(&bmp_password)
4198+
.ok_or(PfxValidationError::MacVerifyFailure)?;
4199+
let ok = verify_pkcs12_mac(
4200+
&mac_data.mac.digest_algorithm,
4201+
&mac_data.mac.digest,
4202+
&mac_data.salt,
4203+
iterations,
4204+
&data,
4205+
&bmp_password,
4206+
)
4207+
.ok_or(PfxValidationError::MacVerifyFailure)?;
4208+
if !ok {
41724209
return Err(PfxValidationError::MacVerifyFailure);
41734210
}
41744211
Ok(())
41754212
}
41764213

4214+
// Convert a UTF-8 password to the PKCS#12 BMPString form (RFC 7292
4215+
// section B.1): each character is encoded as UTF-16BE, followed by a
4216+
// U+0000 terminator.
4217+
fn bmp_string(s: &str) -> Vec<u8> {
4218+
let utf16: Vec<u16> = s.encode_utf16().collect();
4219+
let mut bytes = Vec::with_capacity(utf16.len() * 2 + 2);
4220+
for c in utf16 {
4221+
bytes.extend_from_slice(&c.to_be_bytes());
4222+
}
4223+
bytes.extend_from_slice(&[0x00, 0x00]);
4224+
bytes
4225+
}
4226+
4227+
// PKCS#12 password-based key derivation, generic over a Digest. Implements
4228+
// the algorithm from RFC 7292 Appendix B.2:
4229+
// https://www.rfc-editor.org/rfc/rfc7292#appendix-B.2
4230+
//
4231+
// `v` is the hash function's block size in bytes (64 for SHA-1, SHA-224,
4232+
// SHA-256; 128 for SHA-384, SHA-512, SHA-512/224, SHA-512/256). `id` is
4233+
// the diversifier (1 = encryption key, 2 = IV, 3 = MAC key). `size` is
4234+
// the number of output bytes desired.
4235+
fn pkcs12_pbkdf<D: Digest + FixedOutputReset>(
4236+
pass: &[u8],
4237+
salt: &[u8],
4238+
iterations: u64,
4239+
id: u8,
4240+
size: usize,
4241+
v: usize,
4242+
) -> Vec<u8> {
4243+
// u = hash output size in bytes.
4244+
let u = <D as Digest>::output_size();
4245+
// Step 1: D = id repeated v bytes.
4246+
let d = vec![id; v];
4247+
// Steps 2 & 3: S and P are the salt / password padded to a multiple of
4248+
// v bytes by cyclic repetition. Empty inputs contribute an empty string.
4249+
let s: Vec<u8> = if salt.is_empty() {
4250+
Vec::new()
4251+
} else {
4252+
salt
4253+
.iter()
4254+
.cycle()
4255+
.take(v * salt.len().div_ceil(v))
4256+
.copied()
4257+
.collect()
4258+
};
4259+
let p: Vec<u8> = if pass.is_empty() {
4260+
Vec::new()
4261+
} else {
4262+
pass
4263+
.iter()
4264+
.cycle()
4265+
.take(v * pass.len().div_ceil(v))
4266+
.copied()
4267+
.collect()
4268+
};
4269+
// Step 4: I = S || P.
4270+
let mut i: Vec<u8> = Vec::with_capacity(s.len() + p.len());
4271+
i.extend_from_slice(&s);
4272+
i.extend_from_slice(&p);
4273+
// Step 5: c = ceil(size / u).
4274+
let c = size.div_ceil(u);
4275+
let mut out: Vec<u8> = Vec::with_capacity(c * u);
4276+
let mut hasher = D::new();
4277+
for _ in 0..c {
4278+
// Step 6a: Ai = H^iterations(D || I).
4279+
Digest::update(&mut hasher, &d);
4280+
Digest::update(&mut hasher, &i);
4281+
let mut ai = hasher.finalize_reset().to_vec();
4282+
for _ in 1..iterations {
4283+
Digest::update(&mut hasher, &ai);
4284+
ai = hasher.finalize_reset().to_vec();
4285+
}
4286+
// Step 7 (partial): A = A || Ai.
4287+
out.extend_from_slice(&ai);
4288+
if i.is_empty() {
4289+
continue;
4290+
}
4291+
// Step 6b: B = Ai repeated cyclically to v bytes.
4292+
let b: Vec<u8> = ai.iter().cycle().take(v).copied().collect();
4293+
// Step 6c: treating I as v-byte blocks, set each block to
4294+
// (block + B + 1) mod 2^(8v). Big-endian add with carry.
4295+
for chunk in i.chunks_mut(v) {
4296+
let mut carry: u16 = 1;
4297+
for j in (0..v).rev() {
4298+
let sum = chunk[j] as u16 + b[j] as u16 + carry;
4299+
chunk[j] = (sum & 0xff) as u8;
4300+
carry = sum >> 8;
4301+
}
4302+
}
4303+
}
4304+
// Step 8: take the first `size` bytes of A.
4305+
out.truncate(size);
4306+
out
4307+
}
4308+
4309+
fn pkcs12_hmac<D>(key: &[u8], data: &[u8]) -> Vec<u8>
4310+
where
4311+
D: Digest
4312+
+ digest::core_api::CoreProxy
4313+
+ FixedOutputReset
4314+
+ digest::core_api::BlockSizeUser,
4315+
<D as digest::core_api::CoreProxy>::Core: digest::core_api::BufferKindUser<
4316+
BufferKind = digest::block_buffer::Eager,
4317+
> + digest::core_api::FixedOutputCore
4318+
+ digest::HashMarker
4319+
+ Default
4320+
+ Clone,
4321+
<<D as digest::core_api::CoreProxy>::Core as digest::core_api::BlockSizeUser>::BlockSize: digest::typenum::IsLess<digest::typenum::U256>,
4322+
digest::typenum::Le<<<D as digest::core_api::CoreProxy>::Core as digest::core_api::BlockSizeUser>::BlockSize, digest::typenum::U256>: digest::typenum::NonZero,
4323+
{
4324+
let mut mac = <Hmac<D> as Mac>::new_from_slice(key).unwrap();
4325+
Mac::update(&mut mac, data);
4326+
mac.finalize().into_bytes().to_vec()
4327+
}
4328+
4329+
// OID identifiers in components.
4330+
const OID_SHA1: &[u64] = &[1, 3, 14, 3, 2, 26];
4331+
const OID_SHA224: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 4];
4332+
const OID_SHA256: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 1];
4333+
const OID_SHA384: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 2];
4334+
const OID_SHA512: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 3];
4335+
const OID_SHA512_224: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 5];
4336+
const OID_SHA512_256: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 6];
4337+
4338+
enum Pkcs12MacAlgorithm {
4339+
Sha1,
4340+
Sha224,
4341+
Sha256,
4342+
Sha384,
4343+
Sha512,
4344+
Sha512_224,
4345+
Sha512_256,
4346+
}
4347+
4348+
fn pkcs12_mac_algorithm(
4349+
algorithm: &Pkcs12AlgorithmIdentifier,
4350+
) -> Option<Pkcs12MacAlgorithm> {
4351+
let oid = match algorithm {
4352+
Pkcs12AlgorithmIdentifier::Sha1 => return Some(Pkcs12MacAlgorithm::Sha1),
4353+
Pkcs12AlgorithmIdentifier::OtherAlg(other) => {
4354+
other.algorithm_type.components().as_slice()
4355+
}
4356+
_ => return None,
4357+
};
4358+
if oid == OID_SHA1 {
4359+
Some(Pkcs12MacAlgorithm::Sha1)
4360+
} else if oid == OID_SHA224 {
4361+
Some(Pkcs12MacAlgorithm::Sha224)
4362+
} else if oid == OID_SHA256 {
4363+
Some(Pkcs12MacAlgorithm::Sha256)
4364+
} else if oid == OID_SHA384 {
4365+
Some(Pkcs12MacAlgorithm::Sha384)
4366+
} else if oid == OID_SHA512 {
4367+
Some(Pkcs12MacAlgorithm::Sha512)
4368+
} else if oid == OID_SHA512_224 {
4369+
Some(Pkcs12MacAlgorithm::Sha512_224)
4370+
} else if oid == OID_SHA512_256 {
4371+
Some(Pkcs12MacAlgorithm::Sha512_256)
4372+
} else {
4373+
None
4374+
}
4375+
}
4376+
4377+
fn verify_pkcs12_mac(
4378+
algorithm: &Pkcs12AlgorithmIdentifier,
4379+
expected: &[u8],
4380+
salt: &[u8],
4381+
iterations: u64,
4382+
data: &[u8],
4383+
password: &[u8],
4384+
) -> Option<bool> {
4385+
let mac_alg = pkcs12_mac_algorithm(algorithm)?;
4386+
let computed = match mac_alg {
4387+
Pkcs12MacAlgorithm::Sha1 => {
4388+
let key =
4389+
pkcs12_pbkdf::<sha1::Sha1>(password, salt, iterations, 3, 20, 64);
4390+
pkcs12_hmac::<sha1::Sha1>(&key, data)
4391+
}
4392+
Pkcs12MacAlgorithm::Sha224 => {
4393+
let key =
4394+
pkcs12_pbkdf::<sha2::Sha224>(password, salt, iterations, 3, 28, 64);
4395+
pkcs12_hmac::<sha2::Sha224>(&key, data)
4396+
}
4397+
Pkcs12MacAlgorithm::Sha256 => {
4398+
let key =
4399+
pkcs12_pbkdf::<sha2::Sha256>(password, salt, iterations, 3, 32, 64);
4400+
pkcs12_hmac::<sha2::Sha256>(&key, data)
4401+
}
4402+
Pkcs12MacAlgorithm::Sha384 => {
4403+
let key =
4404+
pkcs12_pbkdf::<sha2::Sha384>(password, salt, iterations, 3, 48, 128);
4405+
pkcs12_hmac::<sha2::Sha384>(&key, data)
4406+
}
4407+
Pkcs12MacAlgorithm::Sha512 => {
4408+
let key =
4409+
pkcs12_pbkdf::<sha2::Sha512>(password, salt, iterations, 3, 64, 128);
4410+
pkcs12_hmac::<sha2::Sha512>(&key, data)
4411+
}
4412+
Pkcs12MacAlgorithm::Sha512_224 => {
4413+
let key = pkcs12_pbkdf::<sha2::Sha512_224>(
4414+
password, salt, iterations, 3, 28, 128,
4415+
);
4416+
pkcs12_hmac::<sha2::Sha512_224>(&key, data)
4417+
}
4418+
Pkcs12MacAlgorithm::Sha512_256 => {
4419+
let key = pkcs12_pbkdf::<sha2::Sha512_256>(
4420+
password, salt, iterations, 3, 32, 128,
4421+
);
4422+
pkcs12_hmac::<sha2::Sha512_256>(&key, data)
4423+
}
4424+
};
4425+
use subtle::ConstantTimeEq;
4426+
Some(bool::from(computed.ct_eq(expected)))
4427+
}
4428+
41774429
#[derive(Debug, thiserror::Error, deno_error::JsError)]
41784430
pub enum CrlValidationError {
41794431
#[class(generic)]
@@ -4199,3 +4451,86 @@ pub fn op_node_validate_crl(
41994451
}
42004452
Ok(())
42014453
}
4454+
4455+
#[cfg(test)]
4456+
mod tests {
4457+
use super::*;
4458+
4459+
// Cross-check our generic implementation of the PKCS#12 PBKDF
4460+
// (RFC 7292 Appendix B.2) against test vectors computed with an
4461+
// independent reference implementation.
4462+
4463+
// SHA-1 vector lifted from the `p12` crate's own test suite, which
4464+
// matches output produced by Bouncy Castle.
4465+
#[test]
4466+
fn pkcs12_pbkdf_sha1() {
4467+
let pass = bmp_string("");
4468+
assert_eq!(pass, vec![0, 0]);
4469+
let salt: [u8; 8] = [0x9a, 0xf4, 0x70, 0x29, 0x58, 0xa8, 0xe9, 0x5c];
4470+
let got = pkcs12_pbkdf::<sha1::Sha1>(&pass, &salt, 2048, 1, 24, 64);
4471+
let expected: [u8; 24] = [
4472+
0xc2, 0x29, 0x4a, 0xa6, 0xd0, 0x29, 0x30, 0xeb, 0x5c, 0xe9, 0xc3, 0x29,
4473+
0xec, 0xcb, 0x9a, 0xee, 0x1c, 0xb1, 0x36, 0xba, 0xea, 0x74, 0x65, 0x57,
4474+
];
4475+
assert_eq!(got, expected);
4476+
}
4477+
4478+
// SHA-256 / SHA-384 / SHA-512 vectors were generated by porting the
4479+
// RFC 7292 B.2 pseudocode to Python and feeding the same inputs.
4480+
#[test]
4481+
fn pkcs12_pbkdf_sha256() {
4482+
let pass = bmp_string("secret");
4483+
let salt: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
4484+
let got = pkcs12_pbkdf::<sha2::Sha256>(&pass, &salt, 1000, 3, 32, 64);
4485+
let expected: [u8; 32] = [
4486+
0x7f, 0xe1, 0x91, 0x75, 0x7d, 0xca, 0xf1, 0xed, 0x6a, 0x29, 0x77, 0xb9,
4487+
0xb9, 0x15, 0x3f, 0x60, 0x82, 0xaf, 0x0b, 0xda, 0xfd, 0x09, 0x35, 0x2d,
4488+
0xcd, 0xaa, 0x96, 0x7f, 0x57, 0x17, 0x82, 0xb0,
4489+
];
4490+
assert_eq!(got, expected);
4491+
}
4492+
4493+
#[test]
4494+
fn pkcs12_pbkdf_sha384() {
4495+
let pass = bmp_string("secret");
4496+
let salt: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
4497+
let got = pkcs12_pbkdf::<sha2::Sha384>(&pass, &salt, 1000, 3, 48, 128);
4498+
let expected: [u8; 48] = [
4499+
0x6a, 0x59, 0x71, 0x72, 0x05, 0x22, 0x76, 0x31, 0x21, 0xf4, 0x9a, 0x1d,
4500+
0x5c, 0x04, 0x10, 0xa1, 0xdb, 0x42, 0x0b, 0xe4, 0x96, 0x6d, 0xc5, 0x2f,
4501+
0x51, 0x91, 0x9d, 0x91, 0x15, 0x2d, 0x60, 0x2d, 0x31, 0x1c, 0x4c, 0xb0,
4502+
0x8d, 0x99, 0x83, 0xad, 0xaf, 0x68, 0xff, 0x5d, 0xdd, 0x69, 0x0b, 0x87,
4503+
];
4504+
assert_eq!(got, expected);
4505+
}
4506+
4507+
#[test]
4508+
fn pkcs12_pbkdf_sha512() {
4509+
let pass = bmp_string("secret");
4510+
let salt: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
4511+
let got = pkcs12_pbkdf::<sha2::Sha512>(&pass, &salt, 1000, 3, 64, 128);
4512+
let expected: [u8; 64] = [
4513+
0x3e, 0x5c, 0x5d, 0xb5, 0xf5, 0xe3, 0xd0, 0xc2, 0x23, 0x2e, 0xbf, 0xbb,
4514+
0x86, 0x08, 0x23, 0x7a, 0x2b, 0x0a, 0xf6, 0x00, 0x30, 0xe6, 0xa0, 0x08,
4515+
0x2a, 0xbe, 0x7c, 0x19, 0x3f, 0x52, 0x5e, 0x98, 0x97, 0x6f, 0xb2, 0xbb,
4516+
0x1c, 0x88, 0xc3, 0xc6, 0xf8, 0xb5, 0x64, 0x91, 0x74, 0x3f, 0xc5, 0x04,
4517+
0xfb, 0x3d, 0xd9, 0x10, 0xf4, 0xf9, 0x5e, 0xf6, 0x99, 0xc2, 0x48, 0x15,
4518+
0xf1, 0x3e, 0xc1, 0x74,
4519+
];
4520+
assert_eq!(got, expected);
4521+
}
4522+
4523+
#[test]
4524+
fn bmp_string_basic() {
4525+
assert_eq!(bmp_string(""), vec![0x00, 0x00]);
4526+
// "Beavis" — every code unit becomes two big-endian bytes, then a
4527+
// U+0000 terminator.
4528+
assert_eq!(
4529+
bmp_string("Beavis"),
4530+
vec![
4531+
0x00, 0x42, 0x00, 0x65, 0x00, 0x61, 0x00, 0x76, 0x00, 0x69, 0x00, 0x73,
4532+
0x00, 0x00,
4533+
],
4534+
);
4535+
}
4536+
}

0 commit comments

Comments
 (0)