Skip to content

Commit ddbf974

Browse files
bmuenzenmeyerpanva
authored andcommitted
crypto: add crypto.parsePKCS12()
Return the private key, end-entity certificate, and any other non-matching 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. SecureContext::LoadPKCS12 now uses this same parsing logic. 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 `additionalCertificates`. A bundle containing no private key reports `certificate` as null and returns its certificates through `additionalCertificates`. 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> Co-authored-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65627 Backport-PR-URL: #66233 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent e7ed436 commit ddbf974

13 files changed

Lines changed: 643 additions & 59 deletions

File tree

‎doc/api/crypto.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5450,6 +5450,39 @@ const derivedKey = hkdfSync('sha512', 'key', 'salt', 'info', 64);
54505450
console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653'
54515451
```
54525452

5453+
### `crypto.parsePKCS12(bundle[, options])`
5454+
5455+
<!-- YAML
5456+
added: REPLACEME
5457+
-->
5458+
5459+
* `bundle` {ArrayBuffer|Buffer|TypedArray|DataView} A DER-encoded PKCS#12
5460+
(`.p12` or `.pfx`) bundle.
5461+
* `options` {Object}
5462+
* `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} The passphrase
5463+
protecting the bundle. Omitting this option is equivalent to passing `''`.
5464+
* Returns: {Object}
5465+
* `privateKey` {KeyObject|null} The first private key in the bundle, or
5466+
`null` if none is present.
5467+
* `certificate` {X509Certificate|null} The certificate matching `privateKey`,
5468+
or `null` if no matching certificate is present.
5469+
* `additionalCertificates` {X509Certificate\[]} All other certificates in
5470+
the bundle. If there is no private key, this contains all certificates.
5471+
May be empty.
5472+
5473+
Parses a PKCS#12 bundle, commonly stored with a `.p12` or `.pfx` extension,
5474+
and returns its private key and certificates.
5475+
5476+
```mjs
5477+
import { parsePKCS12 } from 'node:crypto';
5478+
import { readFileSync } from 'node:fs';
5479+
5480+
const { privateKey, certificate, additionalCertificates } = parsePKCS12(
5481+
readFileSync('bundle.p12'),
5482+
{ passphrase: 'secret' },
5483+
);
5484+
```
5485+
54535486
### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`
54545487

54555488
<!-- YAML

