This document describes security considerations for using qrypto.js, including JavaScript-specific limitations and best practices.
qrypto.js implements post-quantum digital signature algorithms (Dilithium5 and ML-DSA-87). While the cryptographic algorithms are secure, JavaScript/Node.js environments have inherent limitations that affect how secret key material can be handled.
JavaScript does not provide:
- Direct memory access - Cannot guarantee memory is overwritten at a specific location
- Compiler barrier semantics - JIT compilers may optimize away "dead store" writes
- Control over garbage collection - Memory may persist until GC runs (non-deterministic)
- Protection against memory swapping - Secrets may be written to disk swap space
Even with Uint8Array.fill(0):
- The operation may be optimized away by the JIT compiler if the array is not used afterward
- Previous values may remain in memory until the garbage collector reclaims the buffer
- The JavaScript runtime may have copied the data internally (e.g., during Buffer operations)
Buffer.alloc()initializes memory to zero but doesn't prevent later exposureBuffer.allocUnsafe()may contain previous memory contents- Buffer pooling may cause memory reuse across different operations
Converting secret key data to/from strings creates copies that:
- Cannot be overwritten (strings are immutable in JavaScript)
- May be interned or cached by the runtime
- Persist until garbage collected
Despite these limitations, qrypto.js implements defense-in-depth measures:
All secret key material is stored in Uint8Array buffers, not strings. This allows:
- Explicit zeroing (even if not guaranteed to be secure)
- Avoiding string interning
- Clearer intent about data sensitivity
Signature verification uses constant-time comparison to prevent timing attacks:
// From cryptoSignVerify:
let diff = 0;
for (i = 0; i < length; ++i) {
diff |= a[i] ^ b[i];
}
return diff === 0;Signing is not constant-time. The cryptoSignSignature() path exhibits measurable timing variability across different secret keys, even when signing the same fixed message. This is not a bug in the implementation — it is an inherent property of the Dilithium/ML-DSA algorithm, which uses rejection sampling during signing.
-
Rejection sampling loop (dominant source): The signing function contains a
while (true)loop that generates candidate signatures and rejects those that would leak information about the secret key. The number of iterations before a valid signature is found depends on the secret key's internal structure (the s1, s2, and t0 polynomials). Different keys produce different rejection rates at the norm checks on z, w0, and the hint vector. This is by design — the rejection sampling is what makes the signature zero-knowledge — but it means signing time is inherently key-dependent. -
JavaScript arithmetic (secondary source): The Montgomery reduction and other arithmetic operations use JavaScript number types. The JavaScript specification does not guarantee that these operations are constant-time, and execution time may vary based on operand values.
Under controlled local measurement using process.hrtime.bigint() with deterministic seed-derived keypairs, warmup runs, and fixed 32-byte messages:
- ML-DSA-87: Cross-key median signing time ranged from ~4.9 ms to ~34.4 ms (~7x spread)
- Dilithium5: Cross-key median signing time ranged from ~4.9 ms to ~23.1 ms (~4.7x spread)
The effect persists under round-robin measurement ordering with retained raw samples, ruling out simple benchmark-order artifacts. A timing regression harness is available at scripts/timing-sign.mjs.
- Signature verification is constant-time (see above) — this issue affects signing only
- An attacker with repeated signing access and high-resolution timing may be able to distinguish keys or infer information about the secret key's rejection behavior
- Practical impact depends on deployment context: local or same-host observers are more plausible than network-only observers, where jitter typically drowns out the signal
- No practical key-recovery exploit has been demonstrated from this timing signal
- For applications with strict constant-time requirements, use the Go implementation (go-qrllib) which provides better timing guarantees through constant-time arithmetic primitives
- Rate-limit signing operations at the application layer to reduce timing attack feasibility
- Run signing operations in isolated environments where timing cannot be observed by adversaries
- Use randomized (hedged) signing to add per-signature randomness, which increases same-key timing variance and makes cross-key correlation harder
- Do not expose a signing oracle directly to untrusted users without authentication and rate limiting
All cryptographic functions validate input lengths and types to prevent:
- Buffer overflow/underflow issues
- Type confusion attacks
- Invalid parameter combinations
The zeroize() function is provided for clearing sensitive buffers:
import { zeroize } from '@aspect-build/qrypto-common';
const sk = new Uint8Array(CryptoSecretKeyBytes);
cryptoSignKeypair(seed, pk, sk);
// Use the secret key...
const signature = cryptoSign(message, sk);
// Clear when done (best effort)
zeroize(sk);Important: Due to JavaScript limitations, this is a best-effort operation. There is no guarantee that:
- The memory is actually zeroed (JIT optimization)
- Copies don't exist elsewhere (GC, Buffer pooling)
- The data wasn't swapped to disk
- Minimize secret lifetime - Generate keys only when needed, zero them as soon as possible
- Avoid serialization - Don't convert secrets to strings, JSON, or other formats
- Don't log secrets - Never log, print, or transmit secret key material
- Use secure storage - For persistent keys, consider:
- Hardware Security Modules (HSMs)
- Operating system keychains
- Encrypted storage with proper key management
- Consider WebCrypto - For browser environments, WebCrypto provides non-extractable keys
If your threat model requires strong memory protection:
- Use native implementations - Consider go-qrllib or C implementations that provide better memory control
- Use HSMs - Hardware Security Modules provide the strongest protection
- Isolate processes - Run cryptographic operations in isolated processes/containers
- Disable swap - On systems handling secrets, consider disabling swap or using encrypted swap
- NIST PQC Round 3 finalist
- Security level: Category 5 (equivalent to AES-256)
- Key sizes: PK=2592, SK=4896, Sig=4595 bytes
- Cross-verified against pq-crystals reference (
ac743d5)
- NIST FIPS 204 standardized algorithm
- Security level: Category 5 (equivalent to AES-256)
- Key sizes: PK=2592, SK=4896, Sig=4627 bytes
- Includes context parameter for domain separation
- Cross-verified against pq-crystals reference (latest)
If you discover a security vulnerability in qrypto.js:
- Do not open a public GitHub issue
- Contact the QRL security team privately
- Provide detailed reproduction steps
- Allow reasonable time for a fix before public disclosure
All npm packages are published with npm provenance, which cryptographically links published packages to their source repository and build workflow.
Verify provenance on npm:
npm audit signaturesAll releases include GitHub attestations backed by Sigstore:
- Build provenance for checksums and package files
- SBOM attestations in SPDX and CycloneDX formats
- SLSA Level 3 provenance for build verification
Each release includes Software Bill of Materials (SBOM) files:
sbom-spdx.json- SPDX formatsbom-cyclonedx.json- CycloneDX format
All releases include cryptographic attestations and checksums for verification.
# Verify attestations for package files
gh attestation verify package.json --owner theQRL
gh attestation verify package-lock.json --owner theQRL
# Verify SBOM attestation
gh attestation verify sbom-spdx.json --owner theQRLDownload and verify checksums from the release:
# Download checksums file
curl -LO https://github.com/theQRL/qrypto.js/releases/download/vX.Y.Z/checksums-sha256.txt
# Verify package files
sha256sum -c checksums-sha256.txt# Install slsa-verifier: https://github.com/slsa-framework/slsa-verifier#installation
# Download provenance
curl -LO https://github.com/theQRL/qrypto.js/releases/download/vX.Y.Z/provenance.intoto.jsonl
# Verify provenance
slsa-verifier verify-artifact package.json \
--provenance-path provenance.intoto.jsonl \
--source-uri github.com/theQRL/qrypto.jsEach release includes SBOMs in two formats:
- SPDX:
sbom-spdx.json - CycloneDX:
sbom-cyclonedx.json
These can be analyzed with tools like:
# Using grype for vulnerability scanning
grype sbom:sbom-spdx.json
# Using syft for inspection
syft convert sbom-cyclonedx.json -o table| Artifact | Attestation Type | Purpose |
|---|---|---|
package.json, package-lock.json |
Build provenance | Verify package dependencies |
checksums-sha256.txt |
Build provenance | Integrity verification |
sbom-spdx.json |
SBOM | Software composition |
sbom-cyclonedx.json |
SBOM | Software composition |
| Source code | SLSA provenance | Build reproducibility |
| npm package | npm provenance | Package authenticity |
Attestations are signed using GitHub's Sigstore integration:
- Identity: GitHub Actions OIDC token
- Transparency: Logged in Sigstore's Rekor transparency log
- Verification: Proves release came from official CI workflow