| Version | Supported |
|---|---|
| 0.2.x | Yes |
| < 0.2 | No |
Please report security vulnerabilities to security@theqrl.org.
Do not open public issues for security vulnerabilities.
wallet.js uses ML-DSA-87 (FIPS 204) for digital signatures via the @theqrl/mldsa87 package.
| Property | Value |
|---|---|
| Security Level | NIST Level 5 (256-bit classical) |
| Public Key Size | 2,592 bytes |
| Secret Key Size | 4,896 bytes |
| Signature Size | 4,627 bytes |
Seed (48 bytes, random)
│
├── SHA-256 ──► ML-DSA-87 KeyGen ──► (pk, sk)
│
└── Descriptor (3 bytes) + Seed ──► Extended Seed (51 bytes)
│
└── Mnemonic (34 words)
Address = SHAKE-256(Descriptor || PublicKey, 20 bytes)
Addresses are 20 bytes, displayed with a Q prefix in hexadecimal (41 characters total).
Important: Unlike BIP39, QRL mnemonics do not include a checksum for error detection.
Implications:
- A typo in a mnemonic word may still produce a valid (but different) wallet
- User errors during backup or restore cannot be detected by the library
- Example: "absorb" and "absent" are both valid words - swapping them produces a different wallet
Recommended Application-Level Mitigations:
-
Address Verification on Restore: Store a hash of the expected address alongside the encrypted mnemonic in wallet files:
// When creating wallet const wallet = MLDSA87.newWallet(); const addressHash = sha256(wallet.getAddress()); saveWalletFile({ encryptedMnemonic, addressHash }); // When restoring wallet const restored = MLDSA87.newWalletFromMnemonic(mnemonic); const restoredHash = sha256(restored.getAddress()); if (!constantTimeEqual(restoredHash, expectedHash)) { throw new Error('Mnemonic does not match expected wallet'); }
-
Visual Confirmation: Always display the derived address to the user after restore and require explicit confirmation.
-
Partial Address Display: Show the first/last few characters of the expected address during restore for visual verification.
ML-DSA-87 (FIPS 204) requires a 32-byte seed for key generation. QRL uses a 48-byte seed for mnemonic compatibility across wallet types. The seed is hashed with SHA-256 to derive the required 32-byte ML-DSA seed:
48-byte QRL Seed → SHA-256 → 32-byte ML-DSA-87 Seed → Key Generation
This is by design for FIPS 204 compliance and go-qrllib cross-implementation compatibility. The 256-bit entropy in the derived seed provides full security for ML-DSA-87's NIST Level 5.
| Asset | Sensitivity | Notes |
|---|---|---|
| Secret Key | Critical | Never expose; can sign arbitrary messages |
| Seed | Critical | Can derive secret key |
| Extended Seed | Critical | Contains seed |
| Mnemonic | Critical | Human-readable extended seed |
| Public Key | Public | Safe to share |
| Address | Public | Safe to share |
Important: JavaScript does not provide guaranteed secure memory handling.
-
Call
zeroize()when done:const wallet = MLDSA87.newWallet(); // ... use wallet ... wallet.zeroize(); // Overwrites sk, seed, extendedSeed with zeros
-
Limitations:
- JavaScript's garbage collector may retain copies
- JIT compilation may create additional copies
- This provides best-effort protection, not cryptographic guarantees
-
Recommendations:
- Minimize wallet lifetime in memory
- Avoid logging or serializing sensitive data
- Consider hardware security modules for high-value applications
| Function | Validation |
|---|---|
new Seed(bytes) |
Exactly 48 bytes |
new ExtendedSeed(bytes) |
Exactly 51 bytes, valid wallet type |
new Descriptor(bytes) |
Exactly 3 bytes, valid wallet type |
sign(sk, message) |
sk is Uint8Array of correct length, message is Uint8Array |
verify(sig, msg, pk) |
All inputs are Uint8Array of correct lengths |
stringToAddress(str) |
Starts with Q, 40 hex characters |
All validation errors throw Error with descriptive messages. Wrap wallet operations in try-catch:
try {
const wallet = MLDSA87.newWalletFromMnemonic(userInput);
} catch (e) {
console.error('Invalid mnemonic:', e.message);
}Design Note: Input validation functions (isValidAddress, etc.) return boolean. Data conversion and cryptographic functions throw on invalid input. Signature verification returns boolean (true/false) without leaking timing information about why verification failed.
Seed generation uses the randombytes package:
- Node.js: Uses
crypto.randomBytes()(CSPRNG) - Browser: Uses
crypto.getRandomValues()(Web Crypto API)
Both are cryptographically secure random number generators.
Timing side-channel resistance depends on the underlying @theqrl/mldsa87 implementation.
The mldsa87 package:
- Uses constant-time comparison for signature verification
- Follows FIPS 204 specification
| Package | Purpose | Security Notes |
|---|---|---|
@theqrl/mldsa87 |
ML-DSA-87 signatures | Audited; FIPS 204 compliant |
@noble/hashes |
SHA-256, SHAKE-256 | Widely audited; constant-time |
randombytes |
Secure random generation | Uses platform CSPRNG |
- Call
zeroize()when wallet is no longer needed - Validate addresses before sending transactions
- Use
isValidAddress()for user-provided addresses - Keep mnemonic backups offline and encrypted
- Use hardware wallets for high-value holdings
- Log or print secret keys, seeds, or mnemonics
- Store unencrypted mnemonics in databases or files
- Transmit seeds/mnemonics over networks
- Reuse seeds across different applications
- Ignore validation errors
This library has been security audited. See the internal audit for details.
| Category | Issues Found | Status |
|---|---|---|
| Critical | 1 | Fixed |
| High | 2 | Fixed |
| Medium | 1 | Fixed |
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/wallet.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/wallet.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/wallet.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