‎lib/crypto.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const {
8484
createSecretKey,
8585
createPublicKey,
8686
createPrivateKey,
87+
parsePKCS12,
8788
KeyObject,
8889
} = require('internal/crypto/keys');
8990
const {
@@ -216,6 +217,7 @@ module.exports = {
216217
getMacs,
217218
hkdf,
218219
hkdfSync,
220+
parsePKCS12,
219221
pbkdf2,
220222
pbkdf2Sync,
221223
generateKeyPair,

‎lib/internal/crypto/keys.js‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeMap,
45
ArrayPrototypeSlice,
56
ObjectDefineProperties,
67
ObjectPrototypeHasOwnProperty,
78
ObjectSetPrototypeOf,
89
StringPrototypeIncludes,
910
StringPrototypeStartsWith,
1011
SymbolToStringTag,
12+
TypedArrayPrototypeIncludes,
1113
Uint8Array,
1214
} = primordials;
1315

@@ -36,6 +38,7 @@ const {
3638
kKeyEncodingPKCS8,
3739
kKeyEncodingSPKI,
3840
kKeyEncodingSEC1,
41+
parsePKCS12: _parsePKCS12,
3942
} = internalBinding('crypto');
4043

4144
const {
@@ -64,6 +67,7 @@ const {
6467

6568
const {
6669
getArrayBufferOrView,
70+
getBufferSourceBytes,
6771
bigIntArrayToUnsignedBigInt,
6872
normalizeAlgorithm,
6973
hasAnyNotIn,
@@ -812,6 +816,65 @@ function createPublicKey(key) {
812816
return new PublicKeyObject(handle);
813817
}
814818

819+
/**
820+
* Parses a PKCS#12 (.p12 / .pfx) bundle. Returns an object holding the first
821+
* private key as `privateKey`, the certificate associated with it as
822+
* `certificate`, and every other certificate in the bundle as an array in
823+
* `additionalCertificates`. `privateKey` and `certificate` are null when the
824+
* bundle contains none.
825+
* @param {ArrayBuffer|Buffer|TypedArray|DataView} bundle
826+
* @param {object} [options]
827+
* @returns {object}
828+
*/
829+
function parsePKCS12(bundle, options = kEmptyObject) {
830+
if (!isArrayBufferView(bundle) && !isAnyArrayBuffer(bundle)) {
831+
throw new ERR_INVALID_ARG_TYPE(
832+
'bundle',
833+
['ArrayBuffer', 'TypedArray', 'DataView', 'Buffer'],
834+
bundle);
835+
}
836+
837+
validateObject(options, 'options');
838+
const { passphrase } = options;
839+
840+
// `undefined` means no passphrase, '' a zero-length one. OpenSSL accepts
841+
// either PKCS#12 password encoding for both, so they behave the same.
842+
let passBuf;
843+
if (passphrase !== undefined) {
844+
passBuf = getArrayBufferOrView(passphrase, 'options.passphrase', 'utf8');
845+
// OpenSSL takes the passphrase as a NUL-terminated C string, so one
846+
// containing a NUL byte would be truncated there and the bundle opened
847+
// with only the bytes before it. A PKCS#12 password cannot represent an
848+
// embedded NUL anyway, so reject it outright.
849+
if (TypedArrayPrototypeIncludes(getBufferSourceBytes(passBuf), 0)) {
850+
throw new ERR_INVALID_ARG_VALUE(
851+
'options.passphrase', passphrase, 'must not contain null bytes');
852+
}
853+
// The binding reads the passphrase as a view; wrap a bare ArrayBuffer.
854+
if (isAnyArrayBuffer(passBuf)) passBuf = Buffer.from(passBuf);
855+
}
856+
857+
// Likewise, the binding reads the bundle as a view.
858+
const bundleBuf = isAnyArrayBuffer(bundle) ? Buffer.from(bundle) : bundle;
859+
860+
const {
861+
0: keyHandle,
862+
1: certHandle,
863+
2: otherHandles,
864+
} = _parsePKCS12(bundleBuf, passBuf);
865+
866+
// Required lazily: internal/crypto/x509 depends on this module.
867+
const { InternalX509Certificate } = require('internal/crypto/x509');
868+
869+
return {
870+
privateKey: keyHandle === null ? null : new PrivateKeyObject(keyHandle),
871+
certificate:
872+
certHandle === null ? null : new InternalX509Certificate(certHandle),
873+
additionalCertificates:
874+
ArrayPrototypeMap(otherHandles, (h) => new InternalX509Certificate(h)),
875+
};
876+
}
877+
815878
/**
816879
* Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the
817880
* algorithm-specific Web Crypto import path.
@@ -1461,6 +1524,7 @@ module.exports = {
14611524
createSecretKey,
14621525
createPublicKey,
14631526
createPrivateKey,
1527+
parsePKCS12,
14641528
KeyObject,
14651529
CryptoKey,
14661530
InternalCryptoKey,

‎node.gyp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
'src/crypto/crypto_hash.cc',
408408
'src/crypto/crypto_keys.cc',
409409
'src/crypto/crypto_keygen.cc',
410+
'src/crypto/crypto_pkcs12.cc',
410411
'src/crypto/crypto_scrypt.cc',
411412
'src/crypto/crypto_tls.cc',
412413
'src/crypto/crypto_x509.cc',
@@ -426,6 +427,7 @@
426427
'src/crypto/crypto_hash.h',
427428
'src/crypto/crypto_keys.h',
428429
'src/crypto/crypto_keygen.h',
430+
'src/crypto/crypto_pkcs12.h',
429431
'src/crypto/crypto_scrypt.h',
430432
'src/crypto/crypto_tls.h',
431433
'src/crypto/crypto_context.h',

‎src/crypto/crypto_context.cc‎

Lines changed: 44 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "base_object-inl.h"
33
#include "crypto/crypto_bio.h"
44
#include "crypto/crypto_common.h"
5+
#include "crypto/crypto_pkcs12.h"
56
#include "crypto/crypto_util.h"
67
#include "env-inl.h"
78
#include "memory_tracker-inl.h"
@@ -15,7 +16,6 @@
1516
#ifdef NODE_OPENSSL_HAS_CERT_COMP
1617
#include <openssl/comp.h>
1718
#endif
18-
#include <openssl/pkcs12.h>
1919
#include <openssl/rand.h>
2020
#include <openssl/x509.h>
2121
#ifdef __APPLE__
@@ -2269,13 +2269,30 @@ void SecureContext::Close(const FunctionCallbackInfo<Value>& args) {
22692269
sc->Reset();
22702270
}
22712271

2272+
namespace {
2273+
// The historical error shape for the TLS `pfx` option: the OpenSSL reason
2274+
// string, except for OpenSSL 3's bare "unsupported" error, which on its own
2275+
// says nothing useful.
2276+
// TODO(@jasnell): Should this use ThrowCryptoError?
2277+
// NOLINTNEXTLINE(runtime/int) -- matches ERR_get_error()
2278+
void ThrowPFXError(Environment* env, unsigned long err) {
2279+
#if OPENSSL_VERSION_MAJOR >= 3
2280+
if (ERR_GET_REASON(err) == ERR_R_UNSUPPORTED) {
2281+
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
2282+
env, "Unsupported PKCS12 PFX data");
2283+
}
2284+
#endif
2285+
2286+
const char* str = ERR_reason_error_string(err);
2287+
str = str != nullptr ? str : "Unknown error";
2288+
env->ThrowError(str);
2289+
}
2290+
} // namespace
2291+
22722292
// Takes .pfx or .p12 and password in string or buffer format
22732293
void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
22742294
Environment* env = Environment::GetCurrent(args);
22752295

2276-
std::vector<char> pass;
2277-
bool ret = false;
2278-
22792296
SecureContext* sc;
22802297
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());
22812298
ClearErrorOnReturn clear_error_on_return;
@@ -2290,66 +2307,55 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
22902307
env, "Unable to load PFX certificate");
22912308
}
22922309

2310+
// PKCS12_parse() takes a NUL-terminated C string; a view is not one, so copy
2311+
// the bytes out. A passphrase is optional; nullptr means none was given.
2312+
std::vector<char> pass_storage;
2313+
const char* pass = nullptr;
22932314
if (args.Length() >= 2) {
22942315
THROW_AND_RETURN_IF_NOT_BUFFER(env, args[1], "Pass phrase");
22952316
Local<ArrayBufferView> abv = args[1].As<ArrayBufferView>();
22962317
size_t passlen = abv->ByteLength();
2297-
pass.resize(passlen + 1);
2298-
abv->CopyContents(pass.data(), passlen);
2299-
pass[passlen] = '\0';
2318+
pass_storage.resize(passlen + 1);
2319+
abv->CopyContents(pass_storage.data(), passlen);
2320+
pass_storage[passlen] = '\0';
2321+
pass = pass_storage.data();
23002322
}
23012323

23022324
// Free previous certs
23032325
sc->issuer_.reset();
23042326
sc->cert_.reset();
23052327

2306-
DeleteFnPtr<PKCS12, PKCS12_free> p12;
2307-
EVPKeyPointer pkey;
2308-
X509Pointer cert;
2309-
StackOfX509 extra_certs;
2310-
2311-
PKCS12* p12_ptr = nullptr;
2312-
EVP_PKEY* pkey_ptr = nullptr;
2313-
X509* cert_ptr = nullptr;
2314-
STACK_OF(X509)* extra_certs_ptr = nullptr;
2315-
2316-
if (!d2i_PKCS12_bio(in.get(), &p12_ptr)) {
2317-
goto done;
2318-
}
2319-
2320-
// Move ownership to the smart pointer:
2321-
p12.reset(p12_ptr);
2322-
2323-
if (!PKCS12_parse(
2324-
p12.get(), pass.data(), &pkey_ptr, &cert_ptr, &extra_certs_ptr)) {
2325-
goto done;
2328+
auto parsed = ParsePKCS12Bundle(in, pass);
2329+
if (!parsed) {
2330+
// Every parse failure carries the OpenSSL error that caused it, which is
2331+
// all this path needs: ThrowPFXError() derives the same message it always
2332+
// has, the UNSUPPORTED_ALGORITHM case included.
2333+
return ThrowPFXError(env, parsed.openssl_error.value_or(0));
23262334
}
23272335

2328-
// Move ownership of the parsed data:
2329-
pkey.reset(pkey_ptr);
2330-
cert.reset(cert_ptr);
2331-
extra_certs.reset(extra_certs_ptr);
2332-
2333-
if (!pkey) {
2336+
// Unlike crypto.parsePKCS12(), TLS needs both halves of the pair.
2337+
if (!parsed.value.key) {
23342338
return THROW_ERR_CRYPTO_OPERATION_FAILED(
23352339
env, "Unable to load private key from PFX data");
23362340
}
23372341

2338-
if (!cert) {
2342+
if (!parsed.value.cert) {
23392343
return THROW_ERR_CRYPTO_OPERATION_FAILED(
23402344
env, "Unable to load certificate from PFX data");
23412345
}
23422346

2347+
const StackOfX509& extra_certs = parsed.value.ca;
2348+
23432349
if (!SSL_CTX_use_certificate_chain(sc->ctx_.get(),
2344-
std::move(cert),
2350+
std::move(parsed.value.cert),
23452351
extra_certs.get(),
23462352
&sc->cert_,
23472353
&sc->issuer_)) {
2348-
goto done;
2354+
return ThrowPFXError(env, ERR_get_error());
23492355
}
23502356

2351-
if (!SSL_CTX_use_PrivateKey(sc->ctx_.get(), pkey.get())) {
2352-
goto done;
2357+
if (!SSL_CTX_use_PrivateKey(sc->ctx_.get(), parsed.value.key.get())) {
2358+
return ThrowPFXError(env, ERR_get_error());
23532359
}
23542360

23552361
// Add CA certs too
@@ -2359,27 +2365,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
23592365
X509_STORE_add_cert(sc->GetCertStoreOwnedByThisSecureContext(), ca);
23602366
CHECK_EQ(1, SSL_CTX_add_client_CA(sc->ctx_.get(), ca));
23612367
}
2362-
ret = true;
2363-
2364-
done:
2365-
if (!ret) {
2366-
// TODO(@jasnell): Should this use ThrowCryptoError?
2367-
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)
2368-
2369-
#if OPENSSL_VERSION_MAJOR >= 3
2370-
if (ERR_GET_REASON(err) == ERR_R_UNSUPPORTED) {
2371-
// OpenSSL's "unsupported" error without any context is very
2372-
// common and not very helpful, so we override it:
2373-
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
2374-
env, "Unsupported PKCS12 PFX data");
2375-
}
2376-
#endif
2377-
2378-
const char* str = ERR_reason_error_string(err);
2379-
str = str != nullptr ? str : "Unknown error";
2380-
2381-
return env->ThrowError(str);
2382-
}
23832368
}
23842369

23852370
#ifndef OPENSSL_NO_ENGINE

0 commit comments

Comments
 (0)