Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -5518,6 +5518,39 @@ const derivedKey = hkdfSync('sha512', 'key', 'salt', 'info', 64);
console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653'
```

### `crypto.parsePKCS12(bundle[, options])`

<!-- YAML
added: REPLACEME
-->

* `bundle` {ArrayBuffer|Buffer|TypedArray|DataView} A DER-encoded PKCS#12
(`.p12` or `.pfx`) bundle.
* `options` {Object}
* `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} The passphrase
protecting the bundle. Omitting this option is equivalent to passing `''`.
* Returns: {Object}
* `privateKey` {KeyObject|null} The first private key in the bundle, or
`null` if none is present.
* `certificate` {X509Certificate|null} The certificate matching `privateKey`,
or `null` if no matching certificate is present.
* `additionalCertificates` {X509Certificate\[]} All other certificates in
the bundle. If there is no private key, this contains all certificates.
May be empty.

Parses a PKCS#12 bundle, commonly stored with a `.p12` or `.pfx` extension,
and returns its private key and certificates.

```mjs
import { parsePKCS12 } from 'node:crypto';
import { readFileSync } from 'node:fs';

const { privateKey, certificate, additionalCertificates } = parsePKCS12(
readFileSync('bundle.p12'),
{ passphrase: 'secret' },
);
```

### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`

