Skip to content
Open
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
16 changes: 16 additions & 0 deletions lib/internal/crypto/x509.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const {

const {
createX509CertificateClass,
getX509CertificateSnapshot: getNativeX509CertificateSnapshot,
isX509Certificate: isNativeX509Certificate,
parseX509Snapshot,
X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT,
X509_CHECK_FLAG_NEVER_CHECK_SUBJECT,
X509_CHECK_FLAG_NO_WILDCARDS,
Expand Down Expand Up @@ -427,8 +429,22 @@ const {
// Keep the binding-returned constructor and the closure reference aligned.
InternalX509Certificate = InternalX509CertificateConstructor;

function getX509CertificateSnapshot(certificate) {
if (!isX509Certificate(certificate)) {
throw new ERR_INVALID_ARG_TYPE(
'certificate', 'X509Certificate', certificate);
}
return getNativeX509CertificateSnapshot(certificate);
}

function createX509CertificateFromSnapshot(snapshot) {
return new InternalX509Certificate(parseX509Snapshot(snapshot));
}

module.exports = {
X509Certificate,
InternalX509Certificate,
createX509CertificateFromSnapshot,
getX509CertificateSnapshot,
isX509Certificate,
};
58 changes: 57 additions & 1 deletion lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const {
Array,
ArrayBufferIsView,
BigInt64Array,
BigUint64Array,
DataView,
Expand All @@ -27,6 +28,7 @@ const {
Int32Array,
Int8Array,
JSONParse,
ObjectDefineProperty,
ObjectPrototypeToString,
SymbolDispose,
Uint16Array,
Expand Down Expand Up @@ -55,12 +57,22 @@ const {
normalizeCpuProfileOptions,
} = require('internal/v8/cpu_profiler');

const {
hasInspector,
hasOpenSSL,
} = internalBinding('config');

let profiler = {};
if (internalBinding('config').hasInspector) {
if (hasInspector) {
profiler = internalBinding('profiler');
}

const assert = require('internal/assert');
const {
codes: {
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { inspect } = require('internal/util/inspect');
const { FastBuffer } = require('internal/buffer');
const { getValidatedPath } = require('internal/fs/utils');
Expand Down Expand Up @@ -438,6 +450,14 @@ function arrayBufferViewIndexToType(index) {
return undefined;
}

const kX509CertificateHostObject = 14;
let x509Serialization;

function getX509Serialization() {
x509Serialization ??= require('internal/crypto/x509');
return x509Serialization;
}

class DefaultSerializer extends Serializer {
constructor() {
super();
Expand All @@ -459,6 +479,23 @@ class DefaultSerializer extends Serializer {
// serialization. Because a) those may contain sensitive data and the user
// may not be aware of that and b) they are often much larger than the
// `Buffer` itself, custom serialization is applied.
if (!ArrayBufferIsView(abView) && hasOpenSSL) {
const {
getX509CertificateSnapshot,
isX509Certificate,
} = getX509Serialization();
if (isX509Certificate(abView)) {
const snapshot = getX509CertificateSnapshot(abView);
this.writeUint32(kX509CertificateHostObject);
this.writeUint32(snapshot.length);
for (let j = 0; j < snapshot.length; j++) {
this.writeUint32(snapshot[j].byteLength);
this.writeRawBytes(snapshot[j]);
}
return;
}
}

let i = 10; // FastBuffer
if (abView.constructor !== Buffer) {
i = arrayBufferViewTypeToIndex(abView);
Expand All @@ -483,6 +520,25 @@ class DefaultDeserializer extends Deserializer {
*/
_readHostObject() {
const typeIndex = this.readUint32();
if (typeIndex === kX509CertificateHostObject) {
const count = this.readUint32();
if (!hasOpenSSL || count === 0) {
throw new ERR_INVALID_STATE('Invalid serialized X509Certificate');
}
const snapshot = new Array(count);
for (let i = 0; i < count; i++) {
const byteLength = this.readUint32();
ObjectDefineProperty(snapshot, i, {
__proto__: null,
configurable: true,
enumerable: true,
value: this.readRawBytes(byteLength),
writable: true,
});
}
const { createX509CertificateFromSnapshot } = getX509Serialization();
return createX509CertificateFromSnapshot(snapshot);
}
const ctor = arrayBufferViewIndexToType(typeIndex);
const byteLength = this.readUint32();
const byteOffset = this._readRawBytes(byteLength);
Expand Down
92 changes: 92 additions & 0 deletions src/crypto/crypto_x509.cc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ std::shared_ptr<const X509CertificateSnapshot> X509CertificateSnapshot::Create(
new X509CertificateSnapshot(std::move(certificates)));
}

std::shared_ptr<const X509CertificateSnapshot> X509CertificateSnapshot::Create(
std::vector<DER> certificates) {
if (certificates.empty()) return {};

for (auto& certificate : certificates) {
auto cert = ParseCertificate(certificate);
if (!cert) return {};

auto der = EncodeCertificate(cert.view());
if (!der.has_value()) return {};
certificate = std::move(der.value());
}

return std::shared_ptr<const X509CertificateSnapshot>(
new X509CertificateSnapshot(std::move(certificates)));
}

X509Pointer X509CertificateSnapshot::Parse(size_t index) const {
if (index >= certificates_.size()) return {};
return ParseCertificate(certificates_[index]);
Expand Down Expand Up @@ -1065,6 +1082,76 @@ void X509Certificate::NewFromHandle(const FunctionCallbackInfo<Value>& args) {
issuer);
}

void X509Certificate::ParseSnapshot(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
if (args.Length() != 1 || !args[0]->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(env,
"The \"snapshot\" argument must be an Array");
return;
}

Local<Array> input = args[0].As<Array>();
std::vector<X509CertificateSnapshot::DER> certificates;
certificates.reserve(input->Length());
for (uint32_t i = 0; i < input->Length(); i++) {
Local<Value> value;
if (!input->Get(context, i).ToLocal(&value)) return;
if (!value->IsArrayBufferView()) {
THROW_ERR_INVALID_ARG_TYPE(
env, "The \"snapshot\" entries must be ArrayBufferViews");
return;
}

ArrayBufferViewContents<unsigned char> contents(
value.As<ArrayBufferView>());
certificates.emplace_back(contents.data(),
contents.data() + contents.length());
}

auto snapshot = X509CertificateSnapshot::Create(std::move(certificates));
if (!snapshot) [[unlikely]] {
THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "Failed to create X.509 certificate snapshot");
return;
}

Local<Object> cert;
if (New(env, std::move(snapshot)).ToLocal(&cert)) {
args.GetReturnValue().Set(cert);
}
}

void X509Certificate::GetSnapshot(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
if (args.Length() != 1 || !HasInstance(env, args[0])) {
THROW_ERR_INVALID_ARG_TYPE(env,
"The \"certificate\" argument must be an "
"X509CertificateHandle");
return;
}

X509Certificate* cert = Unwrap<X509Certificate>(args[0].As<Object>());
CHECK_NOT_NULL(cert);
CHECK_LT(cert->snapshot_index_, cert->snapshot_->size());
const size_t count = cert->snapshot_->size() - cert->snapshot_index_;
CHECK_LE(count, static_cast<size_t>(std::numeric_limits<int>::max()));
Local<Array> output = Array::New(env->isolate(), static_cast<int>(count));
for (size_t i = 0; i < count; i++) {
const auto& der = cert->snapshot_->at(cert->snapshot_index_ + i);
Local<Object> buffer;
if (!Buffer::Copy(
env, reinterpret_cast<const char*>(der.data()), der.size())
.ToLocal(&buffer) ||
!output->CreateDataProperty(context, static_cast<uint32_t>(i), buffer)
.FromMaybe(false)) {
return;
}
}
args.GetReturnValue().Set(output);
}

void X509Certificate::CreateX509CertificateClass(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Expand Down Expand Up @@ -1324,6 +1411,9 @@ void X509Certificate::Initialize(Environment* env, Local<Object> target) {
SetMethodNoSideEffect(
env->context(), target, "isX509Certificate", IsX509Certificate);
SetMethod(env->context(), target, "parseX509", Parse);
SetMethod(env->context(), target, "parseX509Snapshot", ParseSnapshot);
SetMethodNoSideEffect(
env->context(), target, "getX509CertificateSnapshot", GetSnapshot);

NODE_DEFINE_CONSTANT(target, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
NODE_DEFINE_CONSTANT(target, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
Expand All @@ -1339,6 +1429,8 @@ void X509Certificate::RegisterExternalReferences(
registry->Register(IsX509Certificate);
registry->Register(NewFromHandle);
registry->Register(Parse);
registry->Register(ParseSnapshot);
registry->Register(GetSnapshot);
registry->Register(Subject);
registry->Register(SubjectAltName);
registry->Register(InfoAccess);
Expand Down
4 changes: 4 additions & 0 deletions src/crypto/crypto_x509.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class X509CertificateSnapshot final : public MemoryRetainer {
ncrypto::X509View leaf,
const STACK_OF(X509) * issuer_chain = nullptr,
OPENSSL_SIZE_T issuers_start = 0);
static std::shared_ptr<const X509CertificateSnapshot> Create(
std::vector<DER> certificates);

ncrypto::X509Pointer Parse(size_t index) const;

Expand Down Expand Up @@ -89,6 +91,8 @@ class X509Certificate final : public BaseObject {
static void IsX509Certificate(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void NewFromHandle(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ParseSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);

static v8::MaybeLocal<v8::Object> New(
Environment* env,
Expand Down
87 changes: 87 additions & 0 deletions test/parallel/test-crypto-x509-chain-key-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Flags: --expose-internals --no-warnings
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('node:assert');
const { X509Certificate } = require('node:crypto');
const { deserialize, serialize } = require('node:v8');
const {
createX509CertificateFromSnapshot,
} = require('internal/crypto/x509');
const fixtures = require('../common/fixtures');

const leaf = new X509Certificate(`-----BEGIN CERTIFICATE-----
MIIBHTCBxaADAgECAgECMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD3NuYXBzaG90
IGlzc3VlcjAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBgxFjAUBgNV
BAMMDXNuYXBzaG90IGxlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQYBC5j
NN5rAvdjqeCKGhuSxH7DSi35DDi7dArQbeGw5FzdtQmOpglNYbi+TfmcRwkPouuR
i1cCQjEtL+3TW51PMAoGCCqGSM49BAMCA0cAMEQCIE/FmO4pJThTdhYs3kHamBe9
Qo9qR4urR0ZVNoS320q2AiBxs3QJcbhz18plOnwj2n6TTcevxtJauggAca+AVUg+
HQ==
-----END CERTIFICATE-----`);

const issuer = new X509Certificate(`-----BEGIN CERTIFICATE-----
MIIBQjCB6aADAgECAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD3NuYXBzaG90
IGlzc3VlcjAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBoxGDAWBgNV
BAMMD3NuYXBzaG90IGlzc3VlcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABG4H
eatluz8bc/iTzQN0Ada94gDZpq5E1g+xP/6WAKXILrnvRBZhRBi2xIJExYu1QZq2
+mvnupnjSy7kJUdBz2WjIDAeMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgeA
MAoGCCqGSM49BAMCA0gAMEUCIQDqKmAVHvhW0i87KWzp8sjUWId5IVWSgi7iqJx+
los0qwIgOALGVcPyyV56QNJGOIYCcXNLPQ9Adbs2arUVYcWGNEU=
-----END CERTIFICATE-----`);

// The issuer's Key Usage omits keyCertSign. Inspecting or copying the chain
// does not verify its certificates.
assert.strictEqual(leaf.verify(issuer.publicKey), true);
assert.strictEqual(leaf.checkIssued(issuer), false);

function assertChain(certificate, expected) {
const seen = new Set();
for (const raw of expected) {
assert(certificate instanceof X509Certificate);
assert(!seen.has(certificate));
seen.add(certificate);
assert.deepStrictEqual(certificate.raw, raw);
certificate = certificate.issuerCertificate;
}
assert.strictEqual(certificate, undefined);
}

const unrelated = new X509Certificate(fixtures.readKey('agent1-cert.pem'));

// A recognized extension can be malformed without making the enclosing
// certificate invalid DER. Snapshotting does not interpret extensions.
const malformedIssuer = Buffer.from(issuer.raw);
const basicConstraints = Buffer.from('30030101ff', 'hex');
const basicConstraintsOffset = malformedIssuer.indexOf(basicConstraints);
assert.notStrictEqual(basicConstraintsOffset, -1);
malformedIssuer[basicConstraintsOffset] = 0x31;
assert.deepStrictEqual(new X509Certificate(malformedIssuer).raw, malformedIssuer);

for (const expected of [
[leaf.raw, issuer.raw],
[leaf.raw, leaf.raw, issuer.raw],
[leaf.raw, issuer.raw, leaf.raw],
[issuer.raw, leaf.raw],
[leaf.raw, unrelated.raw, issuer.raw],
[leaf.raw, malformedIssuer],
]) {
const certificate = createX509CertificateFromSnapshot(expected);
assertChain(certificate, expected);
assertChain(structuredClone(certificate), expected);
assertChain(deserialize(serialize(certificate)), expected);
}

for (const snapshot of [
[],
[Buffer.alloc(0)],
[leaf.raw, Buffer.from('not a certificate')],
[leaf.raw, Buffer.concat([issuer.raw, Buffer.from([0])])],
]) {
assert.throws(() => createX509CertificateFromSnapshot(snapshot), {
code: 'ERR_CRYPTO_OPERATION_FAILED',
});
}
2 changes: 2 additions & 0 deletions test/parallel/test-crypto-x509-chain-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const assert = require('node:assert');
const { X509Certificate } = require('node:crypto');
const { once } = require('node:events');
const tls = require('node:tls');
const { deserialize, serialize } = require('node:v8');
const {
MessageChannel,
Worker,
Expand Down Expand Up @@ -88,6 +89,7 @@ const server = tls.createServer({
assertChain(structuredClone(first));
assertChain(await cloneViaMessagePort(first));
assertChain(await cloneViaWorker(first));
assertChain(deserialize(serialize(first)));

socket.destroy();
server.close();
Expand Down
2 changes: 2 additions & 0 deletions test/parallel/test-crypto-x509-peer-chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const assert = require('node:assert');
const { X509Certificate } = require('node:crypto');
const { once } = require('node:events');
const tls = require('node:tls');
const { deserialize, serialize } = require('node:v8');
const { MessageChannel, Worker } = require('node:worker_threads');
const fixtures = require('../common/fixtures');

Expand Down Expand Up @@ -38,6 +39,7 @@ function assertChain(actual, expected) {

async function assertClones(certificate, chain) {
assertChain(structuredClone(certificate), chain);
assertChain(deserialize(serialize(certificate)), chain);

const { port1, port2 } = new MessageChannel();
port1.postMessage(certificate);
Expand Down
Loading
Loading