Skip to content

llrt offers rind - #472

Closed
kixelated wants to merge 1 commit into
mainfrom
quest/m2/adoption/llrt
Closed

kixelated wants to merge 1 commit into
mainfrom
quest/m2/adoption/llrt

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Claims quest/m2/adoption/llrt.md.

Summary

llrt gains crypto-rind and tls-rind on kixelated/llrt crypto-rind (fork of awslabs/llrt at b7ccc148, v0.10.0-beta), built against rind 0.2.1 and rind-rustls 0.23.0 from crates.io. Nothing lands in rind and nothing was posted to awslabs; the upstream issue and PR are drafted below for the maintainer to post.

Two commits:

  • refactor(crypto): providers hash the message they sign or verify: llrt's CryptoProvider took a precomputed digest for ECDSA and RSA. rind signs and verifies only digests it computed (no import_less_safe, moq-dev/rind#379), so the trait now takes the message plus HashAlgorithm, and the existing rust/ring/graviola/openssl providers hash first through a shared hash() helper. No behavior change for them.
  • feat: add rind crypto and TLS backends: provider/rind.rs implements the whole trait on rind; tls-rind wires rind_rustls::default_provider() through llrt_tls, llrt_http, llrt_fetch, llrt_test_tls and llrt_core; README, docs/install.md, build.yml release mapping, build-modules.yml, and ci.yml matrix entries mirror graviola's (Linux x64/arm64, macOS arm64, Windows gnu).

Fork CI run: kixelated/llrt#1 (draft, fork-internal, not for merge).

