Service Info

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

Where PQC is Used

🔒
Message Encryption

ML-KEM-768 key encapsulation derives an AES-256-GCM key. Message body encrypted with AES. Only the intended recipient can decrypt.

ML-KEM-768 + AES-256-GCM (FIPS 203)
Message Signing

ML-DSA-65 signature on the message body. Proves the sender's identity and ensures the message wasn't tampered with.

ML-DSA-65 (FIPS 204)
🔐
Sign-then-Encrypt

Full S/MIME-like workflow: sender signs with ML-DSA-65, then encrypts the signed payload with ML-KEM-768 for the recipient.

ML-DSA-65 + ML-KEM-768

Description

The Email Encryption Service demonstrates quantum-safe email protection using two PQC algorithms:

Encryption (ML-KEM-768) — The recipient's ML-KEM public key is used to encapsulate a shared secret, which derives an AES-256-GCM key to encrypt the message body. Only the recipient's private key can recover it.

Signing (ML-DSA-65) — The sender signs the message with ML-DSA-65, proving authenticity and preventing tampering. The signature is verified using the sender's public key.

Send Secure — Combines both: sign the message with ML-DSA-65, then encrypt the signed message with ML-KEM-768. This mirrors the S/MIME sign-then-encrypt pattern used in enterprise email.

POST /api/email/encrypt Encrypt a message
🔒 Qudo provider: kemEncapsulate(pubKey, "ML-KEM-768") -> shared secret, then AES-256-GCM encrypt. Each call produces a unique shared secret.
POST /api/email/decrypt Decrypt a message
🔒 Qudo provider: kemDecapsulate(ciphertext, privKey, "ML-KEM-768") -> recover shared secret, then AES-256-GCM decrypt.
POST /api/email/sign Sign a message
🔒 Qudo provider: sign(data, privKey, "ML-DSA-65") -> 3,309-byte signature. Returns signature + public key for verification.
POST /api/email/verify Verify a signature
🔒 Qudo provider: verify(data, signature, pubKey, "ML-DSA-65") -> true/false.
POST /api/email/send-secure Send secure (sign + encrypt)
🔒 S/MIME pattern: sign(ML-DSA-65) then kemEncapsulate(ML-KEM-768) + AES-256-GCM encrypt.
POST /api/email/receive-secure Receive secure (decrypt + verify)
🔒 S/MIME pattern: kemDecapsulate(ML-KEM-768) + AES-256-GCM decrypt, then verify(ML-DSA-65). Returns plaintext + signatureValid.
GET /api/email/health Service health + supported algorithms
🔒 Returns service status, ML-KEM + ML-DSA algorithms available for email encryption and signing.

Email Encryption Migration Reference

Migrate email encryption from classical S/MIME (RSA/ECDH) to quantum-safe ML-KEM + ML-DSA using the Qudo Cryptographic Module. Use this playground to understand PQC email patterns, then integrate the Qudo JNI provider directly into your own system — no dependency on this REST API.

1. How It Works 2. Try It (Interactive) 3. Migrate Your System 4. Classical vs PQC 5. Performance 6. Compatibility 7. Pitfalls 8. FAQ

1. How PQC Email Encryption Works

Sender (Alice)                                       Recipient (Bob)
   |                                                      |
   |  1. Bob publishes ML-KEM-768 public key              |
   |     Alice has ML-DSA-65 signing keypair               |
   |                                                      |
   |  2. provider.sign(message, alicePrivKey, "ML-DSA-65")|
   |  3. provider.kemEncapsulate(bobPubKey, "ML-KEM-768") |
   |     -> per-message ciphertext + shared secret         |
   |  4. Derive AES-256 key from shared secret (SHA-256)  |
   |  5. AES-256-GCM encrypt signed message                |
   |                                                      |
   |---- KEM ciphertext + AES ciphertext + IV ----------->|
   |                                                      |
   |    6. provider.kemDecapsulate(kemCt, bobPrivKey,     |
   |       "ML-KEM-768") -> recover shared secret          |
   |    7. Derive AES-256 key, decrypt AES-256-GCM        |
   |    8. provider.verify(message, sig, alicePubKey,     |
   |       "ML-DSA-65") -> true                            
