nodejs/node · #67083
v8: support X509Certificate serialization
lib/internal/crypto/x509.js16 + / 0 −
@@ -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, };lib/v8.js57 + / 1 −
@@ -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);src/crypto/crypto_x509.cc92 + / 0 −
@@ -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]);@@ -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);@@ -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);@@ -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);src/crypto/crypto_x509.h4 + / 0 −
@@ -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; @@ -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,test/parallel/test-crypto-x509-chain-key-usage.jsadded87 + / 0 −
@@ -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',+ });+}test/parallel/test-crypto-x509-chain-snapshot.js2 + / 0 −
@@ -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();test/parallel/test-crypto-x509-peer-chain.js2 + / 0 −
@@ -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);test/parallel/test-v8-serdes.js11 + / 0 −
@@ -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) => {typings/internalBinding/crypto.d.ts6 + / 0 −
@@ -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;