Findings

  • The plan's "on rind alone" holds for the CryptoProvider trait: every method calls rind. Outside the trait, crypto-rind still builds what every llrt backend builds: _modern-webcrypto (ChaCha20-Poly1305, SHA-3, cSHAKE from RustCrypto, called directly by llrt's subtle code, not through the provider) and _subtle-full (der/pkcs8/spki encoding, plus ed25519-dalek and x25519-dalek for llrt's own JWK checks). pkcs1 is added for RSA key encoding only. With pqc, ML-KEM, ML-DSA and the hybrid KEMs stay on RustCrypto as on every other backend.
  • rind refuses what llrt's other backends accept: RSA key generation below 2048 or above 8192 bits or with an exponent other than 65537, and ECDSA with SHA-1. The provider maps these to OperationError/NotSupportedError, and subtle.supports() reports them as unsupported. llrt's tests generated 1024-bit RSA keys; they now use 2048, which every backend generates.
  • llrt's graviola release build is crypto-graviola-rust: graviola for hashing, HMAC and AES-GCM, RustCrypto for everything else. So against it rind replaces RustCrypto's ECDSA, ECDH, Ed25519, X25519, RSA, AES-CBC/CTR/KW and PBKDF2.

Validation

  • cargo clippy --all-targets -- -D warnings clean for lambda,macro,no-sdk,uncompressed with crypto-rind,tls-rind (with and without pqc) and crypto-rust,tls-ring; llrt_crypto clean with crypto-openssl; cargo check -p llrt_crypto clean for crypto-rust, crypto-ring, crypto-ring-rust, crypto-graviola, crypto-graviola-rust. cargo fmt --check clean; prettier applied to changed lines only (the touched test files and docs/install.md were already off prettier upstream).
  • cargo test -p llrt_crypto --features crypto-rind: 24 passed.
  • JS unit suite (llrt test -d bundle/js/__tests__/unit, x86_64 i7-8700): crypto-rind,tls-rind and the crypto-rust,tls-ring baseline fail the identical 60 tests (Temporal/Intl without intl402, fetch needing network, buffer and date environment cases, and ML-KEM/ML-DSA without pqc). With pqc added, the rind build passes every crypto test (1066 passed, 52 environment failures).
  • Not run locally: Windows (llrt uses the x86_64-pc-windows-gnu toolchain; rind's CI covers MSVC only) and arm64. The fork CI run covers both.

Benchmarks

One pass of webcrypto-bench.mjs (the WebCrypto timing script below, not committed to llrt) on desktop, Intel Core Ultra 7 270K Plus (Arrow Lake), pinned to P-core 2 under a quest gate --host hold, release builds of the three llrt feature sets. µs per op, lower is better.

Operation graviola (crypto-graviola-rust) rust (crypto-rust) rind rind vs rust
SHA-256 digest 1 MiB 302.3 316.1 221.7 1.4x
HMAC-SHA-256 sign 1 KiB 0.9 0.9 0.9 1.0x
AES-256-GCM encrypt 1 MiB 446.9 1835.8 287.3 6.4x
ECDSA P-256 sign 324.5 331.7 12.9 25.7x
ECDSA P-256 verify 118.6 118.6 32.0 3.7x
ECDSA P-384 sign 948.1 970.1 44.2 21.9x
ECDSA P-384 verify 355.2 353.6 101.4 3.5x
ECDH P-256 deriveBits 215.1 220.0 30.7 7.2x
Ed25519 sign 45.5 47.2 9.3 5.1x
Ed25519 verify 30.6 31.4 22.4 1.4x
X25519 deriveBits 28.9 28.4 14.7 1.9x
RSA-2048 PKCS#1 v1.5 sign 834.6 822.4 387.7 2.1x
RSA-2048 PKCS#1 v1.5 verify 38.1 37.1 10.0 3.7x
RSA-2048 PSS sign 664.3 675.2 388.4 1.7x
RSA-2048 PSS verify 38.2 37.9 10.4 3.6x
RSA-2048 OAEP decrypt 682.8 667.8 389.1 1.7x
PBKDF2-SHA-256 10k iterations 625.8 652.6 565.3 1.2x
RSA-2048 generateKey 27614.6 28932.3 25516.0 1.1x

Times include llrt's JS-to-Rust call overhead (the 0.9 µs HMAC row is mostly that). The host carried other non-gated load (load average ~29 on 24 threads), so treat it as one indicative pass. No Graviton numbers: no arm64 host was free; the plan's Graviton claim needs one before the upstream PR leans on it.

webcrypto-bench.mjs
// Times WebCrypto operations under whichever crypto backend the runtime was built with.
// Run: llrt webcrypto-bench.mjs   (also runs under node for a reference)
const subtle = globalThis.crypto.subtle;
const enc = new TextEncoder();
const msg = enc.encode("x".repeat(1024));
const big = new Uint8Array(1 << 20);

async function bench(name, fn, minMs = 1000) {
  for (let i = 0; i < 3; i++) await fn(); // warm up
  let n = 0;
  const start = performance.now();
  let elapsed = 0;
  while (elapsed < minMs) {
    await fn();
    n++;
    elapsed = performance.now() - start;
  }
  const us = (elapsed * 1000) / n;
  console.log(`${name.padEnd(36)} ${us.toFixed(1).padStart(10)} us/op`);
}

const iv = new Uint8Array(12);
const aes = await subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt"]);
const hmac = await subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const p256 = await subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
const p384 = await subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, false, ["sign", "verify"]);
const ecdhA = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
const ecdhB = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
const ed = await subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]);
const xA = await subtle.generateKey({ name: "X25519" }, false, ["deriveBits"]);
const xB = await subtle.generateKey({ name: "X25519" }, false, ["deriveBits"]);
const rsaParams = { modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
const rsa = await subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", ...rsaParams }, false, ["sign", "verify"]);
const pss = await subtle.generateKey({ name: "RSA-PSS", ...rsaParams }, false, ["sign", "verify"]);
const oaep = await subtle.generateKey({ name: "RSA-OAEP", ...rsaParams }, false, ["encrypt", "decrypt"]);
const pbkdf2 = await subtle.importKey("raw", enc.encode("password"), "PBKDF2", false, ["deriveBits"]);

const p256Sig = await subtle.sign({ name: "ECDSA", hash: "SHA-256" }, p256.privateKey, msg);
const p384Sig = await subtle.sign({ name: "ECDSA", hash: "SHA-384" }, p384.privateKey, msg);
const edSig = await subtle.sign("Ed25519", ed.privateKey, msg);
const rsaSig = await subtle.sign("RSASSA-PKCS1-v1_5", rsa.privateKey, msg);
const pssSig = await subtle.sign({ name: "RSA-PSS", saltLength: 32 }, pss.privateKey, msg);
const oaepCt = await subtle.encrypt("RSA-OAEP", oaep.publicKey, msg.slice(0, 32));

await bench("SHA-256 digest 1 MiB", () => subtle.digest("SHA-256", big));
await bench("HMAC-SHA-256 sign 1 KiB", () => subtle.sign("HMAC", hmac, msg));
await bench("AES-256-GCM encrypt 1 MiB", () => subtle.encrypt({ name: "AES-GCM", iv }, aes, big));
await bench("ECDSA P-256 sign", () => subtle.sign({ name: "ECDSA", hash: "SHA-256" }, p256.privateKey, msg));
await bench("ECDSA P-256 verify", () => subtle.verify({ name: "ECDSA", hash: "SHA-256" }, p256.publicKey, p256Sig, msg));
await bench("ECDSA P-384 sign", () => subtle.sign({ name: "ECDSA", hash: "SHA-384" }, p384.privateKey, msg));
await bench("ECDSA P-384 verify", () => subtle.verify({ name: "ECDSA", hash: "SHA-384" }, p384.publicKey, p384Sig, msg));
await bench("ECDH P-256 deriveBits", () => subtle.deriveBits({ name: "ECDH", public: ecdhB.publicKey }, ecdhA.privateKey, 256));
await bench("Ed25519 sign", () => subtle.sign("Ed25519", ed.privateKey, msg));
await bench("Ed25519 verify", () => subtle.verify("Ed25519", ed.publicKey, edSig, msg));
await bench("X25519 deriveBits", () => subtle.deriveBits({ name: "X25519", public: xB.publicKey }, xA.privateKey, 256));
await bench("RSA-2048 PKCS#1 v1.5 sign", () => subtle.sign("RSASSA-PKCS1-v1_5", rsa.privateKey, msg));
await bench("RSA-2048 PKCS#1 v1.5 verify", () => subtle.verify("RSASSA-PKCS1-v1_5", rsa.publicKey, rsaSig, msg));
await bench("RSA-2048 PSS sign", () => subtle.sign({ name: "RSA-PSS", saltLength: 32 }, pss.privateKey, msg));
await bench("RSA-2048 PSS verify", () => subtle.verify({ name: "RSA-PSS", saltLength: 32 }, pss.publicKey, pssSig, msg));
await bench("RSA-2048 OAEP decrypt", () => subtle.decrypt("RSA-OAEP", oaep.privateKey, oaepCt));
await bench("PBKDF2-SHA-256 10k iterations", () =>
  subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt: msg.slice(0, 16), iterations: 10000 }, pbkdf2, 256)
);
await bench("RSA-2048 generateKey", () =>
  subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", ...rsaParams }, false, ["sign"]), 5000
);

