Service Info

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

Where PQC is Used

ML-DSA Signing (Application)

Sign arbitrary data with ML-DSA-44/65/87 via the SignData RPC. Keypair generated per request using Qudo provider.

ML-DSA-44 / ML-DSA-65 / ML-DSA-87
ML-DSA Verification (Application)

Verify PQC signatures via the VerifySignature RPC. Public key and signature provided by the caller.

ML-DSA (FIPS 204)
🔑
ML-KEM Key Exchange (Application)

Generate ML-KEM keypairs via the ExchangeKeys RPC for quantum-safe key encapsulation.

ML-KEM-512 / ML-KEM-768 / ML-KEM-1024
📝
ML-DSA-65 JWT Auth (gRPC Metadata)

gRPC calls authenticated with ML-DSA-65 signed JWT passed via the 'authorization' metadata key.

ML-DSA-65 (FIPS 204)
🔒
Hybrid TLS Transport

When accessed via NGINX gateway (port 8443), gRPC traffic is encrypted with X25519MLKEM768 hybrid key exchange.

X25519MLKEM768

Description

This service demonstrates PQC operations over gRPC — the same patterns you'll implement in your own gRPC services using the Qudo JNI provider.

Signing: provider.sign(data, privKey, "ML-DSA-65") inside your RPC handlers.
Key Exchange: provider.kemEncapsulate(pubKey, "ML-KEM-768") for quantum-safe shared secrets.
JWT Auth: Sign gRPC metadata tokens with provider.sign().
Transport: One NGINX line: ssl_ecdh_curve X25519MLKEM768:X25519:P-384.

Your .proto files stay the same — PQC changes are entirely in the implementation layer.

POST /api/grpc/sign Sign data via gRPC SignData RPC
🔒 Qudo provider: provider.sign(data, privKey, "ML-DSA-65"). Supports ML-DSA-44/65/87. Returns signature + public key.
POST /api/grpc/verify Verify signature via gRPC VerifySignature RPC
🔒 Qudo provider: provider.verify(data, sig, pubKey, "ML-DSA-65") -> true/false.
POST /api/grpc/key-exchange ML-KEM key encapsulation via gRPC ExchangeKeys RPC
🔒 Qudo provider: provider.kemEncapsulate(pubKey, "ML-KEM-768"). Returns encapsulated key + 32-byte shared secret.
POST /api/grpc/auth/token Generate ML-DSA-65 signed JWT for gRPC auth
🔒 Qudo provider: provider.sign(payload, privKey, "ML-DSA-65"). Pass the token as gRPC metadata: authorization = Bearer <token>.
GET /api/grpc/auth/jwks Get ML-DSA-65 public key (JWKS)
🔒 Publish this public key so other services can verify gRPC JWTs independently using provider.verify().
GET /api/grpc/algorithms List supported PQC algorithms and FIPS standards
🔒 Returns ML-KEM-512/768/1024 and ML-DSA-44/65/87 with NIST security levels.
GET /api/grpc/health gRPC service health + PQC status
🔒 Returns service status, transport (X25519MLKEM768), and authentication (ML-DSA-65 JWT) details.

gRPC Migration Reference

Migrate your gRPC services to quantum-safe cryptography. Use this playground to try PQC signing, key exchange, and JWT auth over gRPC, then apply the Qudo provider to your own gRPC services — your .proto files stay the same.

1. How It Works 2. Architecture 3. Migrate Your System 4. Testing 5. Performance 6. Compatibility 7. Pitfalls 8. FAQ

1. How It Works

Three layers of PQC protection for your gRPC services, all powered by the Qudo provider:

Application: Sign & Verify (ML-DSA)

Sign data with provider.sign(data, privKey, "ML-DSA-65") inside your RPC handlers. Verify with provider.verify(). Supports ML-DSA-44 (Level 2), ML-DSA-65 (Level 3), ML-DSA-87 (Level 5).

🔒
Application: Key Exchange (ML-KEM)

Establish shared secrets with provider.kemEncapsulate(pubKey, "ML-KEM-768"). Supports ML-KEM-512/768/1024. The 32-byte shared secret derives AES keys.

📝
Authentication: ML-DSA-65 JWT

Sign JWTs with provider.sign() and pass via gRPC metadata (authorization: Bearer <token>). Verify in your interceptor with provider.verify().

🔒
Transport: X25519MLKEM768 (NGINX)

One line in your NGINX/Envoy config: ssl_ecdh_curve X25519MLKEM768:X25519:P-384. All gRPC traffic gets hybrid PQC TLS automatically.

2. Architecture

