Skip to content

fix(node_crypto): apply RFC 7748 scalar decoding for X448 - #36466

Merged
nathanwhit merged 1 commit into
denoland:mainfrom
nathanwhit:fix/node-x448-scalar-decoding
Aug 17, 2026
Merged

nathanwhit merged 1 commit into
denoland:mainfrom
nathanwhit:fix/node-x448-scalar-decoding

Conversation

@nathanwhit

Copy link
Copy Markdown
Member

Summary

  • share the RFC 7748 X448 operation between WebCrypto and Node crypto
  • apply X448 scalar decoding when deriving Node-compatible public keys and shared secrets
  • retain rejection of low-order public keys

Problem

The Node crypto paths converted 56-byte X448 private keys by reducing them modulo the Ed448 group order. RFC 7748 instead requires clearing the scalar's two least-significant bits and setting its most-significant bit before scalar multiplication.

As a result, imported X448 private keys could derive different public keys and shared secrets in Deno than in Node/OpenSSL. Both Node crypto paths now use the same RFC 7748 implementation as WebCrypto.

Tests

  • RFC 7748 public-key and shared-secret vectors
  • imported PKCS#8 key compatibility with Node/OpenSSL
  • generated peer agreement
  • low-order public-key rejection
  • focused Rust unit tests, Node crypto unit test, clippy, formatting, and TypeScript lint

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix and a good consolidation. The node_crypto paths were doing

EdwardsScalar::from_bytes_mod_order(&scalar_bytes.into())

which reduces the scalar modulo the Ed448 group order — but RFC 7748's decodeScalar448 clamps instead (clear the low two bits, set bit 447), and a clamped scalar deliberately exceeds the group order. Reducing it produces a different point, so an imported X448 key derived a different public key and a different shared secret in Deno than in Node or OpenSSL. That's a silent interop failure, and the worst kind: key agreement just doesn't agree.

The right call was noticing WebCrypto already had this correct (from #35155) and that the two implementations had diverged, then hoisting the correct one rather than copying the fix. libs/crypto is a slightly odd home for a pure-Rust curve implementation given the crate is otherwise the aws-lc wrapper, but it's the natural shared point between ext/crypto and ext/node_crypto and it pulls in no deno_core dependency, so no layering problem.

Checked the mechanical parts: BASE_POINT as [5, 0, ...] is u = 5 and is equivalent to the MontgomeryPoint::GENERATOR it replaces — the moved public_key_from_scalar test is what pins that. subtle becoming a direct dependency of ext/crypto is just replacing the re-export that came via ed448-goldilocks. Low-order public key rejection is preserved on both paths.

The unit_node test is the one that matters most: hand-building the PKCS#8/SPKI wrappers means it exercises the imported-key path, which is exactly where the bug lived, rather than only generated keys where both sides would have been consistently wrong.

CI has lint title and pre-build failing, but both at ~15m0s, which reads as job timeouts rather than real failures — worth a re-run to confirm before merging. LGTM.

@nathanwhit
nathanwhit merged commit f74b15c into denoland:main Aug 17, 2026
171 of 173 checks passed
bartlomieju pushed a commit that referenced this pull request Aug 27, 2026
## Summary

- share the RFC 7748 X448 operation between WebCrypto and Node crypto
- apply X448 scalar decoding when deriving Node-compatible public keys
and shared secrets
- retain rejection of low-order public keys

## Problem

The Node crypto paths converted 56-byte X448 private keys by reducing
them modulo the Ed448 group order. RFC 7748 instead requires clearing
the scalar's two least-significant bits and setting its most-significant
bit before scalar multiplication.

As a result, imported X448 private keys could derive different public
keys and shared secrets in Deno than in Node/OpenSSL. Both Node crypto
paths now use the same RFC 7748 implementation as WebCrypto.

## Tests

- RFC 7748 public-key and shared-secret vectors
- imported PKCS#8 key compatibility with Node/OpenSSL
- generated peer agreement
- low-order public-key rejection
- focused Rust unit tests, Node crypto unit test, clippy, formatting,
and TypeScript lint
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.

2 participants