🔒
Confidentiality: Real ML-KEM Encapsulation + AES-256-GCM

Each message gets a unique shared secret via ML-KEM encapsulation (FIPS 203) through the QUDO JNI provider. The shared secret derives the AES-256-GCM key. Compromising one message does not reveal any other — true per-message forward secrecy.

Authenticity: ML-DSA (FIPS 204)

The sender signs the plaintext with ML-DSA before encryption (sign-then-encrypt, per S/MIME). The recipient verifies the signature after decryption. Supports ML-DSA-44 (Level 2), ML-DSA-65 (Level 3, default), and ML-DSA-87 (Level 5).

🔑
Key Management

Recipients generate ML-KEM keypairs with provider.generateKeyPair("ML-KEM-768"), store private keys in HSM, and publish public keys via a directory (LDAP, certificate, or key server). Senders retrieve recipient public keys before encapsulation.

2. Try It: Interactive Email Workflows

Test PQC email operations directly. Each workflow shows the full request/response with PQC algorithm details.

Workflow A: Encrypt & Decrypt

Encrypt a message with ML-KEM-768 + AES-256-GCM, then decrypt it.

Workflow B: Sign & Verify

Sign a message with ML-DSA-65, then verify the signature. Try tampering to see verification fail.

Workflow C: Send + Receive Secure Email (Full S/MIME)

Complete end-to-end workflow: sign with ML-DSA-65, encrypt with ML-KEM-768 encapsulation, then decrypt and verify on the recipient side.

3. Migrate Your System

The playground above uses a REST API for convenience. In your production system, use the Qudo JNI provider directly — no HTTP overhead, no dependency on this portal.

import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;
import com.qudo.crypto.QudoKemResult;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;

// ============================================================
// Initialize the Qudo provider once at application startup
// ============================================================
QudoCrypto provider = QudoCrypto.create();

// ============================================================
// 1. KEY GENERATION — generate ML-KEM and ML-DSA keypairs
// ============================================================
// Recipient generates an ML-KEM-768 keypair (store privKey securely, publish pubKey)
try (QudoKeyPair recipientKeys = provider.generateKeyPair("ML-KEM-768")) {
    byte[] recipientPubKey = recipientKeys.getPublicKeyPem();
    byte[] recipientPrivKey = recipientKeys.getPrivateKeyPem();

    // Sender generates an ML-DSA-65 signing keypair
    try (QudoKeyPair senderKeys = provider.generateKeyPair("ML-DSA-65")) {

        byte[] message = "Q4 revenue exceeded projections by 15%".getBytes();

        // ============================================================
        // 2. SIGN — sender signs the message with ML-DSA-65
        // ============================================================
        byte[] signature = provider.sign(message, senderKeys.getPrivateKeyPem(), "ML-DSA-65");

        // ============================================================
        // 3. ENCRYPT — sender encapsulates + encrypts for recipient
        // ============================================================
        // ML-KEM encapsulate: produces a per-message shared secret
        QudoKemResult encap = provider.kemEncapsulate(recipientPubKey, "ML-KEM-768");
        byte[] kemCiphertext = encap.getCiphertext();   // send this to recipient
        byte[] sharedSecret = encap.getSharedSecret();   // use locally for AES key
        encap.destroy(); // zero shared secret from KEM result

        // Derive AES-256 key from shared secret, then zero the secret
        byte[] aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret);
        java.util.Arrays.fill(sharedSecret, (byte) 0);

        // Encrypt with AES-256-GCM
        byte[] iv = new byte[12];
        new SecureRandom().nextBytes(iv);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"),
                     new GCMParameterSpec(128, iv));
        byte[] ciphertext = cipher.doFinal(message);

        // Send to recipient: kemCiphertext, ciphertext, iv, signature, senderPubKey

        // ============================================================
        // 4. DECRYPT — recipient decapsulates + decrypts
        // ============================================================
        // ML-KEM decapsulate: recover the shared secret with private key
        byte[] recoveredSecret = provider.kemDecapsulate(
                kemCiphertext, recipientPrivKey, "ML-KEM-768");

        byte[] recoveredAesKey = MessageDigest.getInstance("SHA-256").digest(recoveredSecret);

        Cipher decCipher = Cipher.getInstance("AES/GCM/NoPadding");
        decCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(recoveredAesKey, "AES"),
                        new GCMParameterSpec(128, iv));
        byte[] plaintext = decCipher.doFinal(ciphertext);

        // ============================================================
        // 5. VERIFY — recipient verifies the sender's signature
        // ============================================================
        boolean valid = provider.verify(
                plaintext, signature, senderKeys.getPublicKeyPem(), "ML-DSA-65");

        System.out.println("Decrypted: " + new String(plaintext));
        System.out.println("Signature valid: " + valid);
    }
}

