diff --git a/benchmark/crypto/class-construction.js b/benchmark/crypto/class-construction.js new file mode 100644 index 000000000000..fe03b1a43cd2 --- /dev/null +++ b/benchmark/crypto/class-construction.js @@ -0,0 +1,86 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('node:assert'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys'); +const certificate = fs.readFileSync(path.join(fixtureDir, 'agent1-cert.pem')); +const dhPrime = crypto.getDiffieHellman('modp14').getPrime(); +const key = Buffer.alloc(32, 0x01); +const keyObject = crypto.createSecretKey(key); +const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' }; +const keyUsages = ['sign']; +const iv = Buffer.alloc(16, 0x02); + +const iterations = { + Certificate: 1e7, + Cipheriv: 1e5, + Decipheriv: 1e5, + DiffieHellman: 10, + DiffieHellmanGroup: 2e5, + ECDH: 2e5, + Hash: 1e5, + Hmac: 5e4, + KeyObject: 1e5, + Sign: 1e5, + Verify: 1e5, + CryptoKey: 5e4, + X509Certificate: 5e3, +}; + +const bench = common.createBenchmark(main, { + type: Object.keys(iterations), + n: [...new Set(Object.values(iterations))], +}, { + combinationFilter({ type, n }) { + // Benchmark test mode reduces numeric options to 1. + return n === 1 || iterations[type] === n; + }, +}); + +function construct(type) { + switch (type) { + case 'Certificate': + return new crypto.Certificate(); + case 'Cipheriv': + return crypto.createCipheriv('aes-256-ctr', key, iv); + case 'Decipheriv': + return crypto.createDecipheriv('aes-256-ctr', key, iv); + case 'DiffieHellman': + return crypto.createDiffieHellman(dhPrime); + case 'DiffieHellmanGroup': + return crypto.getDiffieHellman('modp14'); + case 'ECDH': + return crypto.createECDH('prime256v1'); + case 'Hash': + return crypto.createHash('sha256'); + case 'Hmac': + return crypto.createHmac('sha256', key); + case 'KeyObject': + return crypto.createSecretKey(key); + case 'Sign': + return crypto.createSign('sha256'); + case 'Verify': + return crypto.createVerify('sha256'); + case 'CryptoKey': + return keyObject.toCryptoKey(hmacAlgorithm, true, keyUsages); + case 'X509Certificate': + return new crypto.X509Certificate(certificate); + default: + throw new Error(`Unsupported class: ${type}`); + } +} + +function main({ type, n }) { + const instances = new Array(n); + + bench.start(); + for (let i = 0; i < n; ++i) + instances[i] = construct(type); + bench.end(n); + + assert.strictEqual(typeof instances[n - 1], 'object'); +} diff --git a/benchmark/crypto/class-methods.js b/benchmark/crypto/class-methods.js new file mode 100644 index 000000000000..9db499b7d1a9 --- /dev/null +++ b/benchmark/crypto/class-methods.js @@ -0,0 +1,206 @@ +'use strict'; + +const common = require('../common.js'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +const fixtureDir = path.resolve(__dirname, '../../test/fixtures/keys'); +const certificate = fs.readFileSync(path.join(fixtureDir, 'agent1-cert.pem')); +const key = Buffer.alloc(32, 0x01); +const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' }; +const keyUsages = ['sign']; +const iv = Buffer.alloc(16, 0x02); +const input = Buffer.alloc(16, 0x03); + +const iterations = { + 'Cipheriv-update': 1e6, + 'Decipheriv-update': 1e6, + 'DiffieHellman-getGenerator': 2e5, + 'DiffieHellmanGroup-getGenerator': 2e6, + 'ECDH-getPrivateKey': 2e6, + 'Hash-update': 5e6, + 'Hmac-update': 5e6, + 'KeyObject-equals': 2e6, + 'KeyObject-symmetricKeySize-first': 1e5, + 'KeyObject-type-first': 1e5, + 'KeyObject-type': 1e8, + 'Sign-update': 5e6, + 'Verify-update': 5e6, + 'CryptoKey-toKeyObject': 2e5, + 'CryptoKey-algorithm-first': 1e5, + 'CryptoKey-extractable-first': 1e5, + 'CryptoKey-type-first': 1e5, + 'CryptoKey-usages-first': 1e5, + 'CryptoKey-type': 1e8, + 'X509Certificate-checkHost': 1e6, + 'X509Certificate-publicKey-first': 5e3, + 'X509Certificate-publicKey': 1e8, + 'X509Certificate-subject-first': 5e3, + 'X509Certificate-subject': 1e8, +}; + +const bench = common.createBenchmark(main, { + operation: Object.keys(iterations), + n: [...new Set(Object.values(iterations))], +}, { + combinationFilter({ operation, n }) { + // Benchmark test mode reduces numeric options to 1. + return n === 1 || iterations[operation] === n; + }, +}); + +function setup(operation) { + switch (operation) { + case 'Cipheriv-update': { + const cipher = crypto.createCipheriv('aes-256-ctr', key, iv); + return { + run: () => cipher.update(input), + finish: () => cipher.final(), + }; + } + case 'Decipheriv-update': { + const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv); + return { + run: () => decipher.update(input), + finish: () => decipher.final(), + }; + } + case 'DiffieHellman-getGenerator': { + const dh = crypto.createDiffieHellman( + crypto.getDiffieHellman('modp14').getPrime()); + return { run: () => dh.getGenerator() }; + } + case 'DiffieHellmanGroup-getGenerator': { + const dh = crypto.getDiffieHellman('modp14'); + return { run: () => dh.getGenerator() }; + } + case 'ECDH-getPrivateKey': { + const ecdh = crypto.createECDH('prime256v1'); + ecdh.generateKeys(); + return { run: () => ecdh.getPrivateKey() }; + } + case 'Hash-update': { + const hash = crypto.createHash('sha256'); + return { + run: () => hash.update(input), + finish: () => hash.digest(), + }; + } + case 'Hmac-update': { + const hmac = crypto.createHmac('sha256', key); + return { + run: () => hmac.update(input), + finish: () => hmac.digest(), + }; + } + case 'KeyObject-equals': { + const keyObject = crypto.createSecretKey(key); + return { run: () => keyObject.equals(keyObject) }; + } + case 'KeyObject-symmetricKeySize-first': { + return { run: () => crypto.createSecretKey(key).symmetricKeySize }; + } + case 'KeyObject-type-first': { + return { run: () => crypto.createSecretKey(key).type }; + } + case 'KeyObject-type': { + const keyObjects = Array.from( + { length: 64 }, () => crypto.createSecretKey(key)); + for (const keyObject of keyObjects) { + if (keyObject.type !== 'secret') + throw new Error('Unexpected KeyObject type'); + } + let index = 0; + return { run: () => keyObjects[index++ & 63].type }; + } + case 'Sign-update': { + const sign = crypto.createSign('sha256'); + return { run: () => sign.update(input) }; + } + case 'Verify-update': { + const verify = crypto.createVerify('sha256'); + return { run: () => verify.update(input) }; + } + case 'CryptoKey-toKeyObject': { + const keyObject = crypto.createSecretKey(key); + const cryptoKey = keyObject.toCryptoKey( + hmacAlgorithm, true, keyUsages); + return { run: () => crypto.KeyObject.from(cryptoKey) }; + } + case 'CryptoKey-algorithm-first': + case 'CryptoKey-extractable-first': + case 'CryptoKey-type-first': + case 'CryptoKey-usages-first': { + const keyObject = crypto.createSecretKey(key); + const property = operation.slice('CryptoKey-'.length, -'-first'.length); + return { + run: () => keyObject.toCryptoKey( + hmacAlgorithm, true, keyUsages)[property], + }; + } + case 'CryptoKey-type': { + const keyObject = crypto.createSecretKey(key); + const cryptoKeys = Array.from( + { length: 64 }, + () => keyObject.toCryptoKey(hmacAlgorithm, true, keyUsages)); + for (const cryptoKey of cryptoKeys) { + if (cryptoKey.type !== 'secret') + throw new Error('Unexpected CryptoKey type'); + } + let index = 0; + return { run: () => cryptoKeys[index++ & 63].type }; + } + case 'X509Certificate-checkHost': { + const x509 = new crypto.X509Certificate(certificate); + return { run: () => x509.checkHost('agent1') }; + } + case 'X509Certificate-publicKey-first': { + return { + run: () => new crypto.X509Certificate(certificate).publicKey, + }; + } + case 'X509Certificate-publicKey': { + const certificates = Array.from( + { length: 64 }, () => new crypto.X509Certificate(certificate)); + for (const x509 of certificates) { + if (x509.publicKey === undefined) + throw new Error('Missing certificate public key'); + } + let index = 0; + return { run: () => certificates[index++ & 63].publicKey }; + } + case 'X509Certificate-subject-first': { + return { + run: () => new crypto.X509Certificate(certificate).subject, + }; + } + case 'X509Certificate-subject': { + const certificates = Array.from( + { length: 64 }, () => new crypto.X509Certificate(certificate)); + for (const x509 of certificates) { + if (x509.subject === undefined) + throw new Error('Missing certificate subject'); + } + let index = 0; + return { run: () => certificates[index++ & 63].subject }; + } + default: + throw new Error(`Unsupported operation: ${operation}`); + } +} + +function main({ operation, n }) { + const state = setup(operation); + let result; + + bench.start(); + for (let i = 0; i < n; ++i) + result = state.run(); + bench.end(n); + + if (state.finish) + state.finish(); + if (result === state) + throw new Error('Unexpected benchmark result'); +} diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index aedf74633e37..638e60c923b7 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -445,8 +445,8 @@ export default [ 'node-core/lowercase-name-for-primitive': 'error', 'node-core/non-ascii-character': 'error', 'node-core/no-array-destructuring': 'error', + 'node-core/no-crypto-class-instanceof': 'error', 'node-core/no-cryptokey-public-accessors': 'error', - 'node-core/no-keyobject-cryptokey-instanceof': 'error', 'node-core/no-keyobject-public-accessors': 'error', 'node-core/prefer-primordials': [ 'error', diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 2bc63a2419fa..1623825b5273 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -103,12 +103,13 @@ for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) encodingNames[m[0]] = m[1]; -// KeyObject state lives on the native NativeKeyObject base class. JS reads -// the native type enum and a KeyObjectHandle in one call and caches that -// slot tuple in a private field so no forgeable own Symbols are exposed on -// public KeyObject instances. +// KeyObject state lives on the native NativeKeyObject base class. JS caches +// the constructor-known type enum in a private field, then replaces it with +// the native type and KeyObjectHandle tuple when the handle is first needed. +// No forgeable own Symbols are exposed on public KeyObject instances. let getKeyObjectSlots; // Populated by the createNativeKeyObjectClass callback. let isKeyObject; +let getKeyObjectTypeValue; const kKeyObjectSlotType = 0; const kKeyObjectSlotHandle = 1; @@ -148,12 +149,18 @@ const { #slots; constructor(type, handle) { - if (type !== 'secret' && type !== 'public' && type !== 'private') - throw new ERR_INVALID_ARG_VALUE('type', type); + let keyType; + switch (type) { + case 'secret': keyType = kKeyTypeSecret; break; + case 'public': keyType = kKeyTypePublic; break; + case 'private': keyType = kKeyTypePrivate; break; + default: throw new ERR_INVALID_ARG_VALUE('type', type); + } if (typeof handle !== 'object' || !(handle instanceof KeyObjectHandle)) throw new ERR_INVALID_ARG_TYPE('handle', 'object', handle); super(handle); + this.#slots = keyType; } get type() { @@ -223,12 +230,23 @@ const { if (key == null || typeof key !== 'object') return false; return #slots in key || isNativeKeyObject(key); }; + getKeyObjectTypeValue = (key) => { + if (!key || typeof key !== 'object') + throw new ERR_INVALID_THIS('KeyObject'); + if (#slots in key) { + const cached = key.#slots; + if (typeof cached === 'number') return cached; + if (cached !== undefined) return cached[kKeyObjectSlotType]; + } + return getKeyObjectSlots(key)[kKeyObjectSlotType]; + }; + getKeyObjectSlots = (key) => { if (!key || typeof key !== 'object') throw new ERR_INVALID_THIS('KeyObject'); if (#slots in key) { const cached = key.#slots; - if (cached !== undefined) return cached; + if (cached !== undefined && typeof cached !== 'number') return cached; } const slots = nativeGetKeyObjectSlots(key); key.#slots = slots; @@ -935,7 +953,7 @@ function keyObjectTypeToString(type) { * @returns {'secret' | 'public' | 'private'} */ function getKeyObjectType(key) { - return keyObjectTypeToString(getKeyObjectSlots(key)[kKeyObjectSlotType]); + return keyObjectTypeToString(getKeyObjectTypeValue(key)); } /** @@ -1036,11 +1054,11 @@ function getKeyObjectAsymmetricKeyDetails(key) { // -> CryptoKey.prototype // -> Object.prototype // -// All five internal slots are read from C++ in a single call via -// `getCryptoKeySlots`. The resulting array is cached in a private -// class field on `InternalCryptoKey` so that it is invisible to -// reflection (`Object.getOwnPropertySymbols` etc.) and leaves each -// CryptoKey's hidden class pristine. The `getCryptoKey{Type, +// Normal construction caches all five internal slots in a private class +// field on `InternalCryptoKey`. Partially initialized transferred keys read +// them from C++ in a single `getCryptoKeySlots` call on first access. The +// cache is invisible to reflection (`Object.getOwnPropertySymbols` etc.) and +// leaves each CryptoKey's hidden class pristine. The `getCryptoKey{Type, // Extractable,Algorithm,Usages,Handle}` helpers index into that // array and convert native enums/masks back to Web Crypto strings. // The internal algorithm object is stored as a null-prototype clone @@ -1149,6 +1167,15 @@ const { if (algorithm !== undefined) algorithm = cloneInternalAlgorithm(algorithm); super(handle, algorithm, usagesMask, extractable); + if (algorithm !== undefined) { + this.#slots = [ + handle.getKeyType(), + extractable, + algorithm, + usagesMask, + handle, + ]; + } } static { @@ -1189,12 +1216,10 @@ const { return [CryptoKey, InternalCryptoKey]; }); -// The helpers below return a CryptoKey's internal slot value, -// populating the per-instance cache on first access via a single -// native call. The public `type` getter converts the native enum to -// the Web Crypto string. The `usages` helper converts the native usage -// mask to Web Crypto strings. The public `algorithm` / `usages` getters -// on `CryptoKey.prototype` cache their returned objects. +// The helpers below return a CryptoKey's internal slot value. The public +// `type` getter converts the native enum to the Web Crypto string. The +// `usages` helper converts the native usage mask to Web Crypto strings. The +// public `algorithm` / `usages` getters cache their returned objects. /** * Returns the value of a CryptoKey's `[[type]]` internal slot. diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js index a75425ffcac8..cf1de4474756 100644 --- a/lib/internal/crypto/x509.js +++ b/lib/internal/crypto/x509.js @@ -1,13 +1,15 @@ 'use strict'; const { + FunctionPrototypeCall, + ObjectGetPrototypeOf, ObjectSetPrototypeOf, - SafeMap, - Symbol, + SafeWeakMap, } = primordials; const { - parseX509, + createX509CertificateClass, + isX509Certificate: isNativeX509Certificate, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, X509_CHECK_FLAG_NO_WILDCARDS, @@ -46,26 +48,13 @@ const { codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, + ERR_INVALID_THIS, }, } = require('internal/errors'); -const { - markTransferMode, - kClone, - kDeserialize, -} = require('internal/worker/js_transferable'); - -const { - kHandle, -} = require('internal/crypto/util'); - let lazyTranslatePeerCertificate; -const kInternalState = Symbol('kInternalState'); - -function isX509Certificate(value) { - return value[kInternalState] !== undefined; -} +let isX509Certificate; function getFlags(options = kEmptyObject) { validateObject(options, 'options'); @@ -96,312 +85,347 @@ function getFlags(options = kEmptyObject) { return flags; } -class InternalX509Certificate { - [kInternalState] = new SafeMap(); - - constructor(handle) { - markTransferMode(this, true, false); - this[kHandle] = handle; - } +const crossRealmState = new SafeWeakMap(); + +const kSlotSubject = 0; +const kSlotSubjectAltName = 1; +const kSlotIssuer = 2; +const kSlotIssuerCertificate = 3; +const kSlotInfoAccess = 4; +const kSlotValidFrom = 5; +const kSlotValidTo = 6; +const kSlotValidFromDate = 7; +const kSlotValidToDate = 8; +const kSlotFingerprint = 9; +const kSlotFingerprint256 = 10; +const kSlotFingerprint512 = 11; +const kSlotKeyUsage = 12; +const kSlotSerialNumber = 13; +const kSlotSignatureAlgorithm = 14; +const kSlotSignatureAlgorithmOid = 15; +const kSlotRaw = 16; +const kSlotPublicKey = 17; +const kSlotPem = 18; +const kSlotCA = 19; +const kStateCacheMask = 20; + +function createState() { + const state = []; + state[kStateCacheMask] = 0; + return state; } -class X509Certificate { - [kInternalState] = new SafeMap(); - - constructor(buffer) { - if (typeof buffer === 'string') - buffer = Buffer.from(buffer); - if (!isArrayBufferView(buffer)) { - throw new ERR_INVALID_ARG_TYPE( - 'buffer', - ['string', 'Buffer', 'TypedArray', 'DataView'], - buffer); +let InternalX509Certificate; + +const { + 0: X509Certificate, + 1: InternalX509CertificateConstructor, +} = createX509CertificateClass((NativeX509Certificate) => { + const { + checkCA: nativeCheckCA, + checkEmail: nativeCheckEmail, + checkHost: nativeCheckHost, + checkIP: nativeCheckIP, + checkIssued: nativeCheckIssued, + checkPrivateKey: nativeCheckPrivateKey, + fingerprint: nativeFingerprint, + fingerprint256: nativeFingerprint256, + fingerprint512: nativeFingerprint512, + getIssuerCert: nativeGetIssuerCert, + infoAccess: nativeInfoAccess, + issuer: nativeIssuer, + keyUsage: nativeKeyUsage, + pem: nativePem, + publicKey: nativePublicKey, + raw: nativeRaw, + serialNumber: nativeSerialNumber, + signatureAlgorithm: nativeSignatureAlgorithm, + signatureAlgorithmOid: nativeSignatureAlgorithmOid, + subject: nativeSubject, + subjectAltName: nativeSubjectAltName, + toLegacy: nativeToLegacy, + validFrom: nativeValidFrom, + validFromDate: nativeValidFromDate, + validTo: nativeValidTo, + validToDate: nativeValidToDate, + verify: nativeVerify, + } = NativeX509Certificate.prototype; + + let getState; + + function getCachedValue(cert, index, getter) { + const state = getState(cert); + const bit = 1 << index; + if ((state[kStateCacheMask] & bit) === 0) { + state[index] = FunctionPrototypeCall(getter, cert); + state[kStateCacheMask] |= bit; + } + return state[index]; + } + + class X509Certificate { + constructor(buffer) { + if (typeof buffer === 'string') + buffer = Buffer.from(buffer); + if (!isArrayBufferView(buffer)) { + throw new ERR_INVALID_ARG_TYPE( + 'buffer', + ['string', 'Buffer', 'TypedArray', 'DataView'], + buffer); + } + const prototype = ObjectGetPrototypeOf(this); + const certificate = new InternalX509Certificate(buffer); + ObjectSetPrototypeOf(certificate, prototype); + // eslint-disable-next-line no-constructor-return + return certificate; } - markTransferMode(this, true, false); - this[kHandle] = parseX509(buffer); - } - [kInspect](depth, options) { - if (depth < 0) - return this; - - const opts = { - ...options, - depth: options.depth == null ? null : options.depth - 1, - }; - - return `X509Certificate ${inspect({ - subject: this.subject, - subjectAltName: this.subjectAltName, - issuer: this.issuer, - infoAccess: this.infoAccess, - validFrom: this.validFrom, - validTo: this.validTo, - validFromDate: this.validFromDate, - validToDate: this.validToDate, - fingerprint: this.fingerprint, - fingerprint256: this.fingerprint256, - fingerprint512: this.fingerprint512, - keyUsage: this.keyUsage, - serialNumber: this.serialNumber, - signatureAlgorithm: this.signatureAlgorithm, - signatureAlgorithmOid: this.signatureAlgorithmOid, - }, opts)}`; - } + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `X509Certificate ${inspect({ + subject: getCachedValue(this, kSlotSubject, nativeSubject), + subjectAltName: + getCachedValue(this, kSlotSubjectAltName, nativeSubjectAltName), + issuer: getCachedValue(this, kSlotIssuer, nativeIssuer), + infoAccess: getCachedValue(this, kSlotInfoAccess, nativeInfoAccess), + validFrom: getCachedValue(this, kSlotValidFrom, nativeValidFrom), + validTo: getCachedValue(this, kSlotValidTo, nativeValidTo), + validFromDate: + getCachedValue(this, kSlotValidFromDate, nativeValidFromDate), + validToDate: + getCachedValue(this, kSlotValidToDate, nativeValidToDate), + fingerprint: + getCachedValue(this, kSlotFingerprint, nativeFingerprint), + fingerprint256: + getCachedValue(this, kSlotFingerprint256, nativeFingerprint256), + fingerprint512: + getCachedValue(this, kSlotFingerprint512, nativeFingerprint512), + keyUsage: getCachedValue(this, kSlotKeyUsage, nativeKeyUsage), + serialNumber: + getCachedValue(this, kSlotSerialNumber, nativeSerialNumber), + signatureAlgorithm: + getCachedValue( + this, kSlotSignatureAlgorithm, nativeSignatureAlgorithm), + signatureAlgorithmOid: + getCachedValue( + this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid), + }, opts)}`; + } - [kClone]() { - const handle = this[kHandle]; - return { - data: { handle }, - deserializeInfo: 'internal/crypto/x509:InternalX509Certificate', - }; - } + get subject() { + return getCachedValue(this, kSlotSubject, nativeSubject); + } - [kDeserialize]({ handle }) { - this[kHandle] = handle; - } + get subjectAltName() { + return getCachedValue( + this, kSlotSubjectAltName, nativeSubjectAltName); + } - get subject() { - let value = this[kInternalState].get('subject'); - if (value === undefined) { - value = this[kHandle].subject(); - this[kInternalState].set('subject', value); + get issuer() { + return getCachedValue(this, kSlotIssuer, nativeIssuer); } - return value; - } - get subjectAltName() { - let value = this[kInternalState].get('subjectAltName'); - if (value === undefined) { - value = this[kHandle].subjectAltName(); - this[kInternalState].set('subjectAltName', value); + get issuerCertificate() { + const state = getState(this); + const bit = 1 << kSlotIssuerCertificate; + if ((state[kStateCacheMask] & bit) === 0) { + const cert = FunctionPrototypeCall(nativeGetIssuerCert, this); + state[kSlotIssuerCertificate] = cert ? + new InternalX509Certificate(cert) : undefined; + state[kStateCacheMask] |= bit; + } + return state[kSlotIssuerCertificate]; } - return value; - } - get issuer() { - let value = this[kInternalState].get('issuer'); - if (value === undefined) { - value = this[kHandle].issuer(); - this[kInternalState].set('issuer', value); + get infoAccess() { + return getCachedValue(this, kSlotInfoAccess, nativeInfoAccess); } - return value; - } - get issuerCertificate() { - let value = this[kInternalState].get('issuerCertificate'); - if (value === undefined) { - const cert = this[kHandle].getIssuerCert(); - if (cert) - value = new InternalX509Certificate(cert); - this[kInternalState].set('issuerCertificate', value); + get validFrom() { + return getCachedValue(this, kSlotValidFrom, nativeValidFrom); } - return value; - } - get infoAccess() { - let value = this[kInternalState].get('infoAccess'); - if (value === undefined) { - value = this[kHandle].infoAccess(); - this[kInternalState].set('infoAccess', value); + get validTo() { + return getCachedValue(this, kSlotValidTo, nativeValidTo); } - return value; - } - get validFrom() { - let value = this[kInternalState].get('validFrom'); - if (value === undefined) { - value = this[kHandle].validFrom(); - this[kInternalState].set('validFrom', value); + get validFromDate() { + return getCachedValue(this, kSlotValidFromDate, nativeValidFromDate); } - return value; - } - get validTo() { - let value = this[kInternalState].get('validTo'); - if (value === undefined) { - value = this[kHandle].validTo(); - this[kInternalState].set('validTo', value); + get validToDate() { + return getCachedValue(this, kSlotValidToDate, nativeValidToDate); } - return value; - } - get validFromDate() { - let value = this[kInternalState].get('validFromDate'); - if (value === undefined) { - value = this[kHandle].validFromDate(); - this[kInternalState].set('validFromDate', value); + get fingerprint() { + return getCachedValue(this, kSlotFingerprint, nativeFingerprint); } - return value; - } - get validToDate() { - let value = this[kInternalState].get('validToDate'); - if (value === undefined) { - value = this[kHandle].validToDate(); - this[kInternalState].set('validToDate', value); + get fingerprint256() { + return getCachedValue(this, kSlotFingerprint256, nativeFingerprint256); } - return value; - } - get fingerprint() { - let value = this[kInternalState].get('fingerprint'); - if (value === undefined) { - value = this[kHandle].fingerprint(); - this[kInternalState].set('fingerprint', value); + get fingerprint512() { + return getCachedValue(this, kSlotFingerprint512, nativeFingerprint512); } - return value; - } - get fingerprint256() { - let value = this[kInternalState].get('fingerprint256'); - if (value === undefined) { - value = this[kHandle].fingerprint256(); - this[kInternalState].set('fingerprint256', value); + get keyUsage() { + return getCachedValue(this, kSlotKeyUsage, nativeKeyUsage); } - return value; - } - get fingerprint512() { - let value = this[kInternalState].get('fingerprint512'); - if (value === undefined) { - value = this[kHandle].fingerprint512(); - this[kInternalState].set('fingerprint512', value); + get serialNumber() { + return getCachedValue(this, kSlotSerialNumber, nativeSerialNumber); } - return value; - } - get keyUsage() { - let value = this[kInternalState].get('keyUsage'); - if (value === undefined) { - value = this[kHandle].keyUsage(); - this[kInternalState].set('keyUsage', value); + get signatureAlgorithm() { + return getCachedValue( + this, kSlotSignatureAlgorithm, nativeSignatureAlgorithm); } - return value; - } - get serialNumber() { - let value = this[kInternalState].get('serialNumber'); - if (value === undefined) { - value = this[kHandle].serialNumber(); - this[kInternalState].set('serialNumber', value); + get signatureAlgorithmOid() { + return getCachedValue( + this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid); } - return value; - } - get signatureAlgorithm() { - let value = this[kInternalState].get('signatureAlgorithm'); - if (value === undefined) { - value = this[kHandle].signatureAlgorithm(); - this[kInternalState].set('signatureAlgorithm', value); + get raw() { + return getCachedValue(this, kSlotRaw, nativeRaw); } - return value; - } - get signatureAlgorithmOid() { - let value = this[kInternalState].get('signatureAlgorithmOid'); - if (value === undefined) { - value = this[kHandle].signatureAlgorithmOid(); - this[kInternalState].set('signatureAlgorithmOid', value); + get publicKey() { + const state = getState(this); + const bit = 1 << kSlotPublicKey; + if ((state[kStateCacheMask] & bit) === 0) { + state[kSlotPublicKey] = new PublicKeyObject( + FunctionPrototypeCall(nativePublicKey, this)); + state[kStateCacheMask] |= bit; + } + return state[kSlotPublicKey]; } - return value; - } - get raw() { - let value = this[kInternalState].get('raw'); - if (value === undefined) { - value = this[kHandle].raw(); - this[kInternalState].set('raw', value); + toString() { + return getCachedValue(this, kSlotPem, nativePem); } - return value; - } - get publicKey() { - let value = this[kInternalState].get('publicKey'); - if (value === undefined) { - value = new PublicKeyObject(this[kHandle].publicKey()); - this[kInternalState].set('publicKey', value); + // There's no standardized JSON encoding for X509 certs so we + // fallback to providing the PEM encoding as a string. + toJSON() { return getCachedValue(this, kSlotPem, nativePem); } + + get ca() { + return getCachedValue(this, kSlotCA, nativeCheckCA); } - return value; - } - toString() { - let value = this[kInternalState].get('pem'); - if (value === undefined) { - value = this[kHandle].pem(); - this[kInternalState].set('pem', value); + checkHost(name, options) { + getState(this); + validateString(name, 'name'); + return FunctionPrototypeCall( + nativeCheckHost, this, name, getFlags(options)); } - return value; - } - // There's no standardized JSON encoding for X509 certs so we - // fallback to providing the PEM encoding as a string. - toJSON() { return this.toString(); } + checkEmail(email, options) { + getState(this); + validateString(email, 'email'); + return FunctionPrototypeCall( + nativeCheckEmail, this, email, getFlags(options)); + } - get ca() { - let value = this[kInternalState].get('ca'); - if (value === undefined) { - value = this[kHandle].checkCA(); - this[kInternalState].set('ca', value); + checkIP(ip, options) { + getState(this); + validateString(ip, 'ip'); + // The options argument is currently undocumented since none of the + // options have any effect on the behavior of this function. However, we + // still parse the options argument in case OpenSSL adds flags in the + // future that do affect the behavior of X509_check_ip. This ensures that + // no invalid values are passed as the second argument in the meantime. + return FunctionPrototypeCall( + nativeCheckIP, this, ip, getFlags(options)); } - return value; - } - checkHost(name, options) { - validateString(name, 'name'); - return this[kHandle].checkHost(name, getFlags(options)); - } + checkIssued(otherCert) { + getState(this); + if (!isX509Certificate(otherCert)) { + throw new ERR_INVALID_ARG_TYPE( + 'otherCert', 'X509Certificate', otherCert); + } + return FunctionPrototypeCall(nativeCheckIssued, this, otherCert); + } - checkEmail(email, options) { - validateString(email, 'email'); - return this[kHandle].checkEmail(email, getFlags(options)); - } + checkPrivateKey(pkey) { + getState(this); + if (!isKeyObject(pkey)) + throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); + if (getKeyObjectType(pkey) !== 'private') + throw new ERR_INVALID_ARG_VALUE('pkey', pkey); + return FunctionPrototypeCall( + nativeCheckPrivateKey, this, getKeyObjectHandle(pkey)); + } - checkIP(ip, options) { - validateString(ip, 'ip'); - // The options argument is currently undocumented since none of the options - // have any effect on the behavior of this function. However, we still parse - // the options argument in case OpenSSL adds flags in the future that do - // affect the behavior of X509_check_ip. This ensures that no invalid values - // are passed as the second argument in the meantime. - return this[kHandle].checkIP(ip, getFlags(options)); - } + verify(pkey) { + getState(this); + if (!isKeyObject(pkey)) + throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); + if (getKeyObjectType(pkey) !== 'public') + throw new ERR_INVALID_ARG_VALUE('pkey', pkey); + return FunctionPrototypeCall( + nativeVerify, this, getKeyObjectHandle(pkey)); + } - checkIssued(otherCert) { - if (!isX509Certificate(otherCert)) - throw new ERR_INVALID_ARG_TYPE('otherCert', 'X509Certificate', otherCert); - return this[kHandle].checkIssued(otherCert[kHandle]); + toLegacyObject() { + getState(this); + // TODO(tniessen): do not depend on translatePeerCertificate here, return + // the correct legacy representation from the binding + lazyTranslatePeerCertificate ??= + require('internal/tls/common').translatePeerCertificate; + return lazyTranslatePeerCertificate( + FunctionPrototypeCall(nativeToLegacy, this)); + } } - checkPrivateKey(pkey) { - if (!isKeyObject(pkey)) - throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); - if (getKeyObjectType(pkey) !== 'private') - throw new ERR_INVALID_ARG_VALUE('pkey', pkey); - return this[kHandle].checkPrivateKey(getKeyObjectHandle(pkey)); - } + InternalX509Certificate = class InternalX509Certificate + extends NativeX509Certificate { + #state = createState(); + + static { + isX509Certificate = (value) => { + if (value == null || typeof value !== 'object') return false; + return #state in value || isNativeX509Certificate(value); + }; + + getState = (cert) => { + try { + return cert.#state; + } catch { + // Continue with the cross-realm native brand check. + } + if (!isNativeX509Certificate(cert)) { + throw new ERR_INVALID_THIS('X509Certificate'); + } + let state = crossRealmState.get(cert); + if (state === undefined) { + state = createState(); + crossRealmState.set(cert, state); + } + return state; + }; + } + }; - verify(pkey) { - if (!isKeyObject(pkey)) - throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); - if (getKeyObjectType(pkey) !== 'public') - throw new ERR_INVALID_ARG_VALUE('pkey', pkey); - return this[kHandle].verify(getKeyObjectHandle(pkey)); - } + InternalX509Certificate.prototype.constructor = X509Certificate; + ObjectSetPrototypeOf( + InternalX509Certificate.prototype, + X509Certificate.prototype); - toLegacyObject() { - // TODO(tniessen): do not depend on translatePeerCertificate here, return - // the correct legacy representation from the binding - lazyTranslatePeerCertificate ??= - require('internal/tls/common').translatePeerCertificate; - return lazyTranslatePeerCertificate(this[kHandle].toLegacy()); - } -} + return [X509Certificate, InternalX509Certificate]; +}); -InternalX509Certificate.prototype.constructor = X509Certificate; -ObjectSetPrototypeOf( - InternalX509Certificate.prototype, - X509Certificate.prototype); +// Keep the binding-returned constructor and the closure reference aligned. +InternalX509Certificate = InternalX509CertificateConstructor; module.exports = { X509Certificate, diff --git a/src/crypto/README.md b/src/crypto/README.md index ad06cf989276..4bfec21359e0 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -50,6 +50,7 @@ following table: | `crypto_spkac` | Netscape SPKAC certificate utilities. | | `crypto_ssl` | Implementation of the `SSLWrap` object. | | `crypto_timing` | Implementation of the TimingSafeEqual. | +| `crypto_x509` | X.509 certificate parsing and validation. | When new crypto protocols are added, they will be added into their own `crypto_` `*.h` and `*.cc` files. @@ -171,11 +172,12 @@ JavaScript needs access to those operations and is kept out of user-visible A `KeyObject` is the public Node.js-specific API for keys. It extends a native `NativeKeyObject`, which stores `KeyObjectData` for structured -cloning. The JavaScript API surface reads its key type and a -`KeyObjectHandle` through a hidden native-backed slot tuple, caching that -tuple in a private field outside user-visible own properties. Derived -metadata, such as symmetric key size and asymmetric key details, is read -from the cached handle and appended lazily to the same private-field cache. +cloning. The JavaScript constructor caches the known key type in a private +field outside user-visible own properties. When a `KeyObjectHandle` is first +needed, JavaScript replaces that value with a hidden native-backed slot tuple. +Derived metadata, such as symmetric key size and asymmetric key details, is +read from the cached handle and appended lazily to the same private-field +cache. #### `CryptoKey` @@ -183,7 +185,18 @@ A `CryptoKey` is the Web Crypto API key type. In the Node.js implementation, public `CryptoKey` instances are backed by a native `NativeCryptoKey`, not by a `KeyObject`. `NativeCryptoKey` stores the same `KeyObjectData` representation as `KeyObject`, plus the Web Crypto internal slots -(`[[extractable]]`, `[[algorithm]]`, and `[[usages]]`). +(`[[extractable]]`, `[[algorithm]]`, and `[[usages]]`). Normal construction +primes a private JavaScript slot cache from the constructor arguments. +Partially initialized transferred keys populate that cache from the native +slots on first access. + +### X.509 certificates + +The public `X509Certificate` is backed directly by the native +`X509Certificate` object. JavaScript caches derived certificate properties in +a private array whose final entry is a bitmask of populated slots. Certificates +from another realm use the same cache layout through a private `WeakMap` after +passing the native brand check. ### `CryptoJob` diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc index 6ed7d7d25db0..05336f5e70d2 100644 --- a/src/crypto/crypto_x509.cc +++ b/src/crypto/crypto_x509.cc @@ -807,7 +807,7 @@ Local X509Certificate::GetConstructorTemplate( Local tmpl = env->x509_constructor_template(); if (tmpl.IsEmpty()) { Isolate* isolate = env->isolate(); - tmpl = NewFunctionTemplate(isolate, nullptr); + tmpl = NewFunctionTemplate(isolate, NewFromHandle); tmpl->InstanceTemplate()->SetInternalFieldCount( X509Certificate::kInternalFieldCount); tmpl->SetClassName( @@ -850,8 +850,72 @@ Local X509Certificate::GetConstructorTemplate( return tmpl; } -bool X509Certificate::HasInstance(Environment* env, Local object) { - return GetConstructorTemplate(env)->HasInstance(object); +bool X509Certificate::HasInstance(Environment* env, Local value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +void X509Certificate::NewFromHandle(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (args.Length() == 1 && args[0]->IsArrayBufferView()) { + ArrayBufferViewContents buf(args[0].As()); + auto result = X509Pointer::Parse(ncrypto::Buffer{ + .data = buf.data(), + .len = buf.length(), + }); + if (!result.value) [[unlikely]] { + return ThrowCryptoError(env, result.error.value_or(0)); + } + new X509Certificate(env, + args.This(), + std::make_shared(std::move(result.value))); + return; + } + + if (args.Length() != 1 || !HasInstance(env, args[0])) { + THROW_ERR_INVALID_ARG_TYPE( + env, "value must be an X509CertificateHandle or BufferSource"); + return; + } + + X509Certificate* handle = Unwrap(args[0].As()); + CHECK_NOT_NULL(handle); + Local issuer; + if (handle->issuer_cert_) issuer = handle->issuer_cert_->object(); + new X509Certificate(env, args.This(), handle->cert_, issuer); +} + +void X509Certificate::CreateX509CertificateClass( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsFunction()); + + Local ctor; + if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor)) + return; + + Local ret; + if (!args[0] + .As() + ->Call(env->context(), Undefined(env->isolate()), 1, &ctor) + .ToLocal(&ret)) { + return; + } + + Local constructors = ret.As(); + Local internal_ctor; + if (!constructors->Get(env->context(), 1).ToLocal(&internal_ctor)) return; + CHECK(env->crypto_internal_x509_certificate_constructor().IsEmpty()); + env->set_crypto_internal_x509_certificate_constructor( + internal_ctor.As()); + args.GetReturnValue().Set(constructors); +} + +void X509Certificate::IsX509Certificate( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + args.GetReturnValue().Set(HasInstance(env, args[0])); } MaybeLocal X509Certificate::New(Environment* env, @@ -865,13 +929,13 @@ MaybeLocal X509Certificate::New(Environment* env, std::shared_ptr cert, STACK_OF(X509) * issuer_chain) { EscapableHandleScope scope(env->isolate()); - Local ctor; - if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor)) - return MaybeLocal(); - Local obj; - if (!ctor->NewInstance(env->context()).ToLocal(&obj)) + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { return MaybeLocal(); + } Local issuer_chain_obj; if (issuer_chain != nullptr && sk_X509_num(issuer_chain)) { @@ -963,11 +1027,31 @@ X509Certificate::X509CertificateTransferData::Deserialize( if (!X509Certificate::New(env, data_).ToLocal(&handle)) return {}; - return BaseObjectPtr( - Unwrap(handle.As())); + Local module = + FIXED_ONE_BYTE_STRING(env->isolate(), "internal/crypto/x509"); + if (env->builtin_module_require() + ->Call(context, Null(env->isolate()), 1, &module) + .IsEmpty()) { + return {}; + } + + Local ctor = env->crypto_internal_x509_certificate_constructor(); + CHECK(!ctor.IsEmpty()); + Local cert; + if (!ctor->NewInstance(context, 1, &handle).ToLocal(&cert)) return {}; + + return BaseObjectPtr(Unwrap(cert.As())); } BaseObject::TransferMode X509Certificate::GetTransferMode() const { + Local transfer_mode = + object() + ->GetPrivate(env()->context(), env()->transfer_mode_private_symbol()) + .ToLocalChecked(); + if (transfer_mode->IsUint32() && + (transfer_mode.As()->Value() & TransferMode::kCloneable) == 0) { + return BaseObject::TransferMode::kDisallowCloneAndTransfer; + } return BaseObject::TransferMode::kCloneable; } @@ -977,6 +1061,12 @@ std::unique_ptr X509Certificate::CloneForMessaging() } void X509Certificate::Initialize(Environment* env, Local target) { + SetMethod(env->context(), + target, + "createX509CertificateClass", + CreateX509CertificateClass); + SetMethodNoSideEffect( + env->context(), target, "isX509Certificate", IsX509Certificate); SetMethod(env->context(), target, "parseX509", Parse); NODE_DEFINE_CONSTANT(target, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT); @@ -989,6 +1079,9 @@ void X509Certificate::Initialize(Environment* env, Local target) { void X509Certificate::RegisterExternalReferences( ExternalReferenceRegistry* registry) { + registry->Register(CreateX509CertificateClass); + registry->Register(IsX509Certificate); + registry->Register(NewFromHandle); registry->Register(Parse); registry->Register(Subject); registry->Register(SubjectAltName); diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h index c94e832a2845..dda2ed05ce65 100644 --- a/src/crypto/crypto_x509.h +++ b/src/crypto/crypto_x509.h @@ -49,7 +49,13 @@ class X509Certificate final : public BaseObject { static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static v8::Local GetConstructorTemplate( Environment* env); - static bool HasInstance(Environment* env, v8::Local object); + static bool HasInstance(Environment* env, v8::Local value); + + static void CreateX509CertificateClass( + const v8::FunctionCallbackInfo& args); + static void IsX509Certificate( + const v8::FunctionCallbackInfo& args); + static void NewFromHandle(const v8::FunctionCallbackInfo& args); static v8::MaybeLocal New( Environment* env, diff --git a/src/env_properties.h b/src/env_properties.h index eb26d3b6cf05..886d4adba9fc 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -507,6 +507,7 @@ V(async_hooks_promise_resolve_function, v8::Function) \ V(buffer_prototype_object, v8::Object) \ V(crypto_internal_cryptokey_constructor, v8::Function) \ + V(crypto_internal_x509_certificate_constructor, v8::Function) \ V(crypto_key_object_private_constructor, v8::Function) \ V(crypto_key_object_public_constructor, v8::Function) \ V(crypto_key_object_secret_constructor, v8::Function) \ diff --git a/test/parallel/test-crypto-keyobject-clone-transfer.js b/test/parallel/test-crypto-keyobject-clone-transfer.js index 6786df288589..2ac37ca201e7 100644 --- a/test/parallel/test-crypto-keyobject-clone-transfer.js +++ b/test/parallel/test-crypto-keyobject-clone-transfer.js @@ -53,23 +53,25 @@ async function roundTripViaMessageChannel(key) { return received; } -async function roundTripViaWorker(key) { - const worker = new Worker(` - 'use strict'; - const { parentPort } = require('node:worker_threads'); - const { types: { isKeyObject } } = require('node:util'); - - parentPort.once('message', ({ key, expectedType }) => { - try { - if (!isKeyObject(key) || key.type !== expectedType) { - throw new Error('KeyObject slot mismatch in worker'); - } - parentPort.postMessage({ key }); - } catch (err) { - parentPort.postMessage({ error: err.stack || err.message }); +function workerMain() { + const { parentPort } = require('node:worker_threads'); + const { types: { isKeyObject: workerIsKeyObject } } = require('node:util'); + + parentPort.once('message', ({ key, expectedType }) => { + try { + if (!workerIsKeyObject(key) || key.type !== expectedType) { + throw new Error('KeyObject slot mismatch in worker'); } - }); - `, { eval: true }); + parentPort.postMessage({ key }); + } catch (err) { + parentPort.postMessage({ error: err.stack || err.message }); + } + }); +} + +async function roundTripViaWorker(key) { + const worker = new Worker( + `'use strict';(${workerMain.toString()})()`, { eval: true }); worker.postMessage({ key, expectedType: key.type }); const [msg] = await once(worker, 'message'); diff --git a/test/parallel/test-crypto-x509-brand-check.js b/test/parallel/test-crypto-x509-brand-check.js new file mode 100644 index 000000000000..a0bd64ed8037 --- /dev/null +++ b/test/parallel/test-crypto-x509-brand-check.js @@ -0,0 +1,145 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { X509Certificate } = require('node:crypto'); +const { readFileSync } = require('node:fs'); +const fixtures = require('../common/fixtures'); +const { + isX509Certificate, +} = require('internal/crypto/x509'); + +const certData = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const cert = new X509Certificate(certData); +const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + +const getterNames = [ + 'subject', + 'subjectAltName', + 'issuer', + 'issuerCertificate', + 'infoAccess', + 'validFrom', + 'validTo', + 'validFromDate', + 'validToDate', + 'fingerprint', + 'fingerprint256', + 'fingerprint512', + 'keyUsage', + 'serialNumber', + 'signatureAlgorithm', + 'signatureAlgorithmOid', + 'raw', + 'publicKey', + 'ca', +]; + +const methodNames = [ + 'toString', + 'toJSON', + 'checkHost', + 'checkEmail', + 'checkIP', + 'checkIssued', + 'checkPrivateKey', + 'verify', + 'toLegacyObject', +]; + +assert.strictEqual(isX509Certificate(cert), true); +assert.strictEqual(cert instanceof X509Certificate, true); +assert.strictEqual(Object.getPrototypeOf(X509Certificate.prototype), + Object.prototype); +for (const name of ['pem', 'checkCA', 'getIssuerCert', 'toLegacy']) { + assert.strictEqual(name in cert, false); +} + +for (const value of [ + {}, + { __proto__: null }, + 1, + null, + undefined, + Buffer.alloc(1), + function() {}, +]) { + assert.strictEqual(isX509Certificate(value), false); + for (const name of getterNames) { + const getter = Object.getOwnPropertyDescriptor( + X509Certificate.prototype, name).get; + assert.throws(() => getter.call(value), invalidThis); + } + for (const name of methodNames) { + assert.throws( + () => X509Certificate.prototype[name].call(value), + invalidThis); + } +} + +const spoofed = { __proto__: X509Certificate.prototype }; +assert.strictEqual(spoofed instanceof X509Certificate, true); +assert.strictEqual(isX509Certificate(spoofed), false); +assert.throws(() => spoofed.subject, invalidThis); +assert.throws(() => spoofed.toString(), invalidThis); + +const originalHasInstance = + Object.getOwnPropertyDescriptor(X509Certificate, Symbol.hasInstance); +Object.defineProperty(X509Certificate, Symbol.hasInstance, { + configurable: true, + value: () => true, +}); +try { + const buffer = Buffer.alloc(1); + assert.strictEqual(buffer instanceof X509Certificate, true); + assert.strictEqual(isX509Certificate(buffer), false); + assert.throws(() => X509Certificate.prototype.toString.call(buffer), + invalidThis); +} finally { + if (originalHasInstance === undefined) { + delete X509Certificate[Symbol.hasInstance]; + } else { + Object.defineProperty( + X509Certificate, Symbol.hasInstance, originalHasInstance); + } +} + +class DerivedX509Certificate extends X509Certificate {} +const derived = new DerivedX509Certificate(certData); +assert.strictEqual(derived instanceof DerivedX509Certificate, true); +assert.strictEqual(derived instanceof X509Certificate, true); +assert.strictEqual(isX509Certificate(derived), true); +assert.strictEqual(derived.subject, cert.subject); + +function PrimitivePrototype() {} +PrimitivePrototype.prototype = 1; +const primitivePrototype = Reflect.construct( + X509Certificate, [certData], PrimitivePrototype); +assert.strictEqual(Object.getPrototypeOf(primitivePrototype), Object.prototype); +assert.strictEqual(isX509Certificate(primitivePrototype), true); +assert.strictEqual( + Object.getOwnPropertyDescriptor( + X509Certificate.prototype, 'subject').get.call(primitivePrototype), + cert.subject); + +const firstPrototype = { __proto__: X509Certificate.prototype }; +const secondPrototype = { __proto__: X509Certificate.prototype }; +let prototypeReads = 0; +const ProxyNewTarget = new Proxy(function() {}, { + get(target, property, receiver) { + if (property === 'prototype') { + prototypeReads++; + return prototypeReads === 1 ? firstPrototype : secondPrototype; + } + return Reflect.get(target, property, receiver); + }, +}); +const proxyNewTarget = Reflect.construct( + X509Certificate, [certData], ProxyNewTarget); +assert.strictEqual(prototypeReads, 1); +assert.strictEqual(Object.getPrototypeOf(proxyNewTarget), firstPrototype); +assert.strictEqual(proxyNewTarget.subject, cert.subject); diff --git a/test/parallel/test-crypto-x509-clone-transfer.js b/test/parallel/test-crypto-x509-clone-transfer.js new file mode 100644 index 000000000000..4ab6ea59b5f2 --- /dev/null +++ b/test/parallel/test-crypto-x509-clone-transfer.js @@ -0,0 +1,119 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { once } = require('node:events'); +const { + X509Certificate, + createPrivateKey, +} = require('node:crypto'); +const { readFileSync } = require('node:fs'); +const { + markAsUncloneable, + MessageChannel, + Worker, +} = require('node:worker_threads'); +const fixtures = require('../common/fixtures'); +const { + isX509Certificate, +} = require('internal/crypto/x509'); + +const certData = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const keyData = readFileSync(fixtures.path('keys', 'agent1-key.pem')); +const caData = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); + +const cert = new X509Certificate(certData); +const ca = new X509Certificate(caData); +const privateKey = createPrivateKey(keyData); + +const dataCloneError = { + code: 25, + name: 'DataCloneError', +}; + +{ + const uncloneable = new X509Certificate(certData); + markAsUncloneable(uncloneable); + assert.throws(() => structuredClone(uncloneable), dataCloneError); + + const { port1, port2 } = new MessageChannel(); + assert.throws(() => port1.postMessage(uncloneable), dataCloneError); + port1.close(); + port2.close(); +} + +function assertSameCertificate(original, clone) { + assert.notStrictEqual(clone, original); + assert.strictEqual(clone instanceof X509Certificate, true); + assert.strictEqual(isX509Certificate(clone), true); + assert.strictEqual(clone.subject, original.subject); + assert.strictEqual(clone.issuer, original.issuer); + assert.strictEqual(clone.fingerprint256, original.fingerprint256); + assert.deepStrictEqual(clone.raw, original.raw); + assert.deepStrictEqual(Reflect.ownKeys(clone), []); + assert.strictEqual(clone.checkPrivateKey(privateKey), true); + assert.strictEqual(clone.checkIssued(ca), true); + assert.strictEqual(clone.verify(ca.publicKey), true); +} + +async function roundTripViaMessageChannel(value) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(value); + const [received] = await once(port2, 'message'); + port1.close(); + port2.close(); + return received; +} + +function workerMain() { + const { + X509Certificate: WorkerX509Certificate, + } = require('node:crypto'); + const { parentPort } = require('node:worker_threads'); + + parentPort.once('message', (cert) => { + try { + if (!(cert instanceof WorkerX509Certificate)) { + throw new Error('X509Certificate brand was not preserved'); + } + parentPort.postMessage({ + cert, + ownKeyCount: Reflect.ownKeys(cert).length, + subject: cert.subject, + }); + } catch (error) { + parentPort.postMessage({ error: error.stack || error.message }); + } + }); +} + +async function roundTripViaWorker(value) { + const worker = new Worker( + `'use strict';(${workerMain.toString()})()`, { eval: true }); + + worker.postMessage(value); + const [message] = await once(worker, 'message'); + await worker.terminate(); + assert.strictEqual(message.error, undefined, message.error); + assert.strictEqual(message.ownKeyCount, 0); + assert.strictEqual(message.subject, value.subject); + return message.cert; +} + +(async () => { + const cloned = structuredClone(cert); + assertSameCertificate(cert, cloned); + + const viaPort = await roundTripViaMessageChannel(cert); + assertSameCertificate(cert, viaPort); + + const clonedAgain = structuredClone(viaPort); + assertSameCertificate(cert, clonedAgain); + + const viaWorker = await roundTripViaWorker(cert); + assertSameCertificate(cert, viaWorker); +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-x509-hidden-slots.js b/test/parallel/test-crypto-x509-hidden-slots.js new file mode 100644 index 000000000000..fead69935287 --- /dev/null +++ b/test/parallel/test-crypto-x509-hidden-slots.js @@ -0,0 +1,117 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { + X509Certificate, + createPrivateKey, +} = require('node:crypto'); +const { readFileSync } = require('node:fs'); +const { inspect } = require('node:util'); +const fixtures = require('../common/fixtures'); + +const certData = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const keyData = readFileSync(fixtures.path('keys', 'agent1-key.pem')); +const caData = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); + +const cert = new X509Certificate(certData); +const ca = new X509Certificate(caData); +const privateKey = createPrivateKey(keyData); +const expectedPem = cert.toString(); +const expectedSubject = cert.subject; + +function assertNoOwnKeys(value) { + assert.deepStrictEqual(Object.getOwnPropertyNames(value), []); + assert.deepStrictEqual(Object.getOwnPropertySymbols(value), []); + assert.deepStrictEqual(Reflect.ownKeys(value), []); +} + +for (const name of [ + 'subject', + 'subjectAltName', + 'issuer', + 'issuerCertificate', + 'infoAccess', + 'validFrom', + 'validTo', + 'validFromDate', + 'validToDate', + 'fingerprint', + 'fingerprint256', + 'fingerprint512', + 'keyUsage', + 'serialNumber', + 'signatureAlgorithm', + 'signatureAlgorithmOid', + 'raw', + 'publicKey', + 'ca', +]) { + // Populate every private lazy-cache entry before checking reflection. + Reflect.get(cert, name); +} +cert.toString(); +cert.toJSON(); +assertNoOwnKeys(cert); + +const getterNames = [ + 'subject', + 'subjectAltName', + 'issuer', + 'infoAccess', + 'validFrom', + 'validTo', + 'validFromDate', + 'validToDate', + 'fingerprint', + 'fingerprint256', + 'fingerprint512', + 'keyUsage', + 'serialNumber', + 'signatureAlgorithm', + 'signatureAlgorithmOid', +]; +const originals = new Map(); +for (const name of getterNames) { + originals.set(name, + Object.getOwnPropertyDescriptor( + X509Certificate.prototype, name)); + Object.defineProperty(X509Certificate.prototype, name, { + configurable: true, + get() { return `FORGED-${name}`; }, + }); +} +const originalToString = + Object.getOwnPropertyDescriptor(X509Certificate.prototype, 'toString'); +Object.defineProperty(X509Certificate.prototype, 'toString', { + configurable: true, + value() { return 'FORGED-PEM'; }, +}); + +try { + assert.strictEqual(cert.subject, 'FORGED-subject'); + assert.strictEqual(cert.toString(), 'FORGED-PEM'); + + const rendered = inspect(cert, { depth: 4 }); + assert.match(rendered, /CN=agent1/); + assert.doesNotMatch(rendered, /FORGED/); + assert.strictEqual(cert.toJSON(), expectedPem); + assert.strictEqual(JSON.parse(JSON.stringify(cert)), expectedPem); + + assert.strictEqual(cert.checkIssued(ca), true); + assert.strictEqual(cert.checkPrivateKey(privateKey), true); + assert.strictEqual(cert.verify(ca.publicKey), true); + assertNoOwnKeys(cert); +} finally { + for (const [name, descriptor] of originals) { + Object.defineProperty(X509Certificate.prototype, name, descriptor); + } + Object.defineProperty( + X509Certificate.prototype, 'toString', originalToString); +} + +assert.strictEqual(cert.subject, expectedSubject); +assert.strictEqual(cert.toString(), expectedPem); diff --git a/test/parallel/test-eslint-no-crypto-class-instanceof.js b/test/parallel/test-eslint-no-crypto-class-instanceof.js new file mode 100644 index 000000000000..379b133b3818 --- /dev/null +++ b/test/parallel/test-eslint-no-crypto-class-instanceof.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/no-crypto-class-instanceof'); + +new RuleTester().run('no-crypto-class-instanceof', rule, { + valid: [ + 'value instanceof Buffer;', + 'value instanceof KeyObject;', + ` + const { isKeyObject } = require('internal/crypto/keys'); + isKeyObject(key); + `, + ` + function isCryptoKey(value, CryptoKey) { + return value instanceof CryptoKey; + } + `, + ` + function isCryptoKey(value, globalThis) { + return value instanceof globalThis.CryptoKey; + } + `, + ], + invalid: [ + { + code: ` + const { KeyObject: KO } = require('internal/crypto/keys'); + key instanceof KO; + `, + errors: [{ messageId: 'noKeyObjectInstanceof' }], + }, + { + code: ` + const webcrypto = require('internal/crypto/webcrypto'); + key instanceof webcrypto.CryptoKey; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: 'key instanceof globalThis.CryptoKey;', + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: ` + const { X509Certificate } = require('internal/crypto/x509'); + cert instanceof X509Certificate; + `, + errors: [{ messageId: 'noX509CertificateInstanceof' }], + }, + ], +}); diff --git a/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js b/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js deleted file mode 100644 index 29b70c9ec58a..000000000000 --- a/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const common = require('../common'); -common.skipIfEslintMissing(); - -const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; -const rule = require('../../tools/eslint-rules/no-keyobject-cryptokey-instanceof'); - -new RuleTester().run('no-keyobject-cryptokey-instanceof', rule, { - valid: [ - 'key instanceof Buffer;', - 'key instanceof KeyObject;', - ` - const { isKeyObject } = require('internal/crypto/keys'); - isKeyObject(key); - `, - ` - const { isCryptoKey } = require('internal/crypto/keys'); - isCryptoKey(key); - `, - ], - invalid: [ - { - code: ` - const { KeyObject } = require('internal/crypto/keys'); - key instanceof KeyObject; - `, - errors: [{ messageId: 'noKeyObjectInstanceof' }], - }, - { - code: ` - const { KeyObject: KO } = require('internal/crypto/keys'); - key instanceof KO; - `, - errors: [{ messageId: 'noKeyObjectInstanceof' }], - }, - { - code: ` - const keys = require('internal/crypto/keys'); - key instanceof keys.KeyObject; - `, - errors: [{ messageId: 'noKeyObjectInstanceof' }], - }, - { - code: ` - key instanceof CryptoKey; - `, - errors: [{ messageId: 'noCryptoKeyInstanceof' }], - }, - { - code: ` - const { CryptoKey } = require('internal/crypto/keys'); - key instanceof CryptoKey; - `, - errors: [{ messageId: 'noCryptoKeyInstanceof' }], - }, - { - code: ` - const { CryptoKey: CK } = require('internal/crypto/webcrypto'); - key instanceof CK; - `, - errors: [{ messageId: 'noCryptoKeyInstanceof' }], - }, - { - code: ` - const webcrypto = require('internal/crypto/webcrypto'); - key instanceof webcrypto.CryptoKey; - `, - errors: [{ messageId: 'noCryptoKeyInstanceof' }], - }, - { - code: ` - key instanceof globalThis.CryptoKey; - `, - errors: [{ messageId: 'noCryptoKeyInstanceof' }], - }, - ], -}); diff --git a/tools/eslint-rules/no-crypto-class-instanceof.js b/tools/eslint-rules/no-crypto-class-instanceof.js new file mode 100644 index 000000000000..e54bfc0299cc --- /dev/null +++ b/tools/eslint-rules/no-crypto-class-instanceof.js @@ -0,0 +1,179 @@ +/** + * @file Prevent internal code from brand-checking crypto classes with + * instanceof. + */ +'use strict'; + +const { isRequireCall, isString } = require('./rules-utils.js'); + +const moduleClasses = new Map([ + ['internal/crypto/keys', new Set([ + 'KeyObject', + 'SecretKeyObject', + 'AsymmetricKeyObject', + 'PublicKeyObject', + 'PrivateKeyObject', + 'CryptoKey', + 'InternalCryptoKey', + ])], + ['internal/crypto/webcrypto', new Set(['CryptoKey'])], + ['internal/crypto/x509', new Set([ + 'X509Certificate', + 'InternalX509Certificate', + ])], +]); + +const knownClassNames = new Set(); +for (const classes of moduleClasses.values()) { + for (const name of classes) knownClassNames.add(name); +} + +function getRequiredModule(node) { + if (node?.type !== 'CallExpression' || + !isRequireCall(node) || + !isString(node.arguments[0])) { + return undefined; + } + return node.arguments[0].value; +} + +function getPropertyName(node) { + if (!node) return undefined; + if (node.computed) { + return node.property.type === 'Literal' ? node.property.value : undefined; + } + return node.property.name; +} + +module.exports = { + meta: { + messages: { + noCryptoClassInstanceof: + 'Do not use `instanceof` to brand-check {{name}}; use its native or private brand.', + noCryptoKeyInstanceof: + 'Use `isCryptoKey(value)` instead of `value instanceof CryptoKey`.', + noKeyObjectInstanceof: + 'Use `isKeyObject(value)` instead of `value instanceof KeyObject`.', + noX509CertificateInstanceof: + 'Use `isX509Certificate(value)` instead of `value instanceof X509Certificate`.', + }, + schema: [], + }, + + create(context) { + const sourceCode = context.sourceCode; + const constructorNames = new Map(); + const namespaceClasses = new Map(); + + function registerVariable(node, name, map, value) { + const variables = sourceCode.scopeManager.getDeclaredVariables(node); + for (const variable of variables) { + if (variable.name === name) map.set(variable, value); + } + } + + function resolveVariable(node) { + let scope = sourceCode.getScope(node); + while (scope !== null) { + const variable = scope.set.get(node.name); + if (variable !== undefined) return variable; + scope = scope.upper; + } + return undefined; + } + + function isGlobalReference(node) { + const variable = resolveVariable(node); + return variable === undefined || variable.defs.length === 0; + } + + function registerRequire(node) { + const module = getRequiredModule(node.init); + const classes = moduleClasses.get(module); + if (classes === undefined) return; + + if (node.id.type === 'Identifier') { + registerVariable(node, node.id.name, namespaceClasses, classes); + return; + } + + if (node.id.type !== 'ObjectPattern') return; + for (const property of node.id.properties) { + if (property.type !== 'Property' || + property.value.type !== 'Identifier') { + continue; + } + const importedName = property.key.name ?? property.key.value; + if (classes.has(importedName)) { + registerVariable( + node, property.value.name, constructorNames, importedName); + } + } + } + + function declarationName(variable) { + if (!knownClassNames.has(variable.name)) return undefined; + for (const definition of variable.defs) { + if (definition.type === 'ClassName' || + definition.type === 'FunctionName') { + return variable.name; + } + } + return undefined; + } + + function constructorName(node) { + if (node.type === 'Identifier') { + const variable = resolveVariable(node); + if (variable !== undefined) { + const name = constructorNames.get(variable) ?? + declarationName(variable); + if (name !== undefined) return name; + } + if (node.name === 'CryptoKey' && isGlobalReference(node)) { + return 'CryptoKey'; + } + return undefined; + } + if (node.type !== 'MemberExpression') return undefined; + + const property = getPropertyName(node); + if (node.object.type !== 'Identifier') return undefined; + if (node.object.name === 'globalThis' && + property === 'CryptoKey' && + isGlobalReference(node.object)) { + return 'CryptoKey'; + } + + const variable = resolveVariable(node.object); + const classes = namespaceClasses.get(variable); + return classes?.has(property) ? property : undefined; + } + + return { + VariableDeclarator: registerRequire, + + BinaryExpression(node) { + if (node.operator !== 'instanceof') return; + + const name = constructorName(node.right); + if (name === undefined) return; + + let messageId = 'noCryptoClassInstanceof'; + if (name === 'CryptoKey' || name === 'InternalCryptoKey') { + messageId = 'noCryptoKeyInstanceof'; + } else if (name.endsWith('KeyObject')) { + messageId = 'noKeyObjectInstanceof'; + } else if (name.endsWith('X509Certificate')) { + messageId = 'noX509CertificateInstanceof'; + } + + context.report({ + node, + messageId, + data: { name }, + }); + }, + }; + }, +}; diff --git a/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js b/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js deleted file mode 100644 index 19e13437247a..000000000000 --- a/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @file Prevent internal code from brand-checking keys with instanceof. - */ -'use strict'; - -const { isRequireCall, isString } = require('./rules-utils.js'); - -const CRYPTO_KEYS_MODULE = 'internal/crypto/keys'; -const WEBCRYPTO_MODULE = 'internal/crypto/webcrypto'; - -const keyObjectClassNames = new Set([ - 'KeyObject', - 'SecretKeyObject', - 'AsymmetricKeyObject', - 'PublicKeyObject', - 'PrivateKeyObject', -]); - -const cryptoKeyClassNames = new Set([ - 'CryptoKey', - 'InternalCryptoKey', -]); - -function isKeyModuleRequire(node) { - return node?.type === 'CallExpression' && - isRequireCall(node) && - isString(node.arguments[0]) && - (node.arguments[0].value === CRYPTO_KEYS_MODULE || - node.arguments[0].value === WEBCRYPTO_MODULE); -} - -function getPropertyName(node) { - if (!node) return undefined; - if (node.computed) { - return node.property.type === 'Literal' ? node.property.value : undefined; - } - return node.property.name; -} - -module.exports = { - meta: { - messages: { - noKeyObjectInstanceof: 'Use `isKeyObject(value)` instead of `value instanceof KeyObject`.', - noCryptoKeyInstanceof: 'Use `isCryptoKey(value)` instead of `value instanceof CryptoKey`.', - }, - schema: [], - }, - - create(context) { - const namespaceNames = new Set(); - const keyObjectConstructorNames = new Set(); - const cryptoKeyConstructorNames = new Set(['CryptoKey']); - - function registerRequire(node) { - if (!isKeyModuleRequire(node.init)) return; - - if (node.id.type === 'Identifier') { - namespaceNames.add(node.id.name); - return; - } - - if (node.id.type !== 'ObjectPattern') return; - - for (const property of node.id.properties) { - if (property.type !== 'Property') continue; - const keyName = property.key.name ?? property.key.value; - if (property.value.type !== 'Identifier') continue; - const localName = property.value.name; - if (keyObjectClassNames.has(keyName)) { - keyObjectConstructorNames.add(localName); - } else if (cryptoKeyClassNames.has(keyName)) { - cryptoKeyConstructorNames.add(localName); - } - } - } - - function constructorKind(node) { - if (node.type === 'Identifier') { - if (keyObjectConstructorNames.has(node.name)) return 'KeyObject'; - if (cryptoKeyConstructorNames.has(node.name)) return 'CryptoKey'; - return undefined; - } - - if (node.type !== 'MemberExpression') return undefined; - - const property = getPropertyName(node); - if (node.object.type === 'Identifier') { - if (namespaceNames.has(node.object.name)) { - if (keyObjectClassNames.has(property)) return 'KeyObject'; - if (cryptoKeyClassNames.has(property)) return 'CryptoKey'; - } - if (node.object.name === 'globalThis' && - cryptoKeyClassNames.has(property)) { - return 'CryptoKey'; - } - } - - return undefined; - } - - return { - VariableDeclarator: registerRequire, - - BinaryExpression(node) { - if (node.operator !== 'instanceof') return; - - const kind = constructorKind(node.right); - if (kind === 'KeyObject') { - context.report({ - node, - messageId: 'noKeyObjectInstanceof', - }); - } else if (kind === 'CryptoKey') { - context.report({ - node, - messageId: 'noCryptoKeyInstanceof', - }); - } - }, - }; - }, -}; diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 58303fe3c0f3..62faf4dc6121 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -751,6 +751,27 @@ declare namespace InternalCryptoBinding { toLegacy(): object; } + interface NativeX509CertificateConstructor { + readonly prototype: X509CertificateHandle; + new(value: X509CertificateHandle | ArrayBufferView): X509CertificateHandle; + } + + interface X509CertificateConstructor { + readonly prototype: object; + new(buffer: string | ArrayBufferView): object; + } + + interface InternalX509CertificateConstructor { + readonly prototype: object; + new(value: X509CertificateHandle | ArrayBufferView): object; + } + + type CreateX509CertificateClassCallback = + (NativeX509Certificate: NativeX509CertificateConstructor) => [ + X509Certificate: X509CertificateConstructor, + InternalX509Certificate: InternalX509CertificateConstructor, + ]; + interface CipherInfo { name: string; nid: number; @@ -926,6 +947,12 @@ export interface CryptoBinding { PublicKeyObject: InternalCryptoBinding.KeyObjectSubtypeConstructor, PrivateKeyObject: InternalCryptoBinding.KeyObjectSubtypeConstructor, ]; + createX509CertificateClass( + callback: InternalCryptoBinding.CreateX509CertificateClassCallback, + ): [ + X509Certificate: InternalCryptoBinding.X509CertificateConstructor, + InternalX509Certificate: InternalCryptoBinding.InternalX509CertificateConstructor, + ]; getBundledRootCertificates(): string[]; getCachedAliases(): Record; getCertificateCompressionAlgorithms(): string[]; @@ -942,6 +969,7 @@ export interface CryptoBinding { getHashes(): string[]; isCryptoKey(key: unknown): boolean; isKeyObject(key: unknown): boolean; + isX509Certificate(value: unknown): boolean; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; getOpenSSLSecLevelCrypto(): number | undefined; getSSLCiphers(): string[];