Upstream drafts

llrt has no AI-contribution policy (none in CONTRIBUTING.md or the PR template). Its CONTRIBUTING.md asks for an issue before significant work, and its PR template ends with a licensing confirmation the poster must make themselves. awslabs/llrt#877 ("Reduce number of dependencies of llrt_crypto") is related.

Issue: "Add a rind crypto and TLS backend"
[rind](https://github.com/moq-dev/rind) is a pure-Rust cryptography library (no C toolchain needed), derived from graviola. Much of its arithmetic comes from formally verified sources (s2n-bignum, libcrux/HACL*, fiat-crypto), and it covers the whole WebCrypto surface llrt's `CryptoProvider` needs: SHA-1/2, HMAC, HKDF, PBKDF2, AES-GCM/CBC/CTR/KW, ECDSA and ECDH on P-256/384/521, Ed25519, X25519, and RSA PKCS#1 v1.5/PSS/OAEP. It runs on x86_64 and aarch64 (Linux, macOS, Windows), and `rind-rustls` provides a rustls `CryptoProvider`.

I'd like to add `crypto-rind` and `tls-rind` features next to `crypto-graviola` and `tls-graviola`. A working branch is at https://github.com/kixelated/llrt/tree/crypto-rind. Unlike `crypto-graviola`, it doesn't need RustCrypto to fill gaps in the provider.

One change outside the new backend: rind only signs and verifies digests it computes itself, so `CryptoProvider::{ecdsa,rsa_pss,rsa_pkcs1v15}_{sign,verify}` would take the message and its `HashAlgorithm` instead of a precomputed digest. The existing providers hash first, so their behavior doesn't change.

rind also refuses a few things the other backends accept: RSA key generation outside 2048 to 8192 bits or with an exponent other than 65537, and ECDSA with SHA-1. `subtle.supports()` reports these, and the RSA tests would move from 1024-bit to 2048-bit keys.

Rough numbers from one pass on an Intel Core Ultra 7 270K (µs/op through `crypto.subtle`, `crypto-graviola-rust` vs `crypto-rind`): ECDSA P-256 sign 324.5 vs 12.9, ECDSA P-256 verify 118.6 vs 32.0, AES-256-GCM 1 MiB 446.9 vs 287.3, Ed25519 sign 45.5 vs 9.3, RSA-2048 PKCS#1 v1.5 sign 834.6 vs 387.7, SHA-256 1 MiB 302.3 vs 221.7.

Would you accept a PR for this?
PR: "feat: add rind crypto and TLS backends" (from kixelated/llrt:crypto-rind, after the issue gets a yes)
### Issue # (if available)

Closes #<issue>

### Description of changes

Adds `crypto-rind` and `tls-rind` features backed by [rind](https://github.com/moq-dev/rind) and [rind-rustls](https://crates.io/crates/rind-rustls), next to the graviola ones.

- `refactor(crypto)`: the ECDSA and RSA sign/verify methods of `CryptoProvider` take the message and its `HashAlgorithm` instead of a digest, since rind only signs digests it computes. The rust, ring, graviola and openssl providers hash first through a shared helper, so they behave as before.
- `provider/rind.rs` implements every `CryptoProvider` method on rind. `tls-rind` uses `rind_rustls::default_provider()` in llrt_tls, llrt_http, llrt_fetch and llrt_test_tls.
- `subtle.supports()` reports what rind refuses: RSA key generation outside 2048 to 8192 bits or with an exponent other than 65537, and ECDSA with SHA-1.
- RSA tests now generate 2048-bit keys instead of 1024-bit ones, which every backend supports.
- CI matrix, release build mapping, README and docs/install.md list the new features like graviola's.

Testing: `make check`-equivalent clippy for `crypto-rind,tls-rind` (with and without `pqc`) and `crypto-rust,tls-ring`; the JS unit suite fails the same tests with `crypto-rind,tls-rind` as with `crypto-rust,tls-ring` locally (Temporal/Intl without `intl402`, network), and passes all crypto tests with `pqc`.

Benchmarks (µs/op through `crypto.subtle`, one pass, Intel Core Ultra 7 270K P-core):

| Operation | crypto-graviola-rust | crypto-rust | crypto-rind |
| --- | ---: | ---: | ---: |
| SHA-256 digest 1 MiB | 302.3 | 316.1 | 221.7 |
| AES-256-GCM encrypt 1 MiB | 446.9 | 1835.8 | 287.3 |
| ECDSA P-256 sign | 324.5 | 331.7 | 12.9 |
| ECDSA P-256 verify | 118.6 | 118.6 | 32.0 |
| ECDSA P-384 sign | 948.1 | 970.1 | 44.2 |
| ECDH P-256 deriveBits | 215.1 | 220.0 | 30.7 |
| Ed25519 sign | 45.5 | 47.2 | 9.3 |
| X25519 deriveBits | 28.9 | 28.4 | 14.7 |
| RSA-2048 PKCS#1 v1.5 sign | 834.6 | 822.4 | 387.7 |
| RSA-2048 PSS verify | 38.2 | 37.9 | 10.4 |
| PBKDF2-SHA-256 10k iterations | 625.8 | 652.6 | 565.3 |

### Checklist

- [x] Created unit tests in `tests/unit` and/or in Rust for my feature if needed
- [x] Ran `make fix` to format JS and apply Clippy auto fixes
- [x] Made sure my code didn't add any additional warnings: `make check`
- [ ] Added relevant type info in `types/` directory
- [x] Updated documentation if needed ([API.md](API.md)/[README.md](README.md)/Other)

_By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._

Choices made without asking

  • Refactored llrt's provider trait instead of adding Digest::import_less_safe to rind, keeping rind's moq-dev/rind#379 decision.
  • Moved llrt's RSA tests from 1024-bit to 2048-bit keys rather than special-casing rind.
  • tls-rind panics at TLS setup with rind's MissingFeature message when the CPU lacks a required feature, matching rind's refuse-don't-fall-back rule; every Lambda CPU has them.
  • crypto-rind keeps llrt's RustCrypto PQC (pqc) and modern-WebCrypto code like every other backend.
  • Forked awslabs/llrt to kixelated/llrt and opened the fork-internal kixelated/llrt#1 to get llrt's CI on Windows and arm64.

Waiting

  • Maintainer: approve posting to awslabs/llrt. Options: (a) post the issue above now and open the PR from kixelated/llrt:crypto-rind once it gets a yes and kixelated/llrt#1 is green (recommended: CONTRIBUTING asks for an issue first, and the trait refactor touches every backend); (b) post issue and PR together; (c) hold. The PR template's licensing line is a statement the poster makes. Done when the issue URL is added here.
  • awslabs/llrt: maintainers accept the issue, then merge the PR. Check the issue and PR state.

Follow-ups

  • Graviton numbers: rerun webcrypto-bench.mjs on an arm64 runner before the PR leans on the plan's Graviton pitch. Plan (small, fold into this quest).
  • rind ML-KEM-512/1024 and ML-DSA, so pqc could run on rind too. Drop unless llrt maintainers ask: llrt's PQC is shared RustCrypto code outside the provider trait.
  • RSA SHA-1 signatures and keys under 2048 bits in rind. Drop: rind refuses them by policy.
  • Windows gnu: if the fork CI's rind Windows jobs fail, add x86_64-pc-windows-gnu to rind's build matrix. Plan only if it fails.

Models

  • Size: S
  • Implementer: Claude Code, claude-opus-5-5, high effort

(Written by Claude Opus 5.5 with high)

🤖 Generated with Claude Code

Quest-Claim: ecd9ff4fa688e7dc
@kixelated

Copy link
Copy Markdown
Collaborator Author

Closing: the maintainer dropped this quest on 2026-11-07 (let projects pull rind rather than send them patches). moq-dev/rind#471 deletes it with the others.

(Written by Claude Opus 5.5 with high)

@kixelated kixelated closed this Nov 7, 2026
@kixelated
kixelated deleted the quest/m2/adoption/llrt branch November 7, 2026 23:52
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.

1 participant