// Cleanup at shutdown
provider.close();
This is what runs in your system. The Qudo JNI provider handles all PQC operations natively via OpenSSL FIPS. No REST calls, no external dependencies. Add com.qudo:qudo-jni-crypto to your Maven/Gradle build and set -Djava.library.path to the native library location.

4. Classical vs PQC Comparison

Classical S/MIME

# Key exchange: RSA key transport
openssl rsautl -encrypt -pubin -inkey recipient.pub \
  -in aes.key -out wrapped.key

# Sign: RSA-SHA256
openssl dgst -sha256 -sign sender.key \
  -out sig.bin message.txt

PQC Email (Qudo JNI Provider)

// Key exchange: ML-KEM-768 encapsulation
QudoKemResult e = provider.kemEncapsulate(
    recipientPubKey, "ML-KEM-768");
byte[] sharedSecret = e.getSharedSecret();
// -> derive AES key, encrypt with AES-256-GCM

// Sign: ML-DSA-65 (FIPS 204)
byte[] sig = provider.sign(
    message, senderPrivKey, "ML-DSA-65");

// Verify
boolean ok = provider.verify(
    message, sig, senderPubKey, "ML-DSA-65");
ComponentChanges?Details
Key transport / exchangeYesRSA encrypt / ECDH → ML-KEM-768 encapsulation
Message encryptionNoAES-256-GCM stays the same — symmetric crypto is quantum-safe
Digital signatureYesRSA-SHA256 / ECDSA → ML-DSA-65
S/MIME workflowNoSign-then-encrypt pattern stays the same
CMS / PKCS#7 formatMinimalSame ASN.1 structure, new algorithm OIDs for ML-KEM and ML-DSA
Recipient key managementYesRecipients need ML-KEM-768 keypairs instead of RSA/ECDH keypairs
Sender key managementYesSenders need ML-DSA-65 signing keys instead of RSA/ECDSA

5. Performance

OperationPQC (ML-KEM + ML-DSA)Classical (RSA + ECDSA)
Encrypt (KEM + AES)~130ms~5ms
Decrypt (KEM + AES)~30ms~3ms
Sign (ML-DSA-65)~120ms~2ms (ECDSA)
Verify (ML-DSA-65)~40ms~2ms (ECDSA)
Full sign + encrypt~200ms~10ms
Signature size3,309 bytes64 bytes (ECDSA)
Encapsulated key size~1KB~256 bytes (RSA)
Impact: PQC email operations add ~200ms total for a full sign+encrypt. This is acceptable for email where delivery latency is typically seconds to minutes. For high-volume automated email systems, batch operations and pre-cached keys reduce per-message overhead.

6. Compatibility

ComponentMinimum VersionNotes
OpenSSL3.6.0+Required for ML-KEM keypair generation and ML-DSA signing
Qudo Module1.0.0+Provides ML-KEM-768 and ML-DSA-65 algorithms
Java17+For the Qudo JNI provider runtime. AES-256-GCM via javax.crypto
Email clientsNone nativelyNo major email client supports ML-KEM/ML-DSA natively yet. Use the Qudo provider in your email pipeline.
ThunderbirdFuturePQC S/MIME support expected as IETF standards finalize
OutlookFutureMicrosoft tracking NIST PQC standardization for S/MIME integration
Integration approach: Add the Qudo JNI provider to your email system. Your code calls provider.kemEncapsulate() / provider.sign() directly before sending, and provider.kemDecapsulate() / provider.verify() upon receipt. No middleware needed — the provider runs in-process.