gRPC Client                    Your Reverse Proxy (NGINX)       Your gRPC Server
  |                                |                                |
  |-- TLS 1.3 (X25519MLKEM768) -->|                                |
  |                                |                                |
  |-- gRPC SignData ------------->|-- HTTP/2 ------------------->  |
  |   metadata: authorization      |                                |
  |   = Bearer <ML-DSA-65 JWT>   |  provider.sign(data, privKey,  |
  |   payload: {data, algorithm}   |    "ML-DSA-65") -> signature   |
  |                                |                                |
  |<- {signature, publicKey} -----|<- HTTP/2 --------------------  |
  |   (encrypted AES-256-GCM)     |                                |
  |                                |                                |
  |-- gRPC ExchangeKeys --------->|-- HTTP/2 ------------------->  |
  |   payload: {algorithm}         |  provider.kemEncapsulate(      |
  |                                |    pubKey, "ML-KEM-768")       |
  |<- {encapsulatedKey, secret} --|<- HTTP/2 --------------------  |

Your .proto stays the same. PQC changes are in the implementation layer only.
🔒 PQC at every layer: Transport (X25519MLKEM768 via NGINX) + Auth (ML-DSA-65 JWT via interceptor) + Application (provider.sign/verify/kemEncapsulate)

3. Migrate Your System

Four changes to make your existing gRPC service quantum-safe. Your .proto files, client stubs, and service interfaces stay the same.

A

Change 1: PQC TLS for gRPC Transport (1 line)

What changes: Your NGINX/Envoy gRPC proxy config
What doesn't change: Your gRPC server code, your proto definitions

Before

# NGINX gRPC proxy
grpc_pass grpc://backend:9090;
ssl_ecdh_curve auto;

After

# NGINX gRPC proxy — PQC hybrid
grpc_pass grpc://backend:9090;
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:P-384;
B

Change 2: Replace Signing with ML-DSA (Qudo JNI)

What changes: Your sign/verify implementation inside gRPC RPC handlers
What doesn't change: Your proto definitions, your RPC signatures

Before (ECDSA)

Signature signer = Signature.getInstance(
    "SHA256withECDSA");
signer.initSign(ecPrivateKey);
signer.update(data);
byte[] sig = signer.sign();

After (ML-DSA via Qudo JNI)

// Generate keypair once at startup
QudoKeyPair keys = provider.generateKeyPair(
    "ML-DSA-65");
// Sign in your RPC handler
byte[] sig = provider.sign(data,
    keys.getPrivateKeyPem(), "ML-DSA-65");
// Verify
boolean ok = provider.verify(data, sig,
    keys.getPublicKeyPem(), "ML-DSA-65");
C

Change 3: Replace Key Exchange with ML-KEM (Qudo JNI)

What changes: Your key agreement implementation
What doesn't change: The shared secret size (still 32 bytes)

Before (ECDH)

KeyAgreement ka = KeyAgreement.getInstance(
    "ECDH");
ka.init(myPrivateKey);
ka.doPhase(peerPublicKey, true);
byte[] secret = ka.generateSecret();

After (ML-KEM via Qudo JNI)

// Encapsulate (sender side)
QudoKemResult encap = provider.kemEncapsulate(
    recipientPubKey, "ML-KEM-768");
byte[] sharedSecret = encap.getSharedSecret();
byte[] ciphertext = encap.getCiphertext();

// Decapsulate (recipient side)
byte[] secret = provider.kemDecapsulate(
    ciphertext, privKey, "ML-KEM-768");
// same 32-byte shared secret
D

Change 4: PQC JWT for gRPC Auth (Qudo JNI)

What changes: JWT signing in your gRPC interceptor
What doesn't change: The metadata key (authorization) and Bearer token format

Before

// gRPC metadata
headers.put("authorization",
  "Bearer " + hmacJwt);
// JWT alg: HS256

After

// Sign JWT with Qudo provider
byte[] sig = provider.sign(headerPayload,
    privKey, "ML-DSA-65");
String jwt = headerPayload + "." +
    Base64.encode(sig);
// gRPC metadata (same key)
headers.put("authorization",
  "Bearer " + jwt);
// JWT alg: ML-DSA-65

Summary: What Changes vs What Stays

