Skip to content

Commit 88173ca

Browse files
committed
feat: US-181 - Add jsonwebtoken project-matrix fixture
1 parent a7c64f1 commit 88173ca

9 files changed

Lines changed: 170 additions & 2 deletions

File tree

docs/nodejs-compatibility.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The [project-matrix test suite](https://github.com/rivet-dev/secure-exec/tree/ma
7878
| [pg](https://npmjs.com/package/pg) | Database | PostgreSQL client, Pool/Client classes, type parsers |
7979
| [drizzle-orm](https://npmjs.com/package/drizzle-orm) | Database | ORM schema definition, query building, ESM module graph |
8080
| [ws](https://npmjs.com/package/ws) | Networking | WebSocket client/server, HTTP upgrade, events |
81+
| [jsonwebtoken](https://npmjs.com/package/jsonwebtoken) | Crypto | JWT signing (HS256), verification, decode |
8182
| [zod](https://npmjs.com/package/zod) | Validation | Schema definition, parsing, safe parse, transforms |
8283
| [rivetkit](https://npmjs.com/package/rivetkit) | SDK | Local vendor package resolution |
8384
| crypto (builtin) | Crypto | `crypto.randomBytes`, `randomUUID`, `getRandomValues` |

packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@
341341
this._algorithm = algorithm;
342342
if (typeof key === 'string') {
343343
this._key = Buffer.from(key, 'utf8');
344+
} else if (key && typeof key === 'object' && key._pem !== undefined) {
345+
// SandboxKeyObject — extract underlying key material
346+
this._key = Buffer.from(key._pem, 'utf8');
344347
} else {
345348
this._key = Buffer.from(key);
346349
}
@@ -769,20 +772,37 @@
769772

770773
result.createPublicKey = function createPublicKey(key) {
771774
if (typeof key === 'string') {
775+
if (key.indexOf('-----BEGIN') === -1) {
776+
throw new TypeError('error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE');
777+
}
772778
return new SandboxKeyObject('public', key);
773779
}
774780
if (key && typeof key === 'object' && key._pem) {
775781
return new SandboxKeyObject('public', key._pem);
776782
}
783+
if (key && typeof key === 'object' && key.type === 'private') {
784+
// Node.js createPublicKey accepts private KeyObjects and extracts public key
785+
return new SandboxKeyObject('public', key._pem);
786+
}
777787
if (key && typeof key === 'object' && key.key) {
778788
var keyData = typeof key.key === 'string' ? key.key : key.key.toString('utf8');
779789
return new SandboxKeyObject('public', keyData);
780790
}
791+
if (Buffer.isBuffer(key)) {
792+
var keyStr = key.toString('utf8');
793+
if (keyStr.indexOf('-----BEGIN') === -1) {
794+
throw new TypeError('error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE');
795+
}
796+
return new SandboxKeyObject('public', keyStr);
797+
}
781798
return new SandboxKeyObject('public', String(key));
782799
};
783800

784801
result.createPrivateKey = function createPrivateKey(key) {
785802
if (typeof key === 'string') {
803+
if (key.indexOf('-----BEGIN') === -1) {
804+
throw new TypeError('error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE');
805+
}
786806
return new SandboxKeyObject('private', key);
787807
}
788808
if (key && typeof key === 'object' && key._pem) {
@@ -792,9 +812,26 @@
792812
var keyData = typeof key.key === 'string' ? key.key : key.key.toString('utf8');
793813
return new SandboxKeyObject('private', keyData);
794814
}
815+
if (Buffer.isBuffer(key)) {
816+
var keyStr = key.toString('utf8');
817+
if (keyStr.indexOf('-----BEGIN') === -1) {
818+
throw new TypeError('error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE');
819+
}
820+
return new SandboxKeyObject('private', keyStr);
821+
}
795822
return new SandboxKeyObject('private', String(key));
796823
};
797824

825+
result.createSecretKey = function createSecretKey(key) {
826+
if (typeof key === 'string') {
827+
return new SandboxKeyObject('secret', key);
828+
}
829+
if (Buffer.isBuffer(key) || (key instanceof Uint8Array)) {
830+
return new SandboxKeyObject('secret', Buffer.from(key).toString('utf8'));
831+
}
832+
return new SandboxKeyObject('secret', String(key));
833+
};
834+
798835
result.KeyObject = SandboxKeyObject;
799836
}
800837

packages/secure-exec-core/src/generated/isolate-runtime.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"entry": "src/index.js",
3+
"expectation": "pass"
4+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "project-matrix-jsonwebtoken-pass",
3+
"private": true,
4+
"type": "commonjs",
5+
"dependencies": {
6+
"jsonwebtoken": "9.0.2"
7+
}
8+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use strict";
2+
3+
const jwt = require("jsonwebtoken");
4+
5+
const secret = "test-secret-key-for-fixture";
6+
7+
// Sign a JWT with HS256 (default algorithm)
8+
const payload = { sub: "user-123", name: "Alice", admin: true };
9+
const token = jwt.sign(payload, secret, { algorithm: "HS256", noTimestamp: true });
10+
11+
// Verify the token
12+
const decoded = jwt.verify(token, secret);
13+
14+
// Decode without verification
15+
const unverified = jwt.decode(token, { complete: true });
16+
17+
// Verify with wrong secret fails
18+
let verifyError = null;
19+
try {
20+
jwt.verify(token, "wrong-secret");
21+
} catch (err) {
22+
verifyError = { name: err.name, message: err.message };
23+
}
24+
25+
const result = {
26+
token,
27+
decoded: { sub: decoded.sub, name: decoded.name, admin: decoded.admin },
28+
header: unverified.header,
29+
verifyError,
30+
};
31+
32+
console.log(JSON.stringify(result));

packages/secure-exec/tests/test-suite/node/crypto.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,73 @@ export function runNodeCryptoSuite(context: NodeSuiteContext): void {
863863
expect((result.exports as any).valid).toBe(false);
864864
});
865865

866+
it("createSecretKey produces KeyObject with type 'secret'", async () => {
867+
const runtime = await context.createRuntime();
868+
const result = await runtime.run(`
869+
const crypto = require('crypto');
870+
const key = crypto.createSecretKey(Buffer.from('my-secret'));
871+
module.exports = {
872+
type: key.type,
873+
hasExport: typeof key.export === 'function',
874+
};
875+
`);
876+
expect(result.code).toBe(0);
877+
expect(result.errorMessage).toBeUndefined();
878+
const exports = result.exports as any;
879+
expect(exports.type).toBe("secret");
880+
expect(exports.hasExport).toBe(true);
881+
});
882+
883+
it("createPrivateKey rejects non-PEM strings", async () => {
884+
const runtime = await context.createRuntime();
885+
const result = await runtime.run(`
886+
const crypto = require('crypto');
887+
let threw = false;
888+
try {
889+
crypto.createPrivateKey('not-a-pem-key');
890+
} catch (e) {
891+
threw = true;
892+
}
893+
module.exports = { threw };
894+
`);
895+
expect(result.code).toBe(0);
896+
expect(result.errorMessage).toBeUndefined();
897+
expect((result.exports as any).threw).toBe(true);
898+
});
899+
900+
it("createPublicKey rejects non-PEM strings", async () => {
901+
const runtime = await context.createRuntime();
902+
const result = await runtime.run(`
903+
const crypto = require('crypto');
904+
let threw = false;
905+
try {
906+
crypto.createPublicKey('not-a-pem-key');
907+
} catch (e) {
908+
threw = true;
909+
}
910+
module.exports = { threw };
911+
`);
912+
expect(result.code).toBe(0);
913+
expect(result.errorMessage).toBeUndefined();
914+
expect((result.exports as any).threw).toBe(true);
915+
});
916+
917+
it("HMAC with KeyObject secret produces correct digest", async () => {
918+
const runtime = await context.createRuntime();
919+
const result = await runtime.run(`
920+
const crypto = require('crypto');
921+
const key = crypto.createSecretKey(Buffer.from('hmac-key'));
922+
const hmac = crypto.createHmac('sha256', key);
923+
hmac.update('test data');
924+
const hex = hmac.digest('hex');
925+
module.exports = { hex, length: hex.length };
926+
`);
927+
expect(result.code).toBe(0);
928+
expect(result.errorMessage).toBeUndefined();
929+
const exports = result.exports as any;
930+
expect(exports.length).toBe(64);
931+
});
932+
866933
// crypto.subtle (Web Crypto API) tests
867934

868935
it("subtle.digest('SHA-256', data) matches createHash output", async () => {

progress.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ PRD: ralph/kernel-hardening (46 stories)
116116
- Sandbox Server class needs `setTimeout`, `keepAliveTimeout`, `requestTimeout` properties for framework compatibility — added as no-ops
117117
- Moving a module from Unsupported (Tier 5) to Deferred (Tier 4) requires changes in: module-resolver.ts, require-setup.ts, node-stdlib.md contract, and adding BUILTIN_NAMED_EXPORTS entry
118118
- `declare module` for untyped npm packages must live in a `.d.ts` file (not `.ts`) — TypeScript treats it as augmentation in `.ts` files and fails with TS2665
119+
- Sandbox createPrivateKey/createPublicKey must validate PEM format (throw for non-PEM strings) — libraries like jsonwebtoken rely on the throw to fall through to createSecretKey
120+
- Sandbox createSecretKey creates SandboxKeyObject with type='secret' — needed by libraries checking key.type for symmetric algorithm validation
121+
- SandboxHmac must handle SandboxKeyObject as key (check key._pem property) — libraries pass KeyObject directly to crypto.createHmac()
119122
- Host httpRequest adapter must use `http` or `https` transport based on URL protocol — always using `https` breaks localhost HTTP requests from sandbox
120123
- To test sandbox http.request() client behavior, create an external nodeHttp server in the test code and have the sandbox request to it
121124
- NodeExecutionDriver split into 5 modules in src/node/: isolate-bootstrap.ts (types+utilities), module-resolver.ts, esm-compiler.ts, bridge-setup.ts, execution-lifecycle.ts; facade is execution-driver.ts (<300 lines)
@@ -2265,3 +2268,19 @@ PRD: ralph/kernel-hardening (46 stories)
22652268
- Follow pg-pass/ssh2-pass pattern for packages needing external services: test module loading, API shape, and data-processing features without real connections
22662269
- ClientRequest in sandbox bridge lacks destroy() method — ws calls req.destroy() on failed upgrades, causing "stream.destroy is not a function" error
22672270
---
2271+
2272+
## 2026-03-18 - US-181
2273+
- What was implemented: Added jsonwebtoken project-matrix fixture and fixed sandbox crypto key validation
2274+
- Files changed:
2275+
- packages/secure-exec/tests/projects/jsonwebtoken-pass/package.json — new fixture package
2276+
- packages/secure-exec/tests/projects/jsonwebtoken-pass/fixture.json — fixture metadata
2277+
- packages/secure-exec/tests/projects/jsonwebtoken-pass/src/index.js — JWT sign/verify/decode test
2278+
- packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts — added createSecretKey, fixed createPrivateKey and createPublicKey to validate PEM format, updated SandboxHmac to handle SandboxKeyObject keys
2279+
- packages/secure-exec/tests/test-suite/node/crypto.ts — added 4 tests: createSecretKey, createPrivateKey/createPublicKey PEM validation, HMAC with KeyObject
2280+
- docs/nodejs-compatibility.mdx — added jsonwebtoken to Tested Packages table
2281+
- **Learnings for future iterations:**
2282+
- jsonwebtoken 9.0.2 uses createPrivateKey/createSecretKey/createPublicKey to normalize key material before signing — sandbox must implement all three
2283+
- Sandbox createPrivateKey/createPublicKey must validate PEM format (check for '-----BEGIN') and throw for non-PEM strings — otherwise libraries that try createPrivateKey first and fall back to createSecretKey in catch blocks never reach the fallback
2284+
- SandboxHmac must handle SandboxKeyObject as key (check key._pem) — jwa passes KeyObject directly to crypto.createHmac()
2285+
- createSecretKey creates a KeyObject with type='secret' — needed for HS256/HS384/HS512 algorithm validation in jsonwebtoken
2286+
---

scripts/ralph/prd.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3123,7 +3123,7 @@
31233123
"Tests pass (project-matrix)"
31243124
],
31253125
"priority": 181,
3126-
"passes": false,
3126+
"passes": true,
31273127
"notes": "Depends on crypto.createHmac (US-168). May need to be ordered after crypto stories."
31283128
},
31293129
{

0 commit comments

Comments
 (0)