Skip to content

feat: PQC JWK import/export for ML-DSA and ML-KEM#1016

Merged
boorad merged 3 commits into
mainfrom
feat/pqc-jwk-import-export
May 3, 2026
Merged

feat: PQC JWK import/export for ML-DSA and ML-KEM#1016
boorad merged 3 commits into
mainfrom
feat/pqc-jwk-import-export

Conversation

@boorad
Copy link
Copy Markdown
Collaborator

@boorad boorad commented May 3, 2026

Summary

Adds WebCrypto JWK support for ML-DSA-44/65/87 and ML-KEM-512/768/1024 using Node's kty: 'AKP' encoding. Public keys carry the raw public bytes in pub; private keys add the seed in priv. On private-key import the public key is re-derived from the seed and constant-time compared (CRYPTO_memcmp) against the supplied pub.

While here, hardened JWK import validation across all importers (kmac, rsa, hmac, aes, ec, ed, mldsa, mlkem) so the codebase aligns with Node's validateJwk / validateKeyOps and the WebCrypto spec.

Changes

  • PQC JWK (C++ + TS): export/import for ML-DSA and ML-KEM via kty: 'AKP' with pub/priv (seed). Guarded by OPENSSL_VERSION_NUMBER >= 0x30500000L. Constant-time pub/derived-pub comparison on private import.
  • Centralized validateJwkStructure (utils/validation.ts): one place for ext, use, key_ops array shape, key_ops duplicate detection, and key_ops-covers-usages. Mirrors Node's validateJwk + validateKeyOps.
  • Validation order fix: every JWK importer now validates JWK structure (kty/use/key_ops/ext/alg) before key-usage validation — matches Node and the spec. Previously usage checks ran first, which produced SyntaxError for cases where Node throws DataError.
  • DataError propagation: every handle.initJwk() call site is wrapped in try/catch so C++ runtime errors surface as DataError DOMExceptions instead of generic Errors.
  • Type guard in pqcIsPublicImport: a non-object passed at format === 'jwk' is no longer silently treated as a public key.
  • lazyDOMException fix: object-form domName was producing [object Object] in error messages — now correctly uses domName.name and appends the cause.
  • Restored workspace:* for the example app dep and stopped the release bumper from rewriting it.
  • Doc table: ML-KEM JWK column flipped from to ✅.

Testing

  • ML-DSA & ML-KEM JWK round-trip tests (export → import → sign/verify or encapsulate/decapsulate) for all 6 variants
  • Negative tests: wrong kty, alg mismatch
  • All existing JWK error-message expectations preserved (verified via test message snippets)
  • bun tsc clean across both packages
  • Pre-commit hook (lint-staged + format + tsc + bob build) passes

Closes #996.

boorad added 3 commits May 3, 2026 11:55
Adds WebCrypto JWK support for ML-DSA-44/65/87 and ML-KEM-512/768/1024
using Node's `kty: 'AKP'` encoding (RFC 9269 / W3C WebCrypto draft):
public keys carry the raw public bytes in `pub`, private keys add the
seed in `priv`. On private-key import the public key is re-derived
from the seed and constant-time compared against the supplied `pub`.

Closes part of the #995 Node.js parity audit.
The example app exists to exercise the local workspace copy of the
library — it should always pull from `workspace:*`, not the most
recent npm version. Drop `dependencies.react-native-quick-crypto`
from the @release-it/bumper output so releases no longer rewrite it,
and restore `workspace:*` in example/package.json. The example
version field is still kept in sync.
…ters

Address review feedback on PQC JWK import/export PR (#996):

- Centralize JWK structural validation in `validateJwkStructure`:
  - `ext`: existing
  - `use`: new — must match algorithm's expected use ('sig' or 'enc')
    when usages are non-empty (mirrors Node's `validateJwk`)
  - `key_ops`: new duplicate detection per Node's `validateKeyOps`
- Reorder validation in all 8 importers (kmac, rsa, hmac, aes, ec, ed,
  mldsa, mlkem) so JWK structure (kty/use/key_ops/ext/alg) is checked
  before key-usage validation — matches Node and the WebCrypto spec.
- Wrap every `handle.initJwk()` call site in try/catch so C++ errors
  surface as `DataError` DOMExceptions, not generic `Error`s.
- Add type guard in `pqcIsPublicImport` so a non-object passed at
  `format === 'jwk'` is not silently treated as a public key.
- Fix `lazyDOMException` to format the object-form domName correctly
  (was producing `[object Object]` in the message).
@boorad boorad self-assigned this May 3, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-native-quick-crypto Ready Ready Preview, Comment May 3, 2026 4:25pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

🤖 End-to-End Test Results - Android

Status: ✅ Passed
Platform: Android
Run: 25284472508

📸 Final Test Screenshot

Maestro Test Results - android

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

🤖 End-to-End Test Results - iOS

Status: ✅ Passed
Platform: iOS
Run: 25284472514

📸 Final Test Screenshot

Maestro Test Results - ios

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@boorad boorad merged commit e33f183 into main May 3, 2026
11 checks passed
@boorad boorad deleted the feat/pqc-jwk-import-export branch May 3, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

subtle: implement JWK import/export for ML-DSA and ML-KEM

1 participant