Skip to content

Security: theQRL/wallet.js

SECURITY.md

Security Policy

Supported Versions

Version Supported
0.2.x Yes
< 0.2 No

Reporting Vulnerabilities

Please report security vulnerabilities to security@theqrl.org.

Do not open public issues for security vulnerabilities.


Security Model

Cryptographic Primitives

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

Key Derivation

Seed (48 bytes, random)
    │
    ├── SHA-256 ──► ML-DSA-87 KeyGen ──► (pk, sk)
    │
    └── Descriptor (3 bytes) + Seed ──► Extended Seed (51 bytes)
                                              │
                                              └── Mnemonic (34 words)

Address Derivation

Address = SHAKE-256(Descriptor || PublicKey, 20 bytes)

Addresses are 20 bytes, displayed with a Q prefix in hexadecimal (41 characters total).


Mnemonic Security

No Built-in Checksum

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:

  1. 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');
    }
  2. Visual Confirmation: Always display the derived address to the user after restore and require explicit confirmation.

  3. Partial Address Display: Show the first/last few characters of the expected address during restore for visual verification.


Seed Derivation

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.


Sensitive Data

Assets to Protect

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

Memory Security

Important: JavaScript does not provide guaranteed secure memory handling.

  1. Call zeroize() when done:

    const wallet = MLDSA87.newWallet();
    // ... use wallet ...
    wallet.zeroize(); // Overwrites sk, seed, extendedSeed with zeros
  2. Limitations:

    • JavaScript's garbage collector may retain copies
    • JIT compilation may create additional copies
    • This provides best-effort protection, not cryptographic guarantees
  3. Recommendations:

    • Minimize wallet lifetime in memory
    • Avoid logging or serializing sensitive data
    • Consider hardware security modules for high-value applications

Input Validation

Validated Inputs

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

Error Handling

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.


Randomness

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.


Side-Channel Resistance

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

Dependencies

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

Best Practices

Do

  • 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

Don't

  • 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

Audit Status

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

Supply Chain Security

npm Provenance

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 signatures

Sigstore Attestations

All 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

Dependency Tracking

Each release includes Software Bill of Materials (SBOM) files:

  • sbom-spdx.json - SPDX format
  • sbom-cyclonedx.json - CycloneDX format

Release Verification

All releases include cryptographic attestations and checksums for verification.

Verifying with GitHub CLI

# 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 theQRL

Verifying Checksums

Download 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

Verifying SLSA Provenance

# 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.js

Software Bill of Materials (SBOM)

Each 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

What Gets Attested

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

Trust Model

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

There aren’t any published security advisories