Service Info

Status--
Endpointhttps://localhost:8443
PQC AlgorithmX25519MLKEM768 + ML-DSA-65
FIPS StandardFIPS 203 (ML-KEM) + FIPS 204 (ML-DSA)

Where PQC is Used

🔑
Hybrid Key Exchange (Data Transport)

X25519 + ML-KEM-768 combined shared secret derives the AES-256-GCM key that encrypts all API traffic

X25519MLKEM768
ML-DSA-65 JWT Signing (Authentication)

JWT tokens are signed with ML-DSA-65 via Qudo provider — quantum-safe authentication, not classical HMAC

ML-DSA-65 (FIPS 204)
🛡
Quantum-Safe Forward Secrecy

Each TLS session uses a fresh hybrid key exchange — past sessions stay safe even if long-term keys are compromised

ML-KEM-768 (FIPS 203)

Description

This service demonstrates two layers of quantum-safe protection for REST APIs:

Transport: TLS 1.3 with hybrid key exchange (X25519 + ML-KEM-768) via the Qudo provider. The AES-256-GCM session key is derived from both classical and post-quantum shared secrets — an attacker must break both to decrypt.

Authentication: JWT tokens signed with ML-DSA-65 (FIPS 204) via the Qudo provider. Asymmetric keypair replaces classical HMAC-SHA256 — sign with private key, verify with public key.

GET /api/hello Hello / connectivity check (public)
🔒 No authentication required. When accessed via the NGINX gateway, data is transported over hybrid PQC TLS (X25519MLKEM768).
GET /api/health Service health + Qudo provider status (public)
🔒 Returns whether the Qudo FIPS provider is active and available for PQC operations.
POST /api/auth/token Generate ML-DSA-65 signed JWT token
🔒 Qudo provider: provider.sign(payload, privKey, "ML-DSA-65"). Returns a JWT with {"alg":"ML-DSA-65"} header — the PQC replacement for HS256/ES256.
POST /api/auth/verify Verify a JWT signature (ML-DSA-65)
🔒 Qudo provider: provider.verify(data, sig, pubKey, "ML-DSA-65"). Returns {"valid":true/false}.
GET /api/auth/jwks Get ML-DSA-65 public key (JWKS)
🔒 Publish this public key so JWT consumers can verify tokens independently using provider.verify().
GET /api/secure Protected endpoint — requires ML-DSA-65 JWT
🔒 First call /api/auth/token to get a JWT, then pass it as Authorization: Bearer header. Both transport (X25519MLKEM768) and authentication (ML-DSA-65) are quantum-safe.
GET /api/crypto-info Discover available PQC algorithms — requires JWT
🔒 Lists ML-KEM, ML-DSA, and SLH-DSA algorithms available from the Qudo provider on this system.

REST API Migration Reference

Migrate your REST APIs to quantum-safe cryptography. Use this playground to understand PQC API patterns, then apply the Qudo provider to your own APIs — two changes to your infrastructure, zero changes to your business logic.

⚠️ Playground note: The "Execute" buttons below auto-fetch a JWT using demo credentials (admin/admin) so protected endpoints work with one click. Your production client must not hard-code credentials — authenticate through your real identity provider (OIDC, SAML, etc.), call /api/auth/token with real credentials, store the JWT, and attach it as Authorization: Bearer <token> on each request. The JWT signing logic (ML-DSA-65 via Qudo) is what you migrate; the login UX is yours.
1. How It Works 2. Architecture 3. Migrate Your System 4. Testing 5. Performance 6. Compatibility 7. Pitfalls 8. FAQ

1. How It Works

Two layers of PQC protection are applied to your REST APIs. Both use the Qudo provider — no application code rewrite needed.

🔒
Transport: Hybrid Key Exchange (NGINX)

TLS 1.3 with X25519MLKEM768 — combines classical X25519 with post-quantum ML-KEM-768 (FIPS 203). The AES-256-GCM session key is derived from both, so an attacker must break both to decrypt. One line change in your NGINX config.

Authentication: ML-DSA-65 JWT (Application)

JWT tokens signed with ML-DSA-65 (FIPS 204) instead of HMAC-SHA256 or ECDSA. Asymmetric keypair: sign with private key, verify with public key via provider.sign() and provider.verify(). The public key is published at a JWKS endpoint for clients to verify independently.

