Skip to content

Commit d618239

Browse files
committed
test: add CryptoKey class regression tests
Adds focused tests that check guarantees either introduced by the native `CryptoKey` refactor or carried over from the existing state. - `test-webcrypto-cryptokey-brand-check.js` - each of the four prototype getters (`type`, `extractable`, `algorithm`, `usages`) throws `ERR_INVALID_THIS` for foreign receivers (plain objects, null-proto, primitives, null/undefined, functions, a subverted `Symbol.hasInstance`, and a real `BaseObject` of a different kind). `util.types.isCryptoKey()` remains accurate after prototype spoofing, and spoofed objects are rejected by public SubtleCrypto methods too. - `test-webcrypto-cryptokey-clone-transfer.js` - exhaustive structured-clone, `MessagePort.postMessage`, and `Worker.postMessage` round-trips, including RSA-PSS public/private keys through a Worker. Verifies slot preservation, inspect-output equivalence, reflection cleanliness, and crypto operation interop across clones including repeated round-trips. - `test-webcrypto-cryptokey-hidden-slots.js` - replaces all four prototype getters with forged versions and confirms internal consumers (export, inspect, KeyObject.from(), and deprecated node:crypto CryptoKey consumers) still read the real native slots. - `test-webcrypto-cryptokey-no-own-symbols.js`, asserts CryptoKey instances expose no own symbol-keyed properties even after every public getter has been touched (proof the `#slots` private field plus native storage leaves the instance shape pristine). - RSA JWK import regression coverage rejects private keys missing any required CRT parameter (`p`, `q`, `dp`, `dq`, or `qi`). Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 7684ddb commit d618239

6 files changed

Lines changed: 711 additions & 0 deletions
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict';
2+
3+
// The four CryptoKey prototype getters (`type`, `extractable`,
4+
// `algorithm`, `usages`) are user-configurable per Web IDL, so they
5+
// can be invoked with an arbitrary `this`. The native callbacks that
6+
// implement them must brand-check their receiver and throw cleanly
7+
// (ERR_INVALID_THIS) rather than crashing the process or returning
8+
// garbage. This test exercises four progressively more hostile
9+
// receiver shapes, including subverting `instanceof` via
10+
// `Symbol.hasInstance`, to make sure the C++ brand check holds.
11+
//
12+
// It also verifies that `util.types.isCryptoKey()` cannot be fooled
13+
// by prototype spoofing.
14+
15+
const common = require('../common');
16+
if (!common.hasCrypto)
17+
common.skip('missing crypto');
18+
19+
const assert = require('node:assert');
20+
const { types: { isCryptoKey } } = require('node:util');
21+
const { subtle } = globalThis.crypto;
22+
23+
(async () => {
24+
const key = await subtle.generateKey(
25+
{ name: 'HMAC', hash: 'SHA-256' },
26+
true,
27+
['sign'],
28+
);
29+
30+
const CryptoKey = key.constructor;
31+
32+
// Capture the underlying prototype getters once, so that subsequent
33+
// tampering with `CryptoKey.prototype` cannot affect what we call.
34+
const getters = {
35+
type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get,
36+
extractable:
37+
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get,
38+
algorithm:
39+
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get,
40+
usages:
41+
Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get,
42+
};
43+
44+
// Sanity: each getter works on a real CryptoKey.
45+
Object.entries(getters).forEach(([name, getter]) => {
46+
assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`);
47+
});
48+
assert.strictEqual(isCryptoKey(key), true);
49+
50+
const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' };
51+
52+
// Plain object receiver.
53+
Object.entries(getters).forEach(([, getter]) => {
54+
assert.throws(() => getter.call({}), invalidThis);
55+
});
56+
57+
// Null-prototype object receiver.
58+
Object.entries(getters).forEach(([, getter]) => {
59+
assert.throws(() => getter.call({ __proto__: null }), invalidThis);
60+
});
61+
62+
// Primitive receiver.
63+
Object.entries(getters).forEach(([, getter]) => {
64+
assert.throws(() => getter.call(1), invalidThis);
65+
});
66+
67+
// Null.
68+
Object.entries(getters).forEach(([, getter]) => {
69+
// eslint-disable-next-line no-useless-call
70+
assert.throws(() => getter.call(null), invalidThis);
71+
});
72+
73+
// Undefined.
74+
Object.entries(getters).forEach(([, getter]) => {
75+
assert.throws(() => getter.call(), invalidThis);
76+
});
77+
78+
// Function
79+
Object.entries(getters).forEach(([, getter]) => {
80+
assert.throws(() => getter.call(function() {}), invalidThis);
81+
});
82+
83+
// Prototype spoofing with InternalCryptoKey.prototype must not pass
84+
// util.types.isCryptoKey().
85+
const spoofed = {};
86+
Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key));
87+
assert.strictEqual(spoofed instanceof CryptoKey, true);
88+
assert.strictEqual(isCryptoKey(spoofed), false);
89+
await assert.rejects(
90+
subtle.sign('HMAC', spoofed, Buffer.from('payload')),
91+
invalidThis);
92+
await assert.rejects(
93+
subtle.exportKey('jwk', spoofed),
94+
invalidThis);
95+
96+
// Subvert `instanceof CryptoKey` via Symbol.hasInstance, then
97+
// invoke the native getters on a forged object. The C++ tag
98+
// check must reject the receiver even though `instanceof`
99+
// reports true.
100+
Object.defineProperty(CryptoKey, Symbol.hasInstance, {
101+
configurable: true,
102+
value: () => true,
103+
});
104+
const fake = { foo: 'bar' };
105+
assert.strictEqual(fake instanceof CryptoKey, true);
106+
assert.strictEqual(isCryptoKey(fake), false);
107+
Object.entries(getters).forEach(([, getter]) => {
108+
assert.throws(() => getter.call(fake), invalidThis);
109+
});
110+
111+
// Subverted `instanceof` plus a real BaseObject of a different
112+
// kind (a Buffer) as the receiver. Without the C++ tag check
113+
// this would type-confuse `Unwrap<NativeCryptoKey>`.
114+
const buf = Buffer.alloc(16);
115+
assert.strictEqual(buf instanceof CryptoKey, true);
116+
assert.strictEqual(isCryptoKey(buf), false);
117+
Object.entries(getters).forEach(([, getter]) => {
118+
assert.throws(() => getter.call(buf), invalidThis);
119+
});
120+
121+
// The real CryptoKey continues to work after all of the above.
122+
assert.strictEqual(getters.type.call(key), 'secret');
123+
assert.strictEqual(getters.extractable.call(key), true);
124+
assert.strictEqual(getters.algorithm.call(key).name, 'HMAC');
125+
assert.deepStrictEqual(getters.usages.call(key), ['sign']);
126+
})().then(common.mustCall());

0 commit comments

Comments
 (0)