ComponentChanges?What to do
.proto filesNoSame proto3 definitions — RPCs, messages unchanged
gRPC transportYes — 1 lineAdd ssl_ecdh_curve X25519MLKEM768 to NGINX/Envoy
Signing logicYes — use Qudoprovider.sign(data, privKey, "ML-DSA-65")
Key exchangeYes — use Qudoprovider.kemEncapsulate(pubKey, "ML-KEM-768")
Auth interceptorYes — use QudoSign JWT with provider.sign() instead of HMAC
Client stubsNoRegenerated from same .proto — no changes
Business logicNoYour RPC implementations stay the same
Key insight: gRPC separates protocol (proto) from implementation. PQC changes are entirely at the implementation layer — use the Qudo JNI provider in your RPC handlers. Your .proto files, client stubs, and service interfaces don't change.

4. Testing

Test gRPC Sign + Verify (end-to-end)

# Sign
SIGN=$(curl -sk -X POST https://localhost:8443/api/grpc/sign \
  -H "Content-Type: application/json" \
  -d '{"data":"SGVsbG8gUFFD","algorithm":"ML-DSA-65"}')
echo "$SIGN" | python3 -c "import sys,json; d=json.load(sys.stdin); print('status:', d['status'], '| algo:', d['algorithm'])"

# Extract and verify
SIG=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['signature'])")
PUB=$(echo "$SIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['publicKey'])")
curl -sk -X POST https://localhost:8443/api/grpc/verify \
  -H "Content-Type: application/json" \
  -d "{\"data\":\"SGVsbG8gUFFD\",\"signature\":\"$SIG\",\"publicKey\":\"$PUB\",\"algorithm\":\"ML-DSA-65\"}"
# Expected: {"status":"success","valid":true}

Test ML-KEM Key Exchange

curl -sk -X POST https://localhost:8443/api/grpc/key-exchange \
  -H "Content-Type: application/json" \
  -d '{"algorithm":"ML-KEM-768"}'
# Expected: {"status":"success","algorithm":"ML-KEM-768","sharedSecretLength":32}

Test PQC JWT Auth