<!-- YAML
Expand Down
2 changes: 2 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const {
createSecretKey,
createPublicKey,
createPrivateKey,
parsePKCS12,
KeyObject,
} = require('internal/crypto/keys');
const {
Expand Down Expand Up @@ -216,6 +217,7 @@ module.exports = {
getMacs,
hkdf,
hkdfSync,
parsePKCS12,
pbkdf2,
pbkdf2Sync,
generateKeyPair,
Expand Down
64 changes: 64 additions & 0 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'use strict';

const {
ArrayPrototypeMap,
ArrayPrototypeSlice,
ObjectDefineProperties,
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf,
StringPrototypeIncludes,
StringPrototypeStartsWith,
SymbolToStringTag,
TypedArrayPrototypeIncludes,
Uint8Array,
} = primordials;

Expand Down Expand Up @@ -35,6 +37,7 @@ const {
kKeyEncodingPKCS8,
kKeyEncodingSPKI,
kKeyEncodingSEC1,
parsePKCS12: _parsePKCS12,
} = internalBinding('crypto');

const {
Expand Down Expand Up @@ -63,6 +66,7 @@ const {

const {
getArrayBufferOrView,
getBufferSourceBytes,
bigIntArrayToUnsignedBigInt,
normalizeAlgorithm,
hasAnyNotIn,
Expand Down Expand Up @@ -784,6 +788,65 @@ function createPublicKey(key) {
return new PublicKeyObject(handle);
}

/**
* Parses a PKCS#12 (.p12 / .pfx) bundle. Returns an object holding the first
* private key as `privateKey`, the certificate associated with it as
* `certificate`, and every other certificate in the bundle as an array in
* `additionalCertificates`. `privateKey` and `certificate` are null when the
* bundle contains none.
* @param {ArrayBuffer|Buffer|TypedArray|DataView} bundle
* @param {object} [options]
* @returns {object}
*/
function parsePKCS12(bundle, options = kEmptyObject) {
if (!isArrayBufferView(bundle) && !isAnyArrayBuffer(bundle)) {
throw new ERR_INVALID_ARG_TYPE(
'bundle',
['ArrayBuffer', 'TypedArray', 'DataView', 'Buffer'],
bundle);
}

validateObject(options, 'options');
const { passphrase } = options;

// `undefined` means no passphrase, '' a zero-length one. OpenSSL accepts
// either PKCS#12 password encoding for both, so they behave the same.
let passBuf;
if (passphrase !== undefined) {
passBuf = getArrayBufferOrView(passphrase, 'options.passphrase', 'utf8');
// OpenSSL takes the passphrase as a NUL-terminated C string, so one
// containing a NUL byte would be truncated there and the bundle opened
// with only the bytes before it. A PKCS#12 password cannot represent an
// embedded NUL anyway, so reject it outright.
if (TypedArrayPrototypeIncludes(getBufferSourceBytes(passBuf), 0)) {
throw new ERR_INVALID_ARG_VALUE(
'options.passphrase', passphrase, 'must not contain null bytes');
}
// The binding reads the passphrase as a view; wrap a bare ArrayBuffer.
if (isAnyArrayBuffer(passBuf)) passBuf = Buffer.from(passBuf);
}

// Likewise, the binding reads the bundle as a view.
const bundleBuf = isAnyArrayBuffer(bundle) ? Buffer.from(bundle) : bundle;

const {
0: keyHandle,
1: certHandle,
2: otherHandles,
} = _parsePKCS12(bundleBuf, passBuf);

// Required lazily: internal/crypto/x509 depends on this module.
const { InternalX509Certificate } = require('internal/crypto/x509');

return {
privateKey: keyHandle === null ? null : new PrivateKeyObject(keyHandle),
certificate:
certHandle === null ? null : new InternalX509Certificate(certHandle),
additionalCertificates:
ArrayPrototypeMap(otherHandles, (h) => new InternalX509Certificate(h)),
};
}

/**
* Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the
* algorithm-specific Web Crypto import path.
Expand Down Expand Up @@ -1433,6 +1496,7 @@ module.exports = {
createSecretKey,
createPublicKey,
createPrivateKey,
parsePKCS12,
KeyObject,
CryptoKey,
InternalCryptoKey,
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@
'src/crypto/crypto_hash.cc',
'src/crypto/crypto_keys.cc',
'src/crypto/crypto_keygen.cc',
'src/crypto/crypto_pkcs12.cc',
'src/crypto/crypto_scrypt.cc',
'src/crypto/crypto_tls.cc',
'src/crypto/crypto_x509.cc',
Expand All @@ -432,6 +433,7 @@
'src/crypto/crypto_hash.h',
'src/crypto/crypto_keys.h',
'src/crypto/crypto_keygen.h',
'src/crypto/crypto_pkcs12.h',
'src/crypto/crypto_scrypt.h',
'src/crypto/crypto_tls.h',
'src/crypto/crypto_context.h',
Expand Down
103 changes: 44 additions & 59 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "base_object-inl.h"
#include "crypto/crypto_bio.h"
#include "crypto/crypto_common.h"
#include "crypto/crypto_pkcs12.h"
#include "crypto/crypto_tls_certificates.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
Expand All @@ -16,7 +17,6 @@
#ifdef NODE_OPENSSL_HAS_CERT_COMP
#include <openssl/comp.h>
#endif
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <openssl/x509.h>
#ifdef __APPLE__
Expand Down Expand Up @@ -2185,13 +2185,30 @@ void SecureContext::Close(const FunctionCallbackInfo<Value>& args) {
sc->Reset();
}

namespace {
// The historical error shape for the TLS `pfx` option: the OpenSSL reason
// string, except for OpenSSL 3's bare "unsupported" error, which on its own
// says nothing useful.
// TODO(@jasnell): Should this use ThrowCryptoError?
// NOLINTNEXTLINE(runtime/int) -- matches ERR_get_error()
void ThrowPFXError(Environment* env, unsigned long err) {
#if OPENSSL_VERSION_MAJOR >= 3
if (ERR_GET_REASON(err) == ERR_R_UNSUPPORTED) {
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env, "Unsupported PKCS12 PFX data");
}
#endif

const char* str = ERR_reason_error_string(err);
str = str != nullptr ? str : "Unknown error";
env->ThrowError(str);
}
} // namespace

// Takes .pfx or .p12 and password in string or buffer format
void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

std::vector<char> pass;
bool ret = false;

SecureContext* sc;
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());
ClearErrorOnReturn clear_error_on_return;
Expand All @@ -2206,66 +2223,55 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
env, "Unable to load PFX certificate");
}

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

// Free previous certs
sc->issuer_.reset();
sc->cert_.reset();

DeleteFnPtr<PKCS12, PKCS12_free> p12;
EVPKeyPointer pkey;
X509Pointer cert;
StackOfX509 extra_certs;

PKCS12* p12_ptr = nullptr;
EVP_PKEY* pkey_ptr = nullptr;
X509* cert_ptr = nullptr;
STACK_OF(X509)* extra_certs_ptr = nullptr;

if (!d2i_PKCS12_bio(in.get(), &p12_ptr)) {
goto done;
}

// Move ownership to the smart pointer:
p12.reset(p12_ptr);

if (!PKCS12_parse(
p12.get(), pass.data(), &pkey_ptr, &cert_ptr, &extra_certs_ptr)) {
goto done;
auto parsed = ParsePKCS12Bundle(in, pass);
if (!parsed) {
// Every parse failure carries the OpenSSL error that caused it, which is
// all this path needs: ThrowPFXError() derives the same message it always
// has, the UNSUPPORTED_ALGORITHM case included.
return ThrowPFXError(env, parsed.openssl_error.value_or(0));
}

// Move ownership of the parsed data:
pkey.reset(pkey_ptr);
cert.reset(cert_ptr);
extra_certs.reset(extra_certs_ptr);

if (!pkey) {
// Unlike crypto.parsePKCS12(), TLS needs both halves of the pair.
if (!parsed.value.key) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "Unable to load private key from PFX data");
}

if (!cert) {
if (!parsed.value.cert) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "Unable to load certificate from PFX data");
}

const StackOfX509& extra_certs = parsed.value.ca;

if (!SSL_CTX_use_certificate_chain(sc->ctx_.get(),
std::move(cert),
std::move(parsed.value.cert),
extra_certs.get(),
&sc->cert_,
&sc->issuer_)) {
goto done;
return ThrowPFXError(env, ERR_get_error());
}

if (!SSL_CTX_use_PrivateKey(sc->ctx_.get(), pkey.get())) {
goto done;
if (!SSL_CTX_use_PrivateKey(sc->ctx_.get(), parsed.value.key.get())) {
return ThrowPFXError(env, ERR_get_error());
}

// Add CA certs too
Expand All @@ -2275,27 +2281,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
X509_STORE_add_cert(sc->GetCertStoreOwnedByThisSecureContext(), ca);
CHECK_EQ(1, SSL_CTX_add_client_CA(sc->ctx_.get(), ca));
}
ret = true;

done:
if (!ret) {
// TODO(@jasnell): Should this use ThrowCryptoError?
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)

#if OPENSSL_VERSION_MAJOR >= 3
if (ERR_GET_REASON(err) == ERR_R_UNSUPPORTED) {
// OpenSSL's "unsupported" error without any context is very
// common and not very helpful, so we override it:
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env, "Unsupported PKCS12 PFX data");
}
#endif

const char* str = ERR_reason_error_string(err);
str = str != nullptr ? str : "Unknown error";

return env->ThrowError(str);
}
}

#ifndef OPENSSL_NO_ENGINE
Expand Down
Loading
Loading