crypto: harden X509Certificate state

Construct X509Certificate instances directly on the existing native
X.509 wrapper and keep lazy cached values in private state. Use a
non-throwing native brand check while preserving the existing
structured-clone behavior.

Preserve derived-constructor prototype semantics without rereading
new.target.prototype.

Add coverage for receiver validation, hidden state, prototype edge
cases, subclassing, and structured cloning through workers and message
ports.

Replace the key-only instanceof lint rule with a module-aware crypto
class guard and extend it to X509Certificate. Resolve references by
lexical binding so shadowed constructor names remain valid.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65518
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Filip Skokan authored and Node.js GitHub Bot committed 2026-08-26 21:36:07 +00:00
1 parent 3541d5d00d
commit fe6a4cd9c5
14 files changed
+1059 -483

No files matched your search

+1 -1
View File
@@ -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',
+295 -271
View File
@@ -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();
const crossRealmState = new SafeWeakMap();
constructor(handle) {
markTransferMode(this, true, false);
this[kHandle] = handle;
}
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();
let InternalX509Certificate;
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 {
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;
}
markTransferMode(this, true, false);
this[kHandle] = parseX509(buffer);
return state[index];
}
[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)}`;
}
[kClone]() {
const handle = this[kHandle];
return {
data: { handle },
deserializeInfo: 'internal/crypto/x509:InternalX509Certificate',
};
}
[kDeserialize]({ handle }) {
this[kHandle] = handle;
}
get subject() {
let value = this[kInternalState].get('subject');
if (value === undefined) {
value = this[kHandle].subject();
this[kInternalState].set('subject', value);
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;
}
return value;
}
get subjectAltName() {
let value = this[kInternalState].get('subjectAltName');
if (value === undefined) {
value = this[kHandle].subjectAltName();
this[kInternalState].set('subjectAltName', value);
[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)}`;
}
return value;
}
get issuer() {
let value = this[kInternalState].get('issuer');
if (value === undefined) {
value = this[kHandle].issuer();
this[kInternalState].set('issuer', value);
get subject() {
return getCachedValue(this, kSlotSubject, nativeSubject);
}
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 subjectAltName() {
return getCachedValue(
this, kSlotSubjectAltName, nativeSubjectAltName);
}
return value;
}
get infoAccess() {
let value = this[kInternalState].get('infoAccess');
if (value === undefined) {
value = this[kHandle].infoAccess();
this[kInternalState].set('infoAccess', value);
get issuer() {
return getCachedValue(this, kSlotIssuer, nativeIssuer);
}
return value;
}
get validFrom() {
let value = this[kInternalState].get('validFrom');
if (value === undefined) {
value = this[kHandle].validFrom();
this[kInternalState].set('validFrom', 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 validTo() {
let value = this[kInternalState].get('validTo');
if (value === undefined) {
value = this[kHandle].validTo();
this[kInternalState].set('validTo', value);
get infoAccess() {
return getCachedValue(this, kSlotInfoAccess, nativeInfoAccess);
}
return value;
}
get validFromDate() {
let value = this[kInternalState].get('validFromDate');
if (value === undefined) {
value = this[kHandle].validFromDate();
this[kInternalState].set('validFromDate', value);
get validFrom() {
return getCachedValue(this, kSlotValidFrom, nativeValidFrom);
}
return value;
}
get validToDate() {
let value = this[kInternalState].get('validToDate');
if (value === undefined) {
value = this[kHandle].validToDate();
this[kInternalState].set('validToDate', value);
get validTo() {
return getCachedValue(this, kSlotValidTo, nativeValidTo);
}
return value;
}
get fingerprint() {
let value = this[kInternalState].get('fingerprint');
if (value === undefined) {
value = this[kHandle].fingerprint();
this[kInternalState].set('fingerprint', value);
get validFromDate() {
return getCachedValue(this, kSlotValidFromDate, nativeValidFromDate);
}
return value;
}
get fingerprint256() {
let value = this[kInternalState].get('fingerprint256');
if (value === undefined) {
value = this[kHandle].fingerprint256();
this[kInternalState].set('fingerprint256', value);
get validToDate() {
return getCachedValue(this, kSlotValidToDate, nativeValidToDate);
}
return value;
}
get fingerprint512() {
let value = this[kInternalState].get('fingerprint512');
if (value === undefined) {
value = this[kHandle].fingerprint512();
this[kInternalState].set('fingerprint512', value);
get fingerprint() {
return getCachedValue(this, kSlotFingerprint, nativeFingerprint);
}
return value;
}
get keyUsage() {
let value = this[kInternalState].get('keyUsage');
if (value === undefined) {
value = this[kHandle].keyUsage();
this[kInternalState].set('keyUsage', value);
get fingerprint256() {
return getCachedValue(this, kSlotFingerprint256, nativeFingerprint256);
}
return value;
}
get serialNumber() {
let value = this[kInternalState].get('serialNumber');
if (value === undefined) {
value = this[kHandle].serialNumber();
this[kInternalState].set('serialNumber', value);
get fingerprint512() {
return getCachedValue(this, kSlotFingerprint512, nativeFingerprint512);
}
return value;
}
get signatureAlgorithm() {
let value = this[kInternalState].get('signatureAlgorithm');
if (value === undefined) {
value = this[kHandle].signatureAlgorithm();
this[kInternalState].set('signatureAlgorithm', value);
get keyUsage() {
return getCachedValue(this, kSlotKeyUsage, nativeKeyUsage);
}
return value;
}
get signatureAlgorithmOid() {
let value = this[kInternalState].get('signatureAlgorithmOid');
if (value === undefined) {
value = this[kHandle].signatureAlgorithmOid();
this[kInternalState].set('signatureAlgorithmOid', value);
get serialNumber() {
return getCachedValue(this, kSlotSerialNumber, nativeSerialNumber);
}
return value;
}
get raw() {
let value = this[kInternalState].get('raw');
if (value === undefined) {
value = this[kHandle].raw();
this[kInternalState].set('raw', value);
get signatureAlgorithm() {
return getCachedValue(
this, kSlotSignatureAlgorithm, nativeSignatureAlgorithm);
}
return value;
}
get publicKey() {
let value = this[kInternalState].get('publicKey');
if (value === undefined) {
value = new PublicKeyObject(this[kHandle].publicKey());
this[kInternalState].set('publicKey', value);
get signatureAlgorithmOid() {
return getCachedValue(
this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid);
}
return value;
}
toString() {
let value = this[kInternalState].get('pem');
if (value === undefined) {
value = this[kHandle].pem();
this[kInternalState].set('pem', value);
get raw() {
return getCachedValue(this, kSlotRaw, nativeRaw);
}
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(); }
get ca() {
let value = this[kInternalState].get('ca');
if (value === undefined) {
value = this[kHandle].checkCA();
this[kInternalState].set('ca', 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];
}
toString() {
return getCachedValue(this, kSlotPem, nativePem);
}
// 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);
}
checkHost(name, options) {
getState(this);
validateString(name, 'name');
return FunctionPrototypeCall(
nativeCheckHost, this, name, getFlags(options));
}
checkEmail(email, options) {
getState(this);
validateString(email, 'email');
return FunctionPrototypeCall(
nativeCheckEmail, this, email, getFlags(options));
}
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));
}
checkIssued(otherCert) {
getState(this);
if (!isX509Certificate(otherCert)) {
throw new ERR_INVALID_ARG_TYPE(
'otherCert', 'X509Certificate', otherCert);
}
return FunctionPrototypeCall(nativeCheckIssued, this, otherCert);
}
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));
}
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));
}
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));
}
return value;
}
checkHost(name, options) {
validateString(name, 'name');
return this[kHandle].checkHost(name, getFlags(options));
}
InternalX509Certificate = class InternalX509Certificate
extends NativeX509Certificate {
#state = createState();
checkEmail(email, options) {
validateString(email, 'email');
return this[kHandle].checkEmail(email, getFlags(options));
}
static {
isX509Certificate = (value) => {
if (value == null || typeof value !== 'object') return false;
return #state in value || isNativeX509Certificate(value);
};
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));
}
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;
};
}
};
checkIssued(otherCert) {
if (!isX509Certificate(otherCert))
throw new ERR_INVALID_ARG_TYPE('otherCert', 'X509Certificate', otherCert);
return this[kHandle].checkIssued(otherCert[kHandle]);
}
InternalX509Certificate.prototype.constructor = X509Certificate;
ObjectSetPrototypeOf(
InternalX509Certificate.prototype,
X509Certificate.prototype);
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));
}
return [X509Certificate, InternalX509Certificate];
});
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));
}
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());
}
}
InternalX509Certificate.prototype.constructor = X509Certificate;
ObjectSetPrototypeOf(
InternalX509Certificate.prototype,
X509Certificate.prototype);
// Keep the binding-returned constructor and the closure reference aligned.
InternalX509Certificate = InternalX509CertificateConstructor;
module.exports = {
X509Certificate,
+9
View File
@@ -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.
@@ -189,6 +190,14 @@ 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`
All operations that are not either Stream-based or single-use functions
+103 -10
View File
@@ -807,7 +807,7 @@ Local<FunctionTemplate> X509Certificate::GetConstructorTemplate(
Local<FunctionTemplate> 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<FunctionTemplate> X509Certificate::GetConstructorTemplate(
return tmpl;
}
bool X509Certificate::HasInstance(Environment* env, Local<Object> object) {
return GetConstructorTemplate(env)->HasInstance(object);
bool X509Certificate::HasInstance(Environment* env, Local<Value> value) {
return GetConstructorTemplate(env)->HasInstance(value);
}
void X509Certificate::NewFromHandle(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (args.Length() == 1 && args[0]->IsArrayBufferView()) {
ArrayBufferViewContents<unsigned char> buf(args[0].As<ArrayBufferView>());
auto result = X509Pointer::Parse(ncrypto::Buffer<const unsigned char>{
.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<ManagedX509>(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<X509Certificate>(args[0].As<Object>());
CHECK_NOT_NULL(handle);
Local<Object> issuer;
if (handle->issuer_cert_) issuer = handle->issuer_cert_->object();
new X509Certificate(env, args.This(), handle->cert_, issuer);
}
void X509Certificate::CreateX509CertificateClass(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsFunction());
Local<Value> ctor;
if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor))
return;
Local<Value> ret;
if (!args[0]
.As<Function>()
->Call(env->context(), Undefined(env->isolate()), 1, &ctor)
.ToLocal(&ret)) {
return;
}
Local<Array> constructors = ret.As<Array>();
Local<Value> 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<Function>());
args.GetReturnValue().Set(constructors);
}
void X509Certificate::IsX509Certificate(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 1);
args.GetReturnValue().Set(HasInstance(env, args[0]));
}
MaybeLocal<Object> X509Certificate::New(Environment* env,
@@ -865,13 +929,13 @@ MaybeLocal<Object> X509Certificate::New(Environment* env,
std::shared_ptr<ManagedX509> cert,
STACK_OF(X509) * issuer_chain) {
EscapableHandleScope scope(env->isolate());
Local<Function> ctor;
if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor))
return MaybeLocal<Object>();
Local<Object> obj;
if (!ctor->NewInstance(env->context()).ToLocal(&obj))
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return MaybeLocal<Object>();
}
Local<Object> 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<BaseObject>(
Unwrap<X509Certificate>(handle.As<Object>()));
Local<Value> 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<Function> ctor = env->crypto_internal_x509_certificate_constructor();
CHECK(!ctor.IsEmpty());
Local<Value> cert;
if (!ctor->NewInstance(context, 1, &handle).ToLocal(&cert)) return {};
return BaseObjectPtr<BaseObject>(Unwrap<X509Certificate>(cert.As<Object>()));
}
BaseObject::TransferMode X509Certificate::GetTransferMode() const {
Local<Value> transfer_mode =
object()
->GetPrivate(env()->context(), env()->transfer_mode_private_symbol())
.ToLocalChecked();
if (transfer_mode->IsUint32() &&
(transfer_mode.As<Uint32>()->Value() & TransferMode::kCloneable) == 0) {
return BaseObject::TransferMode::kDisallowCloneAndTransfer;
}
return BaseObject::TransferMode::kCloneable;
}
@@ -977,6 +1061,12 @@ std::unique_ptr<worker::TransferData> X509Certificate::CloneForMessaging()
}
void X509Certificate::Initialize(Environment* env, Local<Object> 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<Object> target) {
void X509Certificate::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(CreateX509CertificateClass);
registry->Register(IsX509Certificate);
registry->Register(NewFromHandle);
registry->Register(Parse);
registry->Register(Subject);
registry->Register(SubjectAltName);
+7 -1
View File
@@ -49,7 +49,13 @@ class X509Certificate final : public BaseObject {
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static bool HasInstance(Environment* env, v8::Local<v8::Object> object);
static bool HasInstance(Environment* env, v8::Local<v8::Value> value);
static void CreateX509CertificateClass(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsX509Certificate(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void NewFromHandle(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Object> New(
Environment* env,
+1
View File
@@ -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) \
@@ -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);
@@ -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());
@@ -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);
@@ -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' }],
},
],
});
@@ -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' }],
},
],
});
@@ -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 },
});
},
};
},
};
@@ -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',
});
}
},
};
},
};
+28
View File
@@ -755,6 +755,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;
@@ -934,6 +955,12 @@ export interface CryptoBinding {
PublicKeyObject: InternalCryptoBinding.KeyObjectSubtypeConstructor,
PrivateKeyObject: InternalCryptoBinding.KeyObjectSubtypeConstructor,
];
createX509CertificateClass(
callback: InternalCryptoBinding.CreateX509CertificateClassCallback,
): [
X509Certificate: InternalCryptoBinding.X509CertificateConstructor,
InternalX509Certificate: InternalCryptoBinding.InternalX509CertificateConstructor,
];
getBundledRootCertificates(): string[];
getCachedAliases(): Record<string, number>;
getCertificateCompressionAlgorithms(): string[];
@@ -951,6 +978,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[];