# Get ML-DSA-65 JWT
TOKEN=$(curl -sk -X POST https://localhost:8443/api/grpc/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"test"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# Decode JWT header to confirm ML-DSA-65
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null
# Expected: {"alg":"ML-DSA-65","typ":"JWT","use":"grpc"}

Test Native gRPC (with grpcurl)

# Install: brew install grpcurl
grpcurl -plaintext localhost:9090 list
# Expected: pqc.PqcService

grpcurl -plaintext -d '{"category":"ALL"}' \
  localhost:9090 pqc.PqcService/GetAlgorithmInfo

5. Performance

Measured on Apple M-series, OpenSSL 3.6.1, Qudo FIPS v1.0.0. All operations include keypair generation + crypto operation.

OperationAlgorithmLatencyOutput Sizevs Classical
gRPC SignML-DSA-65~120ms3,309 bytesECDSA: ~5ms / 64 bytes
gRPC SignML-DSA-44~118ms2,420 bytesSmaller, faster than ML-DSA-65
gRPC Key ExchangeML-KEM-768~80ms32 bytes (shared secret)ECDH: ~2ms / 32 bytes
JWT Token GenerationML-DSA-65~70ms~4,500 charsHMAC JWT: ~1ms / ~300 chars
TLS HandshakeX25519MLKEM768~65msN/AX25519: ~5ms
Key takeaway: PQC adds ~60-120ms per crypto operation and produces larger signatures (~3KB vs ~64 bytes). This overhead is acceptable for most server-to-server and API workloads. For latency-sensitive paths, cache JWT tokens (they're valid for 1 hour) and reuse TLS sessions.

Optimization Tips

  • Cache keypairs: Generate ML-DSA keypair once at startup, not per-request (reduces sign latency to ~10ms)
  • Cache JWTs: Token is valid for 1 hour — don't generate a new one per gRPC call
  • Use ML-DSA-44 for high-throughput, lower-security workloads (smaller signatures, faster)
  • TLS session reuse: After the first handshake, subsequent requests reuse the session key (no PQC overhead)
  • Async signing: Offload PQC signing to a thread pool to avoid blocking gRPC request threads

6. Compatibility

ComponentMinimum VersionNotes
OpenSSL3.6.0+Required for ML-KEM, ML-DSA, SLH-DSA support
Qudo Provider1.0.0+FIPS-validated PQC provider for OpenSSL
NGINX1.25+ (built with OpenSSL 3.6+)For hybrid TLS key exchange (X25519MLKEM768)
Java17+gRPC server runtime. PQC via Qudo JNI provider
gRPC Java1.60+Standard gRPC — no PQC-specific version required
Proto / protoc3.25+Standard proto3 — no changes for PQC
grpcurlAnyFor testing native gRPC calls
Chrome124+Supports hybrid PQC key exchange in TLS
Firefox128+Supports hybrid PQC key exchange in TLS
macOS12+For openssl CLI. Qudo provider is arm64 and x86_64
LinuxUbuntu 22.04+ / RHEL 9+OpenSSL 3.x in default repos
Integration: PQC operations use the Qudo JNI provider (com.qudo:qudo-jni-crypto), which calls OpenSSL natively via JNI. Add the dependency to your build and set -Djava.library.path to the native library location.

7. Common Pitfalls

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

Cause: ML-DSA requires the -rawin flag for data larger than 64 bytes. Without it, OpenSSL treats the input as a pre-hashed digest.
Fix: Always use openssl pkeyutl -sign -rawin -inkey key.pem -in data.bin -out sig.bin

Pitfall: SLH-DSA doesn't work with pkeyutl

Cause: The Qudo provider currently supports SLH-DSA for certificate signing (openssl req) but not for arbitrary data signing via pkeyutl.
Workaround: Use ML-DSA-87 (highest security level) for data signing. Use SLH-DSA only for certificate issuance.

Pitfall: JWT token is ~4.5KB, hits header size limits

Cause: ML-DSA-65 signatures are 3,309 bytes vs 64 bytes for ECDSA. The base64-encoded JWT is ~4.5KB.
Fix: Increase NGINX large_client_header_buffers to 4 16k. For gRPC, increase max metadata size: grpc.max_metadata_size=16384.

Pitfall: Qudo provider not loading — "algorithm not found"

Cause: OpenSSL can't find the Qudo provider dylib or the FIPS config isn't loaded.
Fix: Run openssl list -providers and verify qudoprovider is active. If not, check openssl.cnf includes the FIPS module config and the provider section references qudo_sect.

Pitfall: TLS handshake falls back to X25519 (no PQC)

Cause: The client doesn't support X25519MLKEM768, or NGINX's OpenSSL wasn't built with the Qudo provider.
Check: Run openssl s_client -connect host:port -groups X25519MLKEM768 -brief and look for Negotiated TLS1.3 group: X25519MLKEM768. If it shows X25519, the client fell back.

Pitfall: gRPC call hangs or times out with PQC signing

Cause: Each PQC sign operation takes ~120ms (vs ~5ms for ECDSA). If you sign per-request on the gRPC thread, it blocks.
Fix: Generate the keypair once at startup (not per-request). Cache signed JWTs. Use async stubs for non-blocking calls.

8. FAQ

Q: Do I need to change my .proto files to use PQC?

A: No. Your proto definitions, message types, and RPC signatures stay exactly the same. PQC is applied at the implementation layer (how you sign/verify/exchange keys inside your RPC handlers) and the transport layer (TLS config). Existing client stubs work without regeneration.

Q: Can I use PQC gRPC with non-Java clients (Python, Go, C++)?

A: Yes. The gRPC protocol is language-agnostic. Any gRPC client can call the PQC service. For the transport layer, the client just needs TLS 1.3 support (all modern gRPC libraries have this). For application-layer PQC (signing in your client), you need OpenSSL 3.6+ with the Qudo provider on the client machine.

Q: What's the overhead of PQC on gRPC throughput?

A: Signing adds ~120ms per operation. For a service doing 100 signs/sec, this requires a thread pool. The TLS overhead is only on the first handshake (~65ms) — subsequent requests on the same connection have zero PQC overhead thanks to session reuse. Key exchange (ML-KEM) adds ~80ms per operation.

Q: How do I verify gRPC PQC JWTs from a different service?

A: Two options:
1. With Qudo provider: provider.verify(data, sig, pubKey, "ML-DSA-65") — runs locally, no network call
2. Via JWKS endpoint: Fetch the public key from the signing service's JWKS endpoint, then verify locally with provider.verify()
JWT format is standard (header.payload.signature) — only the algorithm changes from HS256 to ML-DSA-65.

Q: Can I gradually migrate? Some RPCs with PQC, some without?

A: Yes. PQC is applied per-RPC in your implementation, not globally. You can migrate one RPC at a time: update SignData to use ML-DSA while keeping other RPCs on ECDSA. The transport layer (TLS) is all-or-nothing per connection, but that's transparent to the application.

Q: How do I roll back if PQC breaks something?

A: Rollback plan by layer:
Transport: Remove X25519MLKEM768 from NGINX ssl_ecdh_curve — falls back to X25519.
JWT: Switch provider.sign() back to HMAC — just change the token generation code.
Signing: Replace provider.sign(..., "ML-DSA-65") with your previous ECDSA code in the RPC handler.
Each layer is independent — roll back one without affecting the others.

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