Why PQC matters: Quantum computers will break RSA, ECDSA, and ECDH. ML-KEM and ML-DSA are NIST-standardized (FIPS 203/204) replacements that are safe against both classical and quantum attacks. Migrating now protects data from "harvest now, decrypt later" attacks.

2. Architecture

Client                         Your Reverse Proxy (NGINX)       Your REST API
  |                                |                                |
  |-- TLS 1.3 ClientHello ------>|                                |
  |   (offers X25519MLKEM768)     |                                |
  |                                |                                |
  |<- TLS 1.3 ServerHello -------|                                |
  |   (selects X25519MLKEM768)    |                                |
  |                                |                                |
  |   Hybrid Key Exchange:         |                                |
  |   X25519 + ML-KEM-768         |                                |
  |   = AES-256-GCM session key   |                                |
  |                                |                                |
  |-- POST /auth/token ---------->|-- HTTP ---------------------->|
  |   (encrypted via AES-256-GCM) |                                |
  |                                |  provider.sign(payload, privKey,|
  |                                |    "ML-DSA-65") -> JWT         |
  |<- { token: "ey..." } --------|<- HTTP ----------------------- |
  |   (encrypted via AES-256-GCM) |                                |
  |                                |                                |
  |-- GET /secure --------------->|-- HTTP + JWT ---------------->|
  |   Authorization: Bearer <JWT> |  provider.verify(token, pubKey,|
  |   (encrypted via AES-256-GCM) |    "ML-DSA-65") -> true/false  |
  |<- { data } ------------------|<- HTTP ----------------------- |
🔒 Two independent PQC layers: Transport (X25519MLKEM768 via NGINX — protects data in transit) + Authentication (ML-DSA-65 JWT via Qudo provider — proves identity)

3. Migrate Your System

Three changes to make your existing REST API quantum-safe. Your API endpoints, business logic, and database stay exactly the same.

A

Change 1: Enable Hybrid PQC TLS on Your Reverse Proxy

What changes: Your NGINX/load balancer config (1 line)
What doesn't change: Your application code, endpoints, business logic

Before (nginx.conf)

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve auto;
# Default: X25519 or P-256 key exchange

After (nginx.conf)

ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:P-384;
# Hybrid PQC: X25519 + ML-KEM-768
# Fallback: X25519, P-384 for older clients
🔒 This single line change makes all API traffic quantum-safe. Clients with TLS 1.3 + ML-KEM support get PQC automatically. Older clients fall back to X25519.

Prerequisite: OpenSSL 3.6+ with Qudo provider installed. NGINX must be built against this OpenSSL.

B

Change 2: Replace JWT Signing with ML-DSA-65

What changes: Your token generation and verification logic
What doesn't change: JWT format (still header.payload.signature), your API endpoints, your auth flow

Before (Classical JWT)

// Shared secret (symmetric, quantum-vulnerable)
header: {"alg": "HS256"}
sign:   HMAC-SHA256(sharedSecret, data)
verify: HMAC-SHA256(sharedSecret, data) == sig

After (PQC JWT via Qudo)

// Asymmetric keypair (quantum-safe, FIPS 204)
header: {"alg": "ML-DSA-65"}
sign:   provider.sign(data, privKey, "ML-DSA-65")
verify: provider.verify(data, sig, pubKey, "ML-DSA-65")

Complete Java Implementation (Qudo JNI Provider)

import com.qudo.crypto.QudoCrypto;
import com.qudo.crypto.QudoKeyPair;
import java.util.Base64;

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

// Generate ML-DSA-65 keypair (do this once, store securely)
QudoKeyPair jwtKeys = provider.generateKeyPair("ML-DSA-65");
byte[] privateKey = jwtKeys.getPrivateKeyPem();
byte[] publicKey = jwtKeys.getPublicKeyPem();  // publish via /jwks

// ============================================================
// Token generation (your /auth/token endpoint)
// ============================================================
String header = Base64.getUrlEncoder().withoutPadding()
    .encodeToString("{\"alg\":\"ML-DSA-65\",\"typ\":\"JWT\"}".getBytes());
String payload = Base64.getUrlEncoder().withoutPadding()
    .encodeToString(("{\"sub\":\"" + username + "\",\"iat\":" + now + ",\"exp\":" + (now+3600) + "}").getBytes());

byte[] dataToSign = (header + "." + payload).getBytes();
byte[] signature = provider.sign(dataToSign, privateKey, "ML-DSA-65");
String token = header + "." + payload + "." +
    Base64.getUrlEncoder().withoutPadding().encodeToString(signature);

