diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js index cf1de4474756..0a0da0296a84 100644 --- a/lib/internal/crypto/x509.js +++ b/lib/internal/crypto/x509.js @@ -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, @@ -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, }; diff --git a/lib/v8.js b/lib/v8.js index ea3366bb9617..6ae1d2c8ee65 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -16,6 +16,7 @@ const { Array, + ArrayBufferIsView, BigInt64Array, BigUint64Array, DataView, @@ -27,6 +28,7 @@ const { Int32Array, Int8Array, JSONParse, + ObjectDefineProperty, ObjectPrototypeToString, SymbolDispose, Uint16Array, @@ -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'); @@ -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(); @@ -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); @@ -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); diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc index 3c208082f944..3a9c296970a5 100644 --- a/src/crypto/crypto_x509.cc +++ b/src/crypto/crypto_x509.cc @@ -128,6 +128,23 @@ std::shared_ptr X509CertificateSnapshot::Create( new X509CertificateSnapshot(std::move(certificates))); } +std::shared_ptr X509CertificateSnapshot::Create( + std::vector 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( + new X509CertificateSnapshot(std::move(certificates))); +} + X509Pointer X509CertificateSnapshot::Parse(size_t index) const { if (index >= certificates_.size()) return {}; return ParseCertificate(certificates_[index]); @@ -1065,6 +1082,76 @@ void X509Certificate::NewFromHandle(const FunctionCallbackInfo& args) { issuer); } +void X509Certificate::ParseSnapshot(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local 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 input = args[0].As(); + std::vector certificates; + certificates.reserve(input->Length()); + for (uint32_t i = 0; i < input->Length(); i++) { + Local 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 contents( + value.As()); + 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 cert; + if (New(env, std::move(snapshot)).ToLocal(&cert)) { + args.GetReturnValue().Set(cert); + } +} + +void X509Certificate::GetSnapshot(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local 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(args[0].As()); + 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(std::numeric_limits::max())); + Local output = Array::New(env->isolate(), static_cast(count)); + for (size_t i = 0; i < count; i++) { + const auto& der = cert->snapshot_->at(cert->snapshot_index_ + i); + Local buffer; + if (!Buffer::Copy( + env, reinterpret_cast(der.data()), der.size()) + .ToLocal(&buffer) || + !output->CreateDataProperty(context, static_cast(i), buffer) + .FromMaybe(false)) { + return; + } + } + args.GetReturnValue().Set(output); +} + void X509Certificate::CreateX509CertificateClass( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1324,6 +1411,9 @@ void X509Certificate::Initialize(Environment* env, Local 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); @@ -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); diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h index 44714d1bcca2..e618edd8e5a0 100644 --- a/src/crypto/crypto_x509.h +++ b/src/crypto/crypto_x509.h @@ -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 Create( + std::vector certificates); ncrypto::X509Pointer Parse(size_t index) const; @@ -89,6 +91,8 @@ class X509Certificate final : public BaseObject { static void IsX509Certificate( const v8::FunctionCallbackInfo& args); static void NewFromHandle(const v8::FunctionCallbackInfo& args); + static void ParseSnapshot(const v8::FunctionCallbackInfo& args); + static void GetSnapshot(const v8::FunctionCallbackInfo& args); static v8::MaybeLocal New( Environment* env, diff --git a/test/parallel/test-crypto-x509-chain-key-usage.js b/test/parallel/test-crypto-x509-chain-key-usage.js new file mode 100644 index 000000000000..100e2dbb9e5e --- /dev/null +++ b/test/parallel/test-crypto-x509-chain-key-usage.js @@ -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', + }); +} diff --git a/test/parallel/test-crypto-x509-chain-snapshot.js b/test/parallel/test-crypto-x509-chain-snapshot.js index 5a0f696da0f1..433abd9fccab 100644 --- a/test/parallel/test-crypto-x509-chain-snapshot.js +++ b/test/parallel/test-crypto-x509-chain-snapshot.js @@ -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, @@ -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(); diff --git a/test/parallel/test-crypto-x509-peer-chain.js b/test/parallel/test-crypto-x509-peer-chain.js index b60f3d00acc0..d14df8eda922 100644 --- a/test/parallel/test-crypto-x509-peer-chain.js +++ b/test/parallel/test-crypto-x509-peer-chain.js @@ -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'); @@ -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); diff --git a/test/parallel/test-v8-serdes.js b/test/parallel/test-v8-serdes.js index 2c7a20b24f96..0c7af6d78eba 100644 --- a/test/parallel/test-v8-serdes.js +++ b/test/parallel/test-v8-serdes.js @@ -56,6 +56,17 @@ const hostObject = new (internalBinding('js_stream').JSStream)(); } } +{ + const buffer = Buffer.from([1, 2, 3, 4]); + Object.defineProperty(buffer, Symbol.toStringTag, { + value: 'CustomBuffer', + }); + + const deserialized = v8.deserialize(v8.serialize(buffer)); + assert(Buffer.isBuffer(deserialized)); + assert.deepStrictEqual(deserialized, Buffer.from(buffer)); +} + { const ser = new v8.DefaultSerializer(); ser._getDataCloneError = common.mustCall((message) => { diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 3d5a9fceb2e6..b794f51bdd37 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -971,6 +971,9 @@ export interface CryptoBinding { getFipsCryptoGeneration(): bigint; getHashes(): string[]; getMacs(): string[]; + getX509CertificateSnapshot( + certificate: InternalCryptoBinding.X509CertificateHandle, + ): InternalCryptoBinding.Buffer[]; isCryptoKey(key: unknown): boolean; isKeyAlgorithmAvailable(name: string): boolean; isKeyObject(key: unknown): boolean; @@ -996,6 +999,9 @@ export interface CryptoBinding { passphrase?: InternalCryptoBinding.ByteSource, ): InternalCryptoBinding.PKCS12ParseResult; parseX509(data: InternalCryptoBinding.ByteSource): InternalCryptoBinding.X509CertificateHandle; + parseX509Snapshot( + snapshot: ArrayBufferView[], + ): InternalCryptoBinding.X509CertificateHandle; privateDecrypt: InternalCryptoBinding.PublicKeyCipher; privateEncrypt: InternalCryptoBinding.PublicKeyCipher; publicDecrypt: InternalCryptoBinding.PublicKeyCipher;