nodejs/node · #67083

v8: support X509Certificate serialization

panva · merged Nov 4, 20269 files · 277 + / 1 −
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;