// ============================================================
// Token verification (your auth filter / middleware)
// ============================================================
String[] parts = token.split("\\.");
byte[] signedData = (parts[0] + "." + parts[1]).getBytes();
byte[] sig = Base64.getUrlDecoder().decode(parts[2]);

boolean valid = provider.verify(signedData, sig, publicKey, "ML-DSA-65");
// valid == true -> allow request, extract claims from parts[1]
This is what runs in your system. Add com.qudo:qudo-jni-crypto to your Maven/Gradle build and set -Djava.library.path to the native library location. The provider handles all PQC operations natively via OpenSSL FIPS.
C

Change 3: Install Qudo Provider

What changes: Your server's OpenSSL installation
What doesn't change: Everything else

# 1. Install Qudo provider
cd qudo-pqc-v0.1.0-macos-arm64
sudo ./install.sh

# 2. Run FIPS install tool
sudo qudo_fipsinstall -module /path/to/qudo-fips.dylib -out /path/to/qudofipsmodule.cnf

# 3. Update openssl.cnf to load the provider
# Add to provider section:
#   qudoprovider = qudo_sect
# Include the FIPS config:
#   .include qudofipsmodule.cnf

# 4. Verify
openssl list -providers
# Should show: qudoprovider - OpenSSL Qudo Provider - active

Summary: What Changes vs What Stays

ComponentChanges?What to do
NGINX configYes — 1 lineAdd ssl_ecdh_curve X25519MLKEM768:X25519:P-384
JWT signingYes — use Qudo providerprovider.sign(data, privKey, "ML-DSA-65") instead of HMAC/ECDSA
JWT verificationYes — use Qudo providerprovider.verify(data, sig, pubKey, "ML-DSA-65")
OpenSSLYes — install providerInstall Qudo Cryptographic Module, update openssl.cnf
API endpointsNoNo changes to routes, request/response formats
Business logicNoApplication code stays the same
DatabaseNoNo schema changes
Client HTTP codeNoTLS 1.3 clients get PQC automatically
Client JWT verificationYes*Consumers need ML-DSA-65 verification (Qudo provider or your JWKS endpoint)

4. Testing & Validation

Verify Hybrid Key Exchange

openssl s_client -connect localhost:8443 -tls1_3 -groups X25519MLKEM768 -brief 2>&1 | grep "Negotiated"
# Expected: Negotiated TLS1.3 group: X25519MLKEM768

Verify JWT Uses ML-DSA-65

# Decode JWT header (first part before the dot)
echo $TOKEN | cut -d. -f1 | base64 -d 2>/dev/null
# Expected: {"alg":"ML-DSA-65","typ":"JWT"}

Verify with Wireshark

# Capture TLS handshake
sudo tcpdump -i lo0 -w /tmp/pqc.pcap port 8443

# Open in Wireshark, filter: tls.handshake.type == 1
# In ClientHello -> Supported Groups: look for 0x4588 (X25519MLKEM768)

5. Performance

OperationPQCClassicalPQC SizeClassical Size
TLS Handshake (first request)~65ms~5msN/AN/A
JWT sign (provider.sign())~70ms~1ms (HMAC)~4,500 chars~300 chars
JWT verify (provider.verify())~5ms<1msN/AN/A
Subsequent TLS requests~0ms overhead~0msN/AN/A
Key takeaway: PQC overhead is only on the first TLS handshake (~65ms) and JWT signing (~70ms). Subsequent requests on the same TLS session have zero PQC overhead. Cache JWTs (valid 1 hour) to avoid repeated signing.

Optimization Tips

  • TLS session reuse: After the first handshake, the AES session key is reused — zero PQC overhead per request
  • Cache JWTs: Generate once, use for 1 hour. Don't call provider.sign() per API request
  • Connection pooling: HTTP keep-alive and connection pooling eliminate repeated TLS handshakes
  • NGINX header size: ML-DSA-65 JWTs are ~4.5KB. Add large_client_header_buffers 4 16k; to your NGINX config

6. Compatibility

