vercel-labs / vercel-labs/scriptc

--dynamic island crypto lacks X25519 / AES / HKDF / SHA-512, which blocks protocol libraries

Open
#36 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
4.9k
Forks
125
Avg merge
2h 14m
Merged PRs (30d)
95

Description

Summary

The island's node:crypto shim carries a hashing-and-random slice only. Packages that do real cryptography import and initialize fine, then throw on the first crypto call — and scriptc coverage --dynamic reports no blockers for such a program, so the failure only surfaces at runtime.

I hit this trying to compile a WhatsApp protocol client (Noise handshake, Signal protocol, AES-GCM transport). It is not a performance question — the handshake cannot start.

Environment

scriptc 0.0.17, Ubuntu 24.04 x86_64, clang 18.1.3, cmake 3.28.3, Node 22.22.2 as differential baseline. Package under test: zapo-js@1.6.2, zero runtime dependencies, Uint8Array throughout (no Buffer).

What works

Measured against Node, byte-identical output:

primitive result
SHA-256 ✅ identical
SHA-1 ✅ identical
MD5 ✅ identical
HMAC-SHA-256 ✅ identical

What throws

primitive error
SHA-512 Digest method not supported
HMAC-SHA-512 Digest method not supported
HKDF crypto.hkdfSync is not available in the scriptc island (the embedded runtime carries the hashing/random slice only)
AES-GCM crypto.createCipheriv is not available in the scriptc island
AES-CBC crypto.createCipheriv is not available in the scriptc island
X25519 ECDH crypto.createPrivateKey is not available in the scriptc island

From packages/runtime/src/scr_island.c, makeCrypto: getHashes() returns ["md5", "sha1", "sha256"]; getCiphers() and getCurves() both return []; 32 APIs are die() stubs, including createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, createSecretKey, diffieHellman, createECDH, sign, verify, hkdf, hkdfSync, scrypt, generateKeyPair.

crypto.subtle is assigned from globalThis.crypto, but the embedded engine does not expose a WebCrypto implementation that covers these, so it is not an alternative route.

Coverage reports no blockers

For a program importing the client:

statements analyzed   4
compile statically    0  (0%)
compile dynamically   4  (100%) (island sites)

embedded npm code imports Node builtins and unresolved specifiers:
  node:crypto           shimmed     (zapo-js)
  node:events           shimmed     (zapo-js)
  ... 11 more builtins ...

builds with --dynamic — no remaining blockers

It builds, the client object constructs correctly, output matches Node — and then it throws on the first createPrivateKey. Suggestion: shimmed currently covers both "fully implemented" and "present but stubbed". A separate marker for the latter would make coverage predictive of what actually runs.

The static surface is narrower still

Probing each API in a plain (non---dynamic) build:

expression static
createHash('sha256').update(x).digest('hex')
createHash('sha1').update(x).digest('hex')
randomBytes(n)
randomUUID()
createHash('sha512') / ('md5') ❌ SC2020
createHmac(...) ❌ SC2020
createCipheriv(...) ❌ SC2020
hkdfSync(...) ❌ SC2020
createPrivateKey ❌ SC2020
generateKeyPairSync('x25519') ❌ SC2020
timingSafeEqual(...) ❌ SC2020
webcrypto ❌ SC2020 — "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain"

So the static path has less crypto than the island (no HMAC, no MD5, no SHA-512), which is surprising given static is the default and preferred lane. Also note createHash only lowers as a complete chain — binding the intermediate (const h = createHash('sha256')) fails with 'Hash' is typed by @types/node but has no scriptc lowering yet.

Request

Rough priority order for making protocol clients viable:

  1. X25519 (createPrivateKey + diffieHellman, or WebCrypto deriveBits) — blocks the handshake outright
  2. AES-GCM via createCipheriv — blocks all post-handshake transport
  3. HKDF (hkdfSync)
  4. SHA-512 and HMAC-SHA-512
  5. Ed25519 sign / verify
  6. AES-CBC / AES-CTR

Items 1–3 alone would take this from "cannot connect" to "usable".

The payoff is real for this class of workload. Measured here on pure TS: ~2 ms startup vs Node's ~27 ms, and 2.1 MB RSS vs 47 MB for an equivalent HTTP server. For multi-tenant connection handling that difference is the whole point.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/runtime/src/scr_island.c and the makeCrypto implementation, then inspect how static crypto lowering and coverage --dynamic classify shimmed APIs. Verify the requested X25519, AES-GCM, and HKDF paths against the Node baseline, and ensure coverage distinguishes implemented APIs from present-but-stubbed ones.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, node.js, typescript
Domain
compilers, cryptography
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.