Files
node/test/parallel/test-crypto-pkcs12.js
T
Filip Skokan acbc3ff133 test,benchmark: drop OpenSSL 1.x-only coverage
Delete test-crypto-ecb.js, which can no longer run anywhere: Blowfish
is available only from OpenSSL's legacy provider and is absent from
BoringSSL.

The addon and cctest version guards become OPENSSL_IS_BORINGSSL checks.
Remove legacy digest-alias coverage and unreachable OpenSSL 1.x
branches. JavaScript test and benchmark gates, skip reasons, and TLS
error expectations that distinguished OpenSSL 1.x from provider-backed
OpenSSL now distinguish BoringSSL, including PKCS12 parsing, named DH
groups, private-key serialization, RSA-PSS parameters, consolidated
provider caches, FIPS availability, and empty KMAC output. Keep the CCM
alternate finalization error only for BoringSSL.

Provider-only test skips use version-neutral terminology or state the
actual minimum OpenSSL version.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/64777
Fixes: https://github.com/nodejs/node/issues/56733
Refs: https://github.com/nodejs/node/pull/64211
Refs: https://github.com/nodejs/node/pull/63966
Refs: https://github.com/nodejs/node/pull/53329
Refs: https://github.com/nodejs/node/pull/48950
Refs: https://github.com/nodejs/node/issues/45465
Refs: https://github.com/nodejs/node/issues/42827
Refs: https://github.com/nodejs/node/issues/47076
Refs: https://github.com/nodejs/node/pull/62862
Refs: https://github.com/nodejs/node/pull/38512
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Richard Lau <richard.lau@ibm.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-10-25 22:36:32 +01:00

227 lines
9.2 KiB
JavaScript

'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
const { hasFIPS, isBoringSSL } = require('../common/crypto');
const fips3 = hasFIPS(3);
if (fips3) {
// The long-standing fixtures protect their contents with PKCS12KDF and
// 3DES, neither of which the FIPS provider offers, so they cannot be opened
// at all. Assert the recognizable error rather than skipping: this is the
// same path the legacy.pfx case below exercises.
for (const name of ['rsa_cert.pfx', 'agent1.pfx']) {
assert.throws(
() => crypto.parsePKCS12(fixtures.readKey(name), { passphrase: 'sample' }),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
}
// Reading the PBMAC1-protected FIPS fixtures needs OpenSSL 3.5 or newer, so
// below that there is nothing further this test can check.
if (!hasFIPS(3, 5)) return;
}
// The FIPS fixtures use PBMAC1/PBKDF2 and AES-256 in place of PKCS12KDF and
// 3DES. Their passphrase is eight characters because the OpenSSL 4 FIPS
// provider rejects shorter ones outright, which would mask the errors the
// cases below are actually about.
const keyAndCert = fips3 ?
{ name: 'agent1-fips.pfx', passphrase: 'password', additional: 1 } :
{ name: 'rsa_cert.pfx', passphrase: 'sample', additional: 0 };
const withCaCert = fips3 ?
{ name: 'agent1-fips.pfx', passphrase: 'password' } :
{ name: 'agent1.pfx', passphrase: 'sample' };
const ecKeyAndCert = fips3 ?
{ name: 'ec-fips.pfx', passphrase: 'password' } :
{ name: 'ec.pfx', passphrase: '' };
const certWithoutKey = fips3 ?
{ name: 'cert-without-key-fips.pfx', passphrase: 'password' } :
{ name: 'cert-without-key.pfx', passphrase: 'test' };
{
// Round-trip: a bundle holding one key and its certificate.
const bundle = fixtures.readKey(keyAndCert.name);
const { privateKey, certificate, additionalCertificates } =
crypto.parsePKCS12(bundle, { passphrase: keyAndCert.passphrase });
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa');
assert.ok(certificate instanceof crypto.X509Certificate);
assert.ok(Array.isArray(additionalCertificates));
assert.strictEqual(additionalCertificates.length, keyAndCert.additional);
// The parsed key and certificate must actually correspond.
assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
// PEM export -- the primary reason callers want this API at all.
const keyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
assert.match(keyPem, /^-----BEGIN PRIVATE KEY-----/);
assert.match(certificate.toString(), /^-----BEGIN CERTIFICATE-----/);
// Round-trips back through createPrivateKey.
const reimported = crypto.createPrivateKey(keyPem);
assert.strictEqual(reimported.asymmetricKeyType, 'rsa');
// Passphrase accepted as a buffer as well as a string.
const { privateKey: key2 } = crypto.parsePKCS12(
bundle, { passphrase: Buffer.from(keyAndCert.passphrase) });
assert.strictEqual(key2.type, 'private');
}
{
// A bundle carrying a second certificate alongside the end-entity one.
const bundle = fixtures.readKey(withCaCert.name);
const { privateKey, certificate, additionalCertificates } =
crypto.parsePKCS12(bundle, { passphrase: withCaCert.passphrase });
assert.strictEqual(privateKey.type, 'private');
assert.ok(certificate instanceof crypto.X509Certificate);
assert.strictEqual(additionalCertificates.length, 1);
assert.ok(additionalCertificates[0] instanceof crypto.X509Certificate);
assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
}
{
// EC key.
const bundle = fixtures.readKey(ecKeyAndCert.name);
const { privateKey, certificate } =
crypto.parsePKCS12(bundle, { passphrase: ecKeyAndCert.passphrase });
assert.strictEqual(privateKey.asymmetricKeyType, 'ec');
assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
}
if (!fips3) {
// ec.pfx carries an empty passphrase, which the FIPS provider will not
// accept at all, so this case has no FIPS counterpart. Omitting the
// passphrase rather than passing it empty must behave identically: OpenSSL
// tries both PKCS#12 password encodings, so either variant opens the file.
const { privateKey } = crypto.parsePKCS12(fixtures.readKey('ec.pfx'));
assert.strictEqual(privateKey.asymmetricKeyType, 'ec');
}
{
// Wrong passphrase must be distinguishable from malformed input: OpenSSL
// reports it as a MAC verification failure of its own, and BoringSSL
// reports the same condition as an incorrect password. It is deliberately
// longer than the FIPS minimum password length so that this checks what it
// says it does under FIPS too.
const code = process.features.openssl_is_boringssl ?
'ERR_OSSL_INCORRECT_PASSWORD' : 'ERR_OSSL_PKCS12_MAC_VERIFY_FAILURE';
const bundle = fixtures.readKey(keyAndCert.name);
assert.throws(
() => crypto.parsePKCS12(bundle, { passphrase: 'wrongpassphrase' }),
{ code });
}
{
// Not a PKCS#12 structure at all. The error must be the same on either
// crypto library: OpenSSL rejects this in d2i_PKCS12_bio(), while
// BoringSSL's only copies the bytes and leaves every structural check to
// PKCS12_parse(), so the binding has to recognize both.
assert.throws(
() => crypto.parsePKCS12(Buffer.from('not a bundle'), { passphrase: 'x' }),
{ code: 'ERR_CRYPTO_OPERATION_FAILED', message: /not a valid PKCS#12/ });
}
{
// A bundle with no private key. PKCS12_parse() identifies the end-entity
// certificate by its association with the key, so with no key there is
// nothing to single out and everything arrives through
// `additionalCertificates` -- including the end-entity certificate itself.
const bundle = fixtures.readKey(certWithoutKey.name);
const { privateKey, certificate, additionalCertificates } =
crypto.parsePKCS12(bundle, { passphrase: certWithoutKey.passphrase });
assert.strictEqual(privateKey, null);
assert.strictEqual(certificate, null);
assert.strictEqual(additionalCertificates.length, 1);
assert.ok(additionalCertificates[0] instanceof crypto.X509Certificate);
}
if (!isBoringSSL) {
// Legacy algorithms (RC2-40-CBC) throw a recognizable, actionable error
// rather than a bare OpenSSL string. Mirrors the behavior added for the
// TLS path in https://github.com/nodejs/node/pull/54485.
const bundle = fixtures.readKey('legacy.pfx');
assert.throws(
() => crypto.parsePKCS12(bundle, { passphrase: 'legacy' }),
{ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' });
}
{
// ArrayBuffer is a documented input type for both the bundle and the
// passphrase, and must behave the same as a view over the same bytes.
const buf = fixtures.readKey(keyAndCert.name);
const bundleAb =
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const passBuf = Buffer.from(keyAndCert.passphrase);
const passAb =
passBuf.buffer.slice(passBuf.byteOffset,
passBuf.byteOffset + passBuf.byteLength);
const { privateKey, certificate } =
crypto.parsePKCS12(bundleAb, { passphrase: passAb });
assert.strictEqual(privateKey.type, 'private');
assert.ok(certificate instanceof crypto.X509Certificate);
assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
// DataView too.
const { privateKey: key2 } = crypto.parsePKCS12(
new DataView(bundleAb), { passphrase: keyAndCert.passphrase });
assert.strictEqual(key2.type, 'private');
}
{
// A passphrase reaches OpenSSL as a NUL-terminated C string, and
// PKCS12_parse() takes no length alongside it. Without a check, everything
// from an embedded NUL onwards is dropped and the bundle opens under the
// truncated prefix -- a passphrase of '<pass>\0junk' would unlock a bundle
// protected by '<pass>'. Reject the passphrase instead, whatever form it
// arrives in.
const bundle = fixtures.readKey(keyAndCert.name);
const withNul = `${keyAndCert.passphrase}\u0000junk`;
const withNulBuf = Buffer.from(withNul);
const withNulAb = withNulBuf.buffer.slice(
withNulBuf.byteOffset, withNulBuf.byteOffset + withNulBuf.byteLength);
const passphrases = [
withNul,
withNulBuf,
withNulAb,
new DataView(withNulAb),
new Uint8Array(withNulAb),
// A trailing NUL is no more representable than an interior one.
`${keyAndCert.passphrase}\u0000`,
// And neither is a passphrase that is nothing but a NUL, which must not
// be mistaken for the empty passphrase.
'\u0000',
Buffer.from([0]),
];
for (const passphrase of passphrases) {
assert.throws(
() => crypto.parsePKCS12(bundle, { passphrase }),
{
code: 'ERR_INVALID_ARG_VALUE',
message: /options\.passphrase.*must not contain null bytes/,
});
}
// The prefix on its own still opens the bundle, which is precisely why the
// truncation had to be rejected rather than tolerated.
assert.strictEqual(
crypto.parsePKCS12(bundle, { passphrase: keyAndCert.passphrase })
.privateKey.type,
'private');
}
{
// Argument validation.
assert.throws(() => crypto.parsePKCS12('a string'),
{ code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => crypto.parsePKCS12(Buffer.alloc(0), 'nope'),
{ code: 'ERR_INVALID_ARG_TYPE' });
}