ComponentMinimum VersionNotes
OpenSSL3.6.0+Required for ML-KEM, ML-DSA support
Qudo Provider1.0.0+FIPS-validated PQC provider
NGINX1.25+ (built with OpenSSL 3.6+)For X25519MLKEM768 key exchange
Java17+For the Qudo JNI provider runtime
curl8.x (with OpenSSL 3.6+)For testing PQC TLS from CLI
Chrome124+Hybrid PQC key exchange support
Firefox128+Hybrid PQC key exchange support
SafariNot yetFalls back to classical X25519 automatically
Python requestsAny (with system OpenSSL 3.6+)TLS handled by underlying OpenSSL
Node.js18+ (with system OpenSSL 3.6+)Uses system TLS for HTTPS
Backward compatible: The NGINX config lists fallback groups (X25519MLKEM768:X25519:P-384). Clients that don't support PQC key exchange automatically negotiate classical X25519. No client breaks.

7. Common Pitfalls

Pitfall: openssl pkeyutl -sign fails with "input data length too long"

Cause: ML-DSA requires -rawin flag for data larger than 64 bytes.
Fix: Always use openssl pkeyutl -sign -rawin -inkey key.pem -in data.bin -out sig.bin

Pitfall: JWT too large for default NGINX header buffer

Cause: ML-DSA-65 JWT is ~4.5KB. Default NGINX large_client_header_buffers is 8KB total.
Fix: Add large_client_header_buffers 4 16k; to your NGINX config.

Pitfall: Qudo provider not detected — openssl list -providers shows only "default"

Cause: The FIPS module config isn't loaded, or the provider can't find libqudo-pqc.
Fix: Ensure openssl.cnf includes .include qudofipsmodule.cnf and the provider section references qudo_sect. Run qudo_fipsinstall to regenerate the MAC hash if the dylib was rebuilt.

Pitfall: Browser shows classical TLS, not PQC

Cause: The browser doesn't support X25519MLKEM768, or it's disabled by policy.
How to check: Chrome DevTools > Security tab > "Key exchange group". If it shows "X25519" instead of "X25519MLKEM768", the browser fell back to classical. Chrome 124+ supports it. Safari doesn't yet and will always fall back.

Pitfall: How to roll back

Transport rollback: Change NGINX ssl_ecdh_curve back to X25519:P-384 — removes PQC, keeps TLS 1.3.
JWT rollback: Switch provider.sign() back to HMAC-SHA256 and update the alg header.
Each layer is independent. You can roll back transport without affecting JWT, and vice versa.

8. FAQ

Q: The playground auto-fetches a JWT — is that what my production client should do?

A: No. The dashboard's auto-fetch uses hard-coded admin/admin credentials so the interactive "Execute" buttons work with one click. A production client must not do this. Instead:
1. Authenticate the user through your real identity provider (OIDC, SAML, your own login form)
2. Call POST /api/auth/token with the user's real credentials
3. Store the returned JWT client-side and attach it to each request as Authorization: Bearer <token>
4. Re-authenticate when the JWT expires (default 1 hour), or use a refresh-token flow

The JWT signing / verification logic (ML-DSA-65 via Qudo provider) is what you migrate. The login UX and credential handling are yours to design. See Section 3 for the Qudo JNI code.

Q: Do I need to change my HTTP client code?

A: No. If your client supports TLS 1.3, the hybrid PQC key exchange happens automatically at the TLS layer. Your HTTP code stays the same.

Q: Is the JWT larger than a classical one?

A: Yes. ML-DSA-65 signatures are ~3.3KB vs ~64 bytes for ES256. Total JWT is ~4-5KB. This is a known tradeoff of lattice-based signatures. Use the token in headers (not cookies) and increase NGINX header buffers.

Q: How do JWT consumers verify the token?

A: Two options:
1. With Qudo provider (recommended): provider.verify(data, sig, pubKey, "ML-DSA-65") — runs locally, no network call
2. Without Qudo provider: Call your token verification endpoint (like this playground's /api/auth/verify) and let the server verify using Qudo

Q: What if a client doesn't support X25519MLKEM768?

A: The NGINX config has fallbacks: X25519MLKEM768:X25519:P-384. Clients without PQC support automatically negotiate classical X25519. No client breaks. Chrome 124+ and Firefox 128+ support hybrid PQC natively.

Q: Is this backward compatible?

A: Yes. Transport layer negotiates the best available key exchange — classical clients still work. JWT consumers need to support ML-DSA-65 verification (via Qudo provider or your verify endpoint). You can migrate transport and JWT independently.

Q: Connection fails with "certificate verify failed"

A: Trust the CA certificate: curl --cacert certs/nginx/ca.crt https://localhost:8443/api/hello. Or use -k / verify=False for testing (not production).

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