Skip to content

crypto: add crypto.parsePKCS12() - #65627

Open
bmuenzenmeyer wants to merge 6 commits into
nodejs:mainfrom
bmuenzenmeyer:pkcs12
Open

crypto: add crypto.parsePKCS12()#65627
bmuenzenmeyer wants to merge 6 commits into
nodejs:mainfrom
bmuenzenmeyer:pkcs12

Conversation

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor

Return the private key, end-entity certificate, and CA certificates from a PKCS#12 (.p12/.pfx) bundle as a KeyObject and X509Certificate instances.


Reading a .p12 / .pfx bundle from JavaScript today means shelling out to the openssl pkcs12 CLI or taking a userland dependency such as node-forge. In talking to a colleague about this unfortunate missing method in core, I (with Claude) noticed Node.js already parses this internally.

SecureContext::LoadPKCS12 has backed tls's pfx option for years, but its results are loaded straight into an SSL_CTX and never reach JavaScript. The capability is in the runtime already; it just isn't reachable. This exposes it.

const { key, cert, ca } = crypto.parsePKCS12(
  readFileSync('bundle.p12'),
  { passphrase: 'secret' },
);

Returns { key: KeyObject|null, cert: X509Certificate|null, ca: X509Certificate[] }.

This PR exposes those internals to end users. Our use case is loading identity files supplied by our environment, to be forwarded during MCP tool calls. This allows us to use real identity instead of service account.

Note

This is my first significant contribution to core that touches the internals. I am still getting my bearings with regard to the module mechanics, bindings, and c++. I'm committed to shaping this, but learning.


Return the private key, end-entity certificate, and CA certificates from
a PKCS#12 (.p12/.pfx) bundle as a KeyObject and X509Certificate
instances.

Node.js already parses PKCS#12 in SecureContext::LoadPKCS12, which backs
tls's `pfx` option, but the results are consumed directly into an
SSL_CTX and never reach JavaScript. Callers who need the key or the
certificates for anything other than an immediate TLS connection have to
shell out to `openssl pkcs12` or take a userland dependency.

The binding wraps d2i_PKCS12_bio() and PKCS12_parse() and follows their
semantics, matching the existing TLS path: the first private key is
returned, the end-entity certificate is the one associated with that
key, and any remaining certificates are returned through `ca`. A bundle
containing no private key reports `cert` as null and returns its
certificates through `ca`.

Absent and empty passphrases are kept distinct, since OpenSSL treats
them differently. Bundles that require OpenSSL's legacy provider throw
ERR_CRYPTO_UNSUPPORTED_OPERATION, reusing the error added for the TLS
path.

Signed-off-by: bmuenzenmeyer <brian.muenzenmeyer@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/gyp

@nodejs-github-bot nodejs-github-bot added lib / src 问题 and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 28, 2026
@panva
panva self-requested a review August 28, 2026 20:57

@panva panva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction looks alright. I noted a few issues to work through.

return {
key: keyHandle === null ? null : new PrivateKeyObject(keyHandle),
cert: certHandle === null ? null : new InternalX509Certificate(certHandle),
ca: ArrayPrototypeMap(caHandles, (h) => new InternalX509Certificate(h)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ca does not necessarily contain CA certificates or trust anchors; it contains every certificate other than the one matching the first private key. The certificate-only fixture even returns an end-entity certificate through ca. Could the result instead be { privateKey, certificate, additionalCertificates }?

if (len > 0) abv->CopyContents(pass_storage.data(), len);
// An explicitly-supplied empty passphrase is NOT the same as no
// passphrase; both reach OpenSSL, and they behave differently.
pass = pass_storage.c_str();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PKCS12_parse() has no password-length parameter, so c_str() silently truncates at an embedded NUL. I verified that both "sample\0junk" and Buffer.from("sample\0junk") open a PFX protected by "sample". Could we reject embedded NUL bytes and add regression tests?

Comment thread doc/api/crypto.md
Comment on lines +5351 to +5354
protecting the bundle. Omit for bundles with no passphrase. Omitting this
option is **not** equivalent to passing an empty string; the two are
handled differently, and a bundle created with one will not open with the
other.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the behavior exposed by PKCS12_parse(). When given either nullptr or "", OpenSSL tries both PKCS#12 password encodings and uses whichever verifies the MAC. The tests already open the same ec.pfx once with "" and once with omission.

{
// Round-trip: a bundle containing one key and one leaf cert, no CA certs.
const bundle = fixtures.readKey('rsa_cert.pfx');
const { key, cert, ca } = crypto.parsePKCS12(bundle, { passphrase: 'sample' });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a fixture that passes with FIPS enabled.

Comment thread lib/internal/crypto/keys.js
// OpenSSL treats NULL and "" differently for both MAC verification and bag
// decryption, so callers must not collapse them. PKCS12_parse() verifies the
// MAC itself when one is present, and tolerates the NULL / "" ambiguity.
PKCS12ParseResult ParseBundle(const BIOPointer& bio, const char* pass) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this direct parser be shared with SecureContext::LoadPKCS12() instead of maintaining two implementations of the same decoding? TLS can consume the shared parse result and then apply its stricter requirement that both a key and certificate are present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you be open to a follow up PR to do this? I'd considered the same pathway you suggest here for DRYness sake, but opted to keep the scope smaller and reviewable especially as I am learning the codebase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, is it a big shift?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you are open to it, i'll look :) just trying to guard everyone's time

Comment thread doc/api/crypto.md
Comment thread doc/api/crypto.md Outdated
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.33333% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.04%. Comparing base (452c8c5) to head (2d6065c).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/crypto/crypto_pkcs12.cc 79.78% 6 Missing and 13 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65627      +/-   ##
==========================================
- Coverage   90.05%   90.04%   -0.01%     
==========================================
  Files         754      755       +1     
  Lines      255727   255877     +150     
  分支    48312    48348      +36     
==========================================
+ Hits       230286   230408     +122     
- Misses      16564    16569       +5     
- Partials     8877     8900      +23     
Files with missing lines Coverage Δ
lib/crypto.js 92.96% <100.00%> (+0.03%) ⬆️
lib/internal/crypto/keys.js 98.13% <100.00%> (+0.07%) ⬆️
src/node_crypto.cc 81.81% <ø> (ø)
src/crypto/crypto_pkcs12.cc 79.78% <79.78%> (ø)

... and 31 files with indirect coverage changes

🚀 新建 features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@panva panva added the commit-queue-squash PRs the Commit Queue should land as one squashed commit. label Aug 29, 2026
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

commit-queue-squash PRs the Commit Queue should land as one squashed commit. lib / src 问题 and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

项目

None yet

Development

Successfully merging this pull request may close these issues.

3 participants