7. Common Pitfalls

Pitfall: Key distribution must be solved before deployment

Cause: ML-KEM requires the sender to have the recipient's public key before encapsulation. Unlike RSA key transport, there's no way to encrypt without a pre-shared public key.
Fix: Set up a key directory (LDAP, database, or certificate-based) where recipients publish their ML-KEM public keys. Generate keypairs with provider.generateKeyPair("ML-KEM-768"), store private keys in HSM, distribute public keys via your directory.

Pitfall: PQC overhead makes short emails larger than the plaintext

Cause: ML-DSA-65 signature (3.3KB) + ML-KEM encapsulated key (~1KB) = ~4.4KB overhead. A 10-byte "ok" message becomes 4.4KB after PQC protection.
Mitigation: For very short messages, consider batching or using ML-DSA-44 (2.4KB signature). For typical email (1KB+), the overhead is proportionally small.

Pitfall: Securely store and manage PQC private keys

Cause: ML-KEM private keys are larger than RSA keys (~3KB for ML-KEM-768). Existing key storage may need updates.
Fix: Store ML-KEM/ML-DSA private keys in an HSM, secure keystore, or KMS. The Qudo provider outputs standard PEM-encoded keys compatible with PKCS#8 storage. Call provider.generateKeyPair("ML-KEM-768") and persist getPrivateKeyPem() securely. The playground stores keys in memory for convenience — never do this in production.

8. FAQ

Q: Can I encrypt emails to recipients who don't have PQC keys?

A: Not with PQC alone. During migration, implement a dual-mode path in your email pipeline: use classical S/MIME (RSA/ECDH) for recipients without PQC keys, and ML-KEM + ML-DSA for PQC-capable recipients. Check the recipient's key type in your directory and route accordingly.

Q: How do I integrate with my existing email system?

A: Add the Qudo JNI provider to your email pipeline:
1. Outgoing: Before SMTP send, call provider.sign() + provider.kemEncapsulate() + AES encrypt
2. Incoming: After IMAP fetch, call provider.kemDecapsulate() + AES decrypt + provider.verify()
The provider runs in-process — no external service dependency. See Section 3 for the complete Java code.

Q: When will Outlook/Thunderbird support PQC S/MIME natively?

A: IETF is finalizing PQC-in-CMS standards (draft-ietf-lamps-cms-kyber, draft-ietf-lamps-dilithium-certificates). Once ratified, email clients will add native support. Timeline is likely 2025-2026 for early adopters. The Qudo provider lets you start using PQC email today in your backend without waiting for client-side support.

Q: Is the sign-then-encrypt order correct?

A: Yes. S/MIME uses sign-then-encrypt: the sender signs the plaintext, then encrypts the signed message. The recipient decrypts first, then verifies the signature on the plaintext. This ensures the signature is bound to the actual message content, not the ciphertext.

Q: Can I use different algorithms for encryption and signing?

A: Yes. Pass the algorithm name directly to the Qudo provider: provider.kemEncapsulate(pubKey, "ML-KEM-1024") or provider.sign(data, privKey, "ML-DSA-87"). Supported: ML-KEM-512/768/1024 (encryption), ML-DSA-44/65/87 (signing). In the playground, pass "algorithm":"ML-KEM-1024" in the JSON body.

Q: How do I manage recipient public keys?

A: Options for key distribution:
1. Key directory — LDAP/Active Directory with ML-KEM-768 public key attribute
2. Certificate-based — Issue ML-DSA-65 certificates via the CA service, publish in a directory
3. Key server — Dedicated key distribution endpoint (similar to PGP key servers)
4. First-contact — Exchange public keys on first encrypted communication (like Signal)

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