Service Info

Status--
Endpointhttp://docker:8085
PQC AlgorithmML-DSA-65
FIPS StandardFIPS 204 (ML-DSA)

Where PQC is Used

💳
Wallet Creation

Generate ML-DSA-65 keypair. Wallet address derived from SHA-256 of public key (0x-prefixed, 20 bytes).

ML-DSA-65 (FIPS 204)
Transaction Signing

Sign transaction data with the wallet's private key using provider.sign(). Signature proves the wallet owner authorized the transaction.

ML-DSA-65 (FIPS 204)
Signature Verification

Verify any transaction signature against the signer's public key using provider.verify(). Anyone with the public key can verify.

ML-DSA-65 (FIPS 204)

Description

This service demonstrates quantum-safe wallet operations — the same patterns you'll implement using the Qudo JNI provider in your own wallet/blockchain system.

Create wallet: provider.generateKeyPair("ML-DSA-65") — address derived from SHA-256 of public key
Sign transaction: provider.sign(txData, privKey, "ML-DSA-65")
Verify transaction: provider.verify(txData, sig, pubKey, "ML-DSA-65")

POST /api/wallet/create Create a PQC wallet
🔒 Qudo provider: provider.generateKeyPair("ML-DSA-65"). Returns walletId, address (0x-prefixed), and public key.
POST /api/wallet/{id}/sign-transaction Sign a transaction with wallet's private key
🔒 Qudo provider: provider.sign(txData, walletPrivKey, "ML-DSA-65"). Returns signature + public key for verification.
POST /api/wallet/verify-transaction Verify a transaction signature
🔒 Qudo provider: provider.verify(txData, sig, pubKey, "ML-DSA-65") -> true/false.
GET /api/wallet/list List all wallets
🔒 Returns all wallets with address, algorithm, and creation time.
GET /api/wallet/health Service health + wallet count
🔒 Returns service status and number of wallets created.

Wallet Migration Reference

Migrate your wallet/blockchain system from classical ECDSA (secp256k1) to quantum-safe ML-DSA-65. Use this playground to try PQC wallet operations, then use the Qudo JNI provider directly in your own wallet infrastructure.

1. How It Works 2. Migrate Your System 3. Performance 4. Pitfalls 5. FAQ

1. How PQC Wallets Work

Wallet Owner                                         Verifier / Network
   |                                                      |
   |  1. provider.generateKeyPair("ML-DSA-65")            |
   |     -> private key (stored securely)                  |
   |     -> public key -> SHA-256 -> wallet address (0x...)|
   |                                                      |
   |  2. provider.sign(txData, privKey, "ML-DSA-65")      |
   |     -> 3,309-byte signature                           |
   |                                                      |
   |---- transaction + signature + public key ----------->|
   |                                                      |
   |     3. provider.verify(txData, sig, pubKey,          |
   |        "ML-DSA-65") -> true/false                     |
   |     4. Derive address from pubKey, match sender       
💳
Wallet Identity: ML-DSA-65 Keypair

The wallet address is derived from SHA-256(publicKey), same pattern as classical wallets but with a quantum-safe keypair. The private key signs transactions; the public key verifies them.

Transaction Signing: provider.sign()

Each transaction is signed with provider.sign(txData, privKey, "ML-DSA-65"). The 3,309-byte signature proves the wallet owner authorized the transaction. Replaces ECDSA secp256k1.

2. Migrate Your System

Replace ECDSA secp256k1 with ML-DSA-65 in your wallet system. The transaction signing pattern stays the same — only the crypto library changes.

Before (ECDSA secp256k1)

// Classical wallet
ECGenParameterSpec spec =
    new ECGenParameterSpec("secp256k1");
KeyPairGenerator kpg =
    KeyPairGenerator.getInstance("EC");
kpg.initialize(spec);
KeyPair kp = kpg.generateKeyPair();

// Sign tx
Signature signer =
    Signature.getInstance("SHA256withECDSA");
signer.initSign(kp.getPrivate());
signer.update(txData);
byte[] sig = signer.sign(); // 64 bytes

After (ML-DSA-65 via Qudo JNI)

// PQC wallet
QudoKeyPair kp = provider.generateKeyPair(
    "ML-DSA-65");

// Derive address (same pattern)
byte[] addr = provider.sha256(
    kp.getPublicKeyPem());

// Sign tx
byte[] sig = provider.sign(txData,
    kp.getPrivateKeyPem(),
    "ML-DSA-65"); // 3,309 bytes

// Verify
boolean ok = provider.verify(txData,
    sig, kp.getPublicKeyPem(),
    "ML-DSA-65");

Complete Java Implementation

import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;

QudoCrypto provider = QudoCrypto.create();

// ============================================================
// 1. Create wallet (generate keypair + derive address)
// ============================================================
QudoKeyPair walletKeys = provider.generateKeyPair("ML-DSA-65");
byte[] addrHash = provider.sha256(walletKeys.getPublicKeyPem());
// Address = first 20 bytes of SHA-256, hex-encoded, 0x-prefixed
StringBuilder sb = new StringBuilder("0x");
for (int i = 0; i < 20; i++) sb.append(String.format("%02x", addrHash[i]));
String address = sb.toString(); // e.g., "0x08df2a5997394d45..."

// Store walletKeys.getPrivateKeyPem() securely (HSM, keystore)
// Publish walletKeys.getPublicKeyPem() + address

// ============================================================
// 2. Sign a transaction
// ============================================================
byte[] txData = "transfer 100 tokens to 0xabc123".getBytes();
byte[] signature = provider.sign(txData, walletKeys.getPrivateKeyPem(), "ML-DSA-65");
// Broadcast: txData + signature + publicKey

// ============================================================
// 3. Verify a transaction (anyone can do this)
// ============================================================
boolean valid = provider.verify(txData, signature,
    walletKeys.getPublicKeyPem(), "ML-DSA-65");
// valid == true -> transaction is authorized by wallet owner

// ============================================================
// 4. Verify sender address matches (on the verifier side)
// ============================================================
// senderPublicKey = public key received with the transaction
byte[] senderHash = provider.sha256(senderPublicKey);
StringBuilder senderSb = new StringBuilder("0x");
for (int i = 0; i < 20; i++) senderSb.append(String.format("%02x", senderHash[i]));
String senderAddress = senderSb.toString();
// Compare senderAddress with the claimed sender in txData

provider.close();
This is what runs in your wallet system. The signing pattern is identical to ECDSA — only the algorithm changes. ML-DSA-65 signatures are 3,309 bytes (vs 64 bytes for ECDSA), so plan for larger transaction payloads.

What Changes vs What Stays

ComponentClassical (ECDSA)PQC (Qudo JNI)
Keypair generationsecp256k1provider.generateKeyPair("ML-DSA-65")
Address derivationKeccak256(pubKey)provider.sha256(pubKey) — same pattern
Transaction signingSHA256withECDSAprovider.sign(tx, key, "ML-DSA-65")
Signature verificationECDSA verifyprovider.verify(tx, sig, key, "ML-DSA-65")
Signature size64 bytes3,309 bytes (~50x larger)
Transaction formatSameSame — just larger signature field
Address formatSame (0x-prefixed)Same

3. Performance

OperationPQC (ML-DSA-65)Classical (ECDSA)
Keypair generation~55ms<1ms
Transaction sign~10ms (cached key)<1ms
Signature verify~5ms<1ms
Signature size3,309 bytes64 bytes
Public key size~2,726 bytes (PEM)33 bytes (compressed)
Key takeaway: Signing with a cached keypair is ~10ms — fast enough for real-time transactions. The main impact is signature size (3.3KB vs 64 bytes), which affects block size and network bandwidth. Generate the keypair once, reuse for all transactions.

4. Pitfalls

Pitfall: Signature size impacts block/transaction size

Cause: ML-DSA-65 signatures are 3,309 bytes vs 64 bytes for ECDSA. A block with 1,000 transactions adds ~3.3MB of signature data.
Mitigation: Use ML-DSA-44 (2,420 bytes) for higher throughput. Or aggregate signatures off-chain and commit a batch proof on-chain.

Pitfall: Public key size affects key directories

Cause: ML-DSA-65 public keys are ~2.7KB vs 33 bytes for compressed ECDSA. Address books and peer discovery need larger storage.
Fix: Use the wallet address (20 bytes) for identification. Only transmit the full public key when verifying a transaction.

Pitfall: Key storage needs updating

Cause: ML-DSA-65 private keys are larger than ECDSA. Existing keystores may need capacity updates.
Fix: The Qudo provider outputs standard PEM keys. Store with provider.generateKeyPair("ML-DSA-65").getPrivateKeyPem() in your existing HSM or keystore.

5. FAQ

Q: Can PQC wallets interact with classical ECDSA wallets?

A: Not directly — the signature algorithms are incompatible. During migration, run dual-signature wallets: sign with both ECDSA and ML-DSA-65. Verifiers check whichever they support. Phase out ECDSA once the network migrates.

Q: Which algorithm for high-throughput transaction signing?

A: ML-DSA-44 (2,420-byte signatures, ~8ms sign). For most wallets, ML-DSA-65 is recommended (3,309 bytes, ~10ms). Only use ML-DSA-87 for high-value cold storage wallets.

Q: How does address derivation work with PQC?

A: Same pattern as classical: address = SHA-256(publicKey)[0:20]. The public key is larger (2.7KB vs 33 bytes) but the derived address is the same size (20 bytes, 0x-prefixed). Clients identify wallets by address, not public key.

Crypto Inventory Scanner

Analyze any endpoint's cryptographic configuration. Enter a host:port to scan its TLS setup and identify what's quantum-safe vs what needs migration.

Scan Endpoint