Note: ML-KEM encryption/decryption utilities are available in the codebase (
src/encryption/mlkem-encryption.service.ts) but the API endpoints are currently not exposed. SIWE authentication (src/auth/siwe.service.ts,src/auth/siwe.guard.ts) is also available for protecting future endpoints. This documentation is maintained for reference and future use.
ZK API includes ML-KEM-1024 (Module-Lattice-Based Key-Encapsulation Mechanism) for post-quantum cryptographic security. ML-KEM is standardized by NIST as FIPS 203 and provides security against both classical and quantum computer attacks.
- Why ML-KEM?
- Architecture
- Multi-Recipient Encryption
- Security Properties
- API Reference
- Client Integration
- TEE Integration
- Testing
- Migration Guide
- FAQ
Current encryption standards like RSA and ECDH are vulnerable to quantum computers using Shor's algorithm. While cryptographically-relevant quantum computers (CRQC) are estimated to be 10-15 years away, encrypted data harvested today could be decrypted in the future (harvest-now-decrypt-later attacks).
✅ Post-Quantum Secure: Resistant to both classical and quantum attacks ✅ NIST Standardized: Official FIPS 203 standard (2024) ✅ Efficient: ~1-2ms encryption/decryption on modern hardware ✅ Reasonable Size: 1568-byte public keys, 3168-byte private keys ✅ Hybrid Compatible: Can be combined with classical crypto
Client Server
| |
| Generate ECDH keypair |
| Compute shared secret ━━━━━>| ECDH (vulnerable!)
| Encrypt with AES |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━>|
| Decrypt with ECDH
| (Quantum computer can break this!)
Client Server (TEE)
| |
| Get ML-KEM public key <━━━━| Sealed in TEE hardware
| Encapsulate → ciphertext |
| Encrypt with AES |
| ━━━━━━━━━━━━━━━━━━━━━━━━━━━>|
| Decapsulate → shared secret
| Decrypt with AES
| (Quantum-resistant!)
ZK API implements multi-recipient ML-KEM encryption, allowing multiple parties to independently decrypt the same data.
1. Client generates random AES-256 key (K)
2. Client encrypts data with K using AES-256-GCM
3. For each recipient (client, server, etc.):
a. ML-KEM encapsulate → shared secret (SS)
b. XOR-encrypt K with SS → encrypted_key
c. Store: recipient_entry = {publicKey, ciphertext + encrypted_key}
4. Final payload = {recipients[], encryptedData, iv, authTag}
✅ Privacy-First: Client can decrypt locally without server ✅ Flexible Access: Server can decrypt for operations when needed ✅ Single Storage: One encrypted blob, multiple recipients ✅ Independent Decryption: No coordination needed between recipients
// Client side (using w3pk)
const encrypted = await w3pk.mlkemEncrypt(
'苟全性命於亂世,不求聞達於諸侯。',
[serverPublicKey] // Server as recipient
);
// Client is automatically added as first recipient
// Client can decrypt locally (NO SERVER!)
const plaintext1 = await w3pk.mlkemDecrypt(encrypted);
// Server can decrypt for operations (with SIWE auth)
const plaintext2 = await fetch('/secret/access/slot123', {
headers: { 'x-siwe-message': '...', 'x-siwe-signature': '...' }
});| Component | Algorithm | Security Level | Quantum Security |
|---|---|---|---|
| Key Encapsulation | ML-KEM-1024 | NIST Level 5 | 256-bit |
| Symmetric Encryption | AES-256-GCM | 256-bit classical | 128-bit quantum |
| Key Derivation | HKDF-SHA256 | 256-bit | 128-bit |
| Authentication | SIWE | Ethereum addresses | N/A |
| Attack Vector | Mitigation |
|---|---|
| Quantum Computer (Shor's) | ✅ ML-KEM immune to Shor's algorithm |
| Harvest-Now-Decrypt-Later | ✅ Data encrypted with ML-KEM at rest |
| Man-in-the-Middle | ✅ TEE attestation verification required |
| Admin Access | ✅ Private key sealed in TEE hardware |
| Code Tampering | ✅ Attestation measurement verifies code integrity |
| Replay Attacks | ✅ SIWE nonces prevent replay |
| Side-Channel |
ML-KEM-1024:
Public Key: 1,568 bytes
Private Key: 3,168 bytes
Ciphertext: 1,568 bytes
Shared Secret: 32 bytes
Per-Secret Overhead:
Per Recipient: ~1,600 bytes (1,568 KEM + 32 encrypted AES key)
Shared Data: ~28 bytes (12 IV + 16 auth tag)
Example: 2 recipients + 100 bytes data
Total: ~3,328 bytes (vs ~145 bytes with ECDH)
Note: The following endpoints are currently not available in the API. This section is maintained for reference.
The ML-KEM encryption endpoints (/secret/attestation, /secret/store, /secret/access) have been removed from the API surface.
Available for Future Implementation:
src/encryption/mlkem-encryption.service.ts- Complete ML-KEM-1024 encryption/decryption servicesrc/auth/siwe.service.ts- SIWE authentication servicesrc/auth/siwe.guard.ts- Guard for protecting endpoints with SIWEsrc/auth/auth.controller.ts- Nonce generation endpoint
These can be re-enabled by creating new controllers that import and use these existing services.
class MlKemEncryptionService {
// Get server's public key for encryption
getPublicKey(): string | null;
// Check if encryption is available
isAvailable(): boolean;
// Decrypt multi-recipient payload
decryptMultiRecipient(payload: MultiRecipientEncryptedPayload): string;
// Legacy single-recipient decryption (deprecated)
decrypt(payload: EncryptedPayload): string;
// For testing only (client should encrypt)
encrypt(plaintext: string): EncryptedPayload;
}interface RecipientEntry {
publicKey: string; // Base64 ML-KEM-1024 public key (1568 bytes)
ciphertext: string; // Base64: KEM ciphertext (1568) + encrypted AES key (32)
}
interface MultiRecipientEncryptedPayload {
recipients: RecipientEntry[]; // Array of recipients
encryptedData: string; // Base64 AES-256-GCM encrypted data
iv: string; // Base64 IV (12 bytes)
authTag: string; // Base64 auth tag (16 bytes)
}w3pk provides seamless ML-KEM encryption with deterministic key derivation from Ethereum wallets.
import { createWeb3Passkey } from 'w3pk';
// 1. Initialize w3pk
const w3pk = createWeb3Passkey();
await w3pk.login();
// 2. Get server attestation
const attestation = await fetch('https://vault.example.com/secret/attestation')
.then(r => r.json());
// 3. CRITICAL: Verify attestation (future implementation)
// const isValid = await verifyAttestation(attestation, expectedMeasurement);
// if (!isValid) throw new Error('Invalid attestation!');
// 4. Encrypt for yourself + server
const encrypted = await w3pk.mlkemEncrypt(
'my secret data',
[attestation.mlkemPublicKey] // Server as recipient
);
// 5. Store encrypted data
const { slot } = await fetch('https://vault.example.com/secret/store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: encrypted,
publicAddresses: [await w3pk.getAddress('STANDARD')]
})
}).then(r => r.json());
// 6a. Client-side decryption (PRIVACY-FIRST!)
const plaintext = await w3pk.mlkemDecrypt(encrypted);
// ✅ No server involved, complete privacy
// 6b. OR: Server-side decryption (for operations)
const siweMessage = await w3pk.signInWithEthereum(domain, { uri: origin });
const { secret } = await fetch(`https://vault.example.com/secret/access/${slot}`, {
headers: {
'x-siwe-message': Buffer.from(siweMessage.message).toString('base64'),
'x-siwe-signature': siweMessage.signature
}
}).then(r => r.json());For custom implementations:
import { deriveMLKemKeypair, mlkemEncrypt, mlkemDecrypt } from 'w3pk';
// Derive keypair from Ethereum private key
const keypair = await deriveMLKemKeypair(ethPrivateKey, 'my-app');
// Encrypt for multiple recipients
const encrypted = await mlkemEncrypt(plaintext, [
publicKey1,
publicKey2,
publicKey3
]);
// Decrypt
const plaintext = await mlkemDecrypt(
encrypted,
keypair.privateKey,
keypair.publicKey // Optional: speeds up recipient lookup
);// src/encryption/mlkem-encryption.service.ts
async onModuleInit() {
this.mlkem = await createMlKem1024();
// Load from environment (local) or generate in TEE (production)
if (process.env.ADMIN_MLKEM_PRIVATE_KEY) {
// Development: load from .env
this.privateKey = Buffer.from(
process.env.ADMIN_MLKEM_PRIVATE_KEY,
'base64'
);
} else {
// Production: generate and seal in TEE
const [publicKey, privateKey] = this.mlkem.generateKeyPair();
this.publicKey = publicKey;
this.privateKey = privateKey;
// Seal private key in TEE hardware (Phala specific)
await this.sealPrivateKey(privateKey);
}
}async getAttestation(): Promise<AttestationResponseDto> {
const attestation = await this.teePlatformService.generateAttestationReport();
return {
platform: attestation.platform, // 'phala', 'amd-sev-snp', etc.
report: attestation.report, // TEE signature
measurement: attestation.measurement, // Code hash
timestamp: attestation.timestamp,
mlkemPublicKey: this.getPublicKey(), // For client encryption
};
}// Example Phala deployment configuration
import { PinkEnvironment } from '@phala/pink-env';
// TEE generates and seals ML-KEM keys
const keys = await generateAndSealMLKemKeys();
// Export public key in attestation
export function getAttestation() {
return {
platform: 'phala',
report: PinkEnvironment.attestation(),
measurement: PinkEnvironment.codeHash(),
mlkemPublicKey: keys.publicKey,
};
}
// Decrypt secrets in TEE
export function decryptSecret(encryptedPayload) {
const privateKey = unsealPrivateKey(); // From TEE storage
return mlkem.decryptMultiRecipient(encryptedPayload, privateKey);
}This section explains how to test the ML-KEM multi-recipient encryption implementation both locally and on Phala Network.
Generate quantum-resistant keys for the zk-api server:
cd /Users/ju/zk-api
pnpm ts-node scripts/generate-admin-keypair.tsThis will output:
✅ Keypair generated successfully!
📋 Add these to your .env.local file:
ADMIN_MLKEM_PUBLIC_KEY=ZLVMNpXCmEp7vhcylKzGXcx8wVEcaQKI...
ADMIN_MLKEM_PRIVATE_KEY=82eI7sQLvGEut7Z4RvaF+Ju60Esj/AW/...
IMPORTANT: Keep the private key secret! In production TEE, this will be sealed in hardware.
Create or update .env.local:
# ML-KEM-1024 Admin Keypair (quantum-resistant encryption)
ADMIN_MLKEM_PUBLIC_KEY=<paste_public_key_here>
ADMIN_MLKEM_PRIVATE_KEY=<paste_private_key_here>pnpm start:devThe server should log:
✅ ML-KEM-1024 keys loaded successfully
Public key: ZLVMNpXCmEp7vhcylKzGXcx8wVEcaQKI... (1568 bytes)
Server will be available at http://localhost:3000
Use the included test script to verify the basic flow:
pnpm ts-node scripts/test-mlkem-flow.tsExpected output:
🧪 Testing ML-KEM encryption flow
1️⃣ Generating TEE keypair...
✅ Public key: ZLVMNpXCmEp7... (1568 bytes)
2️⃣ Client: Getting TEE attestation...
✅ Received TEE public key
3️⃣ Client: Encrypting secret for TEE...
📦 Encapsulating with TEE public key...
🔐 Encrypting with AES-256-GCM...
✅ Encrypted payload ready
4️⃣ Client: Storing encrypted secret...
✅ Assigned slot: 05919c62d6a408cb...
5️⃣ Server: Decrypting secret...
🔓 Decrypting with AES-256-GCM...
✅ Decrypted: "This is my quantum-safe secret! 🔐"
6️⃣ Verification:
✅ SUCCESS! Plaintext matches decrypted text
✅ ML-KEM encryption/decryption working correctly
🎉 All tests passed!
Test the full flow including SIWE authentication and server-side decryption:
pnpm ts-node scripts/test-store-and-access.tsExpected output:
🧪 Testing ML-KEM store and access flow with SIWE authentication
🔗 Server: http://localhost:3000
👤 Test Wallet: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1️⃣ Getting server attestation...
✅ Platform: none
✅ ML-KEM Public Key: k3VARNFcS4hWl6AfR0DMylysiyuCqgwO...
⚠️ Measurement: MOCK_MEASUREMENT...
2️⃣ Generating client ML-KEM keypair...
✅ Generated (1568 bytes)
3️⃣ Encrypting secret for client + server...
📝 Plaintext: "🔐 My quantum-safe secret data! Testing store+access flow."
✅ Encrypted with 2 recipients
4️⃣ Storing encrypted secret on server...
✅ Stored in slot: c62d08e957b68109...
5️⃣ Getting nonce for SIWE authentication...
✅ Nonce: 4fhgr4TAfosNzZI3S
6️⃣ Creating and signing SIWE message...
✅ SIWE message signed
Domain: localhost
Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Signature: 0xf8db734904dfd0d0...
7️⃣ Accessing secret (server-side decryption with SIWE)...
📝 Server decrypted: "🔐 My quantum-safe secret data! Testing store+access flow."
✅ Match: true
📊 Test Summary:
✅ Server attestation retrieved
✅ Multi-recipient encryption successful
✅ Secret stored on server
✅ SIWE authentication successful
✅ Server-side decryption working
✅ Plaintext matches (end-to-end verified)
🎉 All tests passed! Complete store+access flow working correctly.
📋 What was tested:
• ML-KEM-1024 quantum-resistant encryption
• Multi-recipient encryption (client + server)
• SIWE authentication with ethers wallet
• Server-side ML-KEM decryption in TEE
• End-to-end data integrity
This script tests:
- ✅ Multi-recipient encryption: Client + server can both decrypt
- ✅ Store endpoint: Secret stored with access control
- ✅ SIWE authentication: Wallet-based authentication flow
- ✅ Server-side decryption: ML-KEM decryption in TEE
- ✅ End-to-end verification: Plaintext matches original
Create a test client using w3pk (in a separate directory or in w3pk repository):
// test-zk-api-mlkem.ts
import { createWeb3Passkey, mlkemEncrypt } from 'w3pk';
import { Wallet } from 'ethers';
async function testZkApiMLKEM() {
// 1. Get server's attestation (includes ML-KEM public key)
const attestation = await fetch('http://localhost:3000/secret/attestation')
.then(r => r.json());
console.log('📋 Server Attestation:');
console.log(` Platform: ${attestation.platform}`);
console.log(` ML-KEM Public Key: ${attestation.mlkemPublicKey.substring(0, 32)}...`);
// CRITICAL: In production, verify attestation here!
// For local testing, we'll skip verification
// 2. Create w3pk instance and login
const w3pk = createWeb3Passkey();
await w3pk.register({ username: 'test-user' });
await w3pk.login();
console.log(`\n👤 Client Address: ${await w3pk.getAddress('STANDARD')}`);
// 3. Encrypt secret for yourself + server
const plaintext = '苟全性命於亂世,不求聞達於諸侯。';
console.log(`\n📝 Plaintext: "${plaintext}"`);
const encrypted = await w3pk.mlkemEncrypt(
plaintext,
[attestation.mlkemPublicKey] // Server as recipient
);
console.log(`\n🔐 Encrypted Payload:`);
console.log(` Recipients: ${encrypted.recipients.length}`);
console.log(` Encrypted Data: ${encrypted.encryptedData.substring(0, 32)}...`);
// 4. Store encrypted secret on server
const storeResponse = await fetch('http://localhost:3000/secret/store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: encrypted,
publicAddresses: [await w3pk.getAddress('STANDARD')]
})
});
const { slot } = await storeResponse.json();
console.log(`\n✅ Stored in slot: ${slot}`);
// 5a. Client-side decryption (privacy-first!)
console.log(`\n🔓 Client-side decryption (no server involved):`);
const clientDecrypted = await w3pk.mlkemDecrypt(encrypted);
console.log(` Decrypted: "${clientDecrypted}"`);
console.log(` ✅ Match: ${clientDecrypted === plaintext}`);
// 5b. Server-side decryption (requires SIWE auth)
console.log(`\n🔓 Server-side decryption (with SIWE auth):`);
// Generate SIWE message
const domain = 'localhost:3000';
const origin = 'http://localhost:3000';
const siweMessage = await w3pk.signInWithEthereum(domain, {
uri: origin,
statement: 'Access encrypted secret',
});
// Access secret via server
const accessResponse = await fetch(`http://localhost:3000/secret/access/${slot}`, {
headers: {
'x-siwe-message': Buffer.from(siweMessage.message).toString('base64'),
'x-siwe-signature': siweMessage.signature,
}
});
const { secret: serverDecrypted } = await accessResponse.json();
console.log(` Decrypted: "${serverDecrypted}"`);
console.log(` ✅ Match: ${serverDecrypted === plaintext}`);
console.log(`\n🎉 All tests passed! Multi-recipient ML-KEM working correctly.`);
}
testZkApiMLKEM().catch(err => {
console.error('❌ Test failed:', err);
process.exit(1);
});Run the test:
pnpm ts-node test-zk-api-mlkem.tscurl http://localhost:3000/secret/attestation | jqExpected response:
{
"platform": "none",
"report": "...",
"measurement": "...",
"timestamp": "2026-03-22T...",
"mlkemPublicKey": "ZLVMNpXCmEp7vhcylKzGXcx8wVEcaQKI..."
}You'll need to encrypt client-side first using w3pk, then:
curl -X POST http://localhost:3000/secret/store \
-H "Content-Type: application/json" \
-d '{
"secret": {
"recipients": [
{
"publicKey": "client_public_key_base64...",
"ciphertext": "client_ciphertext_base64..."
},
{
"publicKey": "server_public_key_base64...",
"ciphertext": "server_ciphertext_base64..."
}
],
"encryptedData": "encrypted_data_base64...",
"iv": "iv_base64...",
"authTag": "auth_tag_base64..."
},
"publicAddresses": ["0xYourEthereumAddress..."]
}'# Run unit tests
pnpm test
# Run e2e tests
pnpm test:e2e- Phala Account: Register at Phala Cloud
- Phala CLI: Install with
npm install -g @phala/cli - Docker Hub: For hosting container images
- ML-KEM Keys: Generated and added to
.env.prod
Follow the complete deployment process:
# 1. Build the application
pnpm build
# 2. Build and push Docker image for AMD64
docker buildx build --platform linux/amd64 -t YOUR_USERNAME/zk-api:latest --no-cache --push .
# 3. Deploy to Phala Cloud
phala deploy --interactive
# Select docker-compose.yml and .env.prod when prompted
# Choose tdx.small instance type for TEE supportSee PHALA_CONFIG.md for detailed instructions.
Wait for deployment to complete:
phala cvms list
# Wait for status: runningGet your endpoint URL (format: https://<APP_ID>-3000.<CLUSTER>.phala.network)
Run the complete store+access test against your Phala deployment:
ZK_API_URL=https://your-app-id-3000.dstack-pha-prod9.phala.network pnpm ts-node scripts/test-store-and-access.tsExpected output:
🧪 Testing ML-KEM store and access flow with SIWE authentication
🔗 Server: https://71ff0e26187be84e21c1f2553dd9dee39e8f7018-3000.dstack-pha-prod9.phala.network
👤 Test Wallet: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1️⃣ Getting server attestation...
✅ Platform: intel-tdx
✅ ML-KEM Public Key: k3VARNFcS4hWl6AfR0DMylysiyuCqgwO...
⚠️ Measurement: 000000000000000000000000000000...
2️⃣ Generating client ML-KEM keypair...
✅ Generated (1568 bytes)
3️⃣ Encrypting secret for client + server...
📝 Plaintext: "🔐 My quantum-safe secret data! Testing store+access flow."
✅ Encrypted with 2 recipients
4️⃣ Storing encrypted secret on server...
✅ Stored in slot: c62d08e957b68109924a63b3879e31caf3f9f9ccd0ab9b42befe082c645eae99
5️⃣ Getting nonce for SIWE authentication...
✅ Nonce: 4fhgr4TAfosNzZI3S
6️⃣ Creating and signing SIWE message...
✅ SIWE message signed
Domain: 71ff0e26187be84e21c1f2553dd9dee39e8f7018-3000.dstack-pha-prod9.phala.network
Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Signature: 0xf8db734904dfd0d0b294587ab09901...
7️⃣ Accessing secret (server-side decryption with SIWE)...
📝 Server decrypted: "🔐 My quantum-safe secret data! Testing store+access flow."
✅ Match: true
📊 Test Summary:
✅ Server attestation retrieved
✅ Multi-recipient encryption successful
✅ Secret stored on server
✅ SIWE authentication successful
✅ Server-side decryption working
✅ Plaintext matches (end-to-end verified)
🎉 All tests passed! Complete store+access flow working correctly.
📋 What was tested:
• ML-KEM-1024 quantum-resistant encryption
• Multi-recipient encryption (client + server)
• SIWE authentication with ethers wallet
• Server-side ML-KEM decryption in TEE
• End-to-end data integrity
Key differences from local testing:
- ✅
platform: "intel-tdx"(not "none") - Real TEE environment - ✅ Hardware-backed ML-KEM private key (sealed in TEE)
- ✅ Cryptographic attestation from Intel TDX
- ✅ Production-grade security guarantees
When deployed on Phala, the attestation will include:
{
"platform": "phala",
"report": "base64_tee_signature_from_phala...",
"measurement": "sha256_hash_of_code...",
"timestamp": "2026-03-22T...",
"mlkemPublicKey": "server_public_key_from_tee...",
"publicKey": "0xPhalaContractAddress..."
}CRITICAL: Clients MUST verify:
- ✅
measurementmatches published source code hash - ✅
reportsignature is valid (from Phala Network) - ✅
platformis "phala"
import { verifyPhalaAttestation } from 'w3pk'; // Future implementation
const attestation = await fetch('https://your-phala-endpoint/secret/attestation')
.then(r => r.json());
// Verify attestation before trusting public key
const expectedMeasurement = 'sha256_of_published_source_code';
const isValid = await verifyPhalaAttestation(attestation, expectedMeasurement);
if (!isValid) {
throw new Error('❌ TEE attestation verification failed! Do not proceed.');
}
// Now safe to encrypt with mlkemPublicKey
const encrypted = await w3pk.mlkemEncrypt(secret, [attestation.mlkemPublicKey]);The complete production flow on Phala:
┌─────────────────────────────────────────────────────────────┐
│ 1. Client gets attestation from Phala TEE │
│ GET https://your-app.phala.network/secret/attestation │
│ Response: { platform: "phala", mlkemPublicKey, ... } │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Client verifies attestation │
│ ✅ Check measurement matches published source code │
│ ✅ Verify Phala signature on report │
│ ✅ Confirm TEE platform is genuine │
│ ❌ REJECT if verification fails │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Client encrypts with w3pk │
│ const encrypted = await w3pk.mlkemEncrypt( │
│ secret, │
│ [attestation.mlkemPublicKey] // Server in TEE │
│ ); │
│ // Client is auto-added as first recipient │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Client stores encrypted payload │
│ POST /secret/store │
│ Body: { secret: encrypted, publicAddresses: [...] } │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5a. Client decrypts locally (privacy-first!) │
│ const plaintext = await w3pk.mlkemDecrypt(encrypted); │
│ // NO SERVER INVOLVED - complete privacy │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5b. OR: Server decrypts for operations │
│ GET /secret/access/:slot (with SIWE auth) │
│ Server uses sealed private key to decrypt │
│ Returns plaintext for internal operations │
└─────────────────────────────────────────────────────────────┘
- Generate ML-KEM keypair with
scripts/generate-admin-keypair.ts - Configure
.envwith generated keys - Start server and verify keys loaded
- Run
scripts/test-mlkem-flow.tssuccessfully - Run
scripts/test-store-and-access.tssuccessfully - Test with w3pk client (if available)
- Verify multi-recipient encryption works
- Verify both client-side and server-side decryption
- Test SIWE authentication flow
- Test error cases (wrong key, invalid payload, etc.)
- Build and push Docker image to Docker Hub
- Deploy to Phala Cloud with
phala deploy --interactive - Verify deployment status with
phala cvms list - Get attestation and verify
platform: "intel-tdx" - Verify ML-KEM keys are loaded in TEE
- Run
scripts/test-store-and-access.tsagainst Phala endpoint - Verify complete store+access flow works
- Test SIWE authentication with real wallet
- Verify attestation signature (Intel TDX-specific)
- Verify measurement matches published code
- Test with w3pk client integration
- Test with multiple concurrent clients
- Monitor instance costs and performance
- Private key is in plaintext
.envfile - No hardware isolation
- No attestation verification
- Admin can read secrets
Use local testing ONLY for development and integration testing.
✅ Security Properties:
- Private key sealed in TEE hardware (cannot be extracted)
- Attestation cryptographically proves code integrity
- Admin cannot access secrets (even with root access)
- Quantum-resistant encryption (ML-KEM-1024)
- Multi-recipient design (client can decrypt independently)
CRITICAL Client Responsibilities:
- ALWAYS verify attestation before encrypting
- Check measurement matches published source code hash
- Verify signature from Phala Network
- Reject invalid attestations (do not proceed)
Solution: Run pnpm ts-node scripts/generate-admin-keypair.ts and add keys to .env
Cause: Client encrypted with wrong public key or corrupted payload
Solution: Verify client is using mlkemPublicKey from /secret/attestation
Cause: Client didn't include server as recipient
Solution: Pass server's public key to w3pk.mlkemEncrypt(secret, [serverPublicKey])
Possible causes:
- Wrong private key on server
- Corrupted encrypted payload
- Client used wrong encryption algorithm
Debug: Check server logs for detailed error message
- ✅ ML-KEM keypair generation
- ✅ Multi-recipient encryption/decryption
- ✅ Client-side decryption (w3pk)
- ✅ Server-side decryption (TEE)
- ✅ SIWE authentication
- ✅ Invalid payload handling
- ✅ Error cases
Old format (deprecated):
{
ciphertext: "base64...", // Single ML-KEM ciphertext
encryptedData: "base64...",
iv: "base64...",
authTag: "base64..."
}New format (multi-recipient):
{
recipients: [
{ publicKey: "base64...", ciphertext: "base64..." },
{ publicKey: "base64...", ciphertext: "base64..." }
],
encryptedData: "base64...",
iv: "base64...",
authTag: "base64..."
}Migration script:
# Re-encrypt existing secrets with multi-recipient format
pnpm ts-node scripts/migrate-to-multi-recipient.tsIf you have plaintext secrets in storage:
// 1. Get all secrets
const secrets = await loadAllSecrets();
// 2. Encrypt each with ML-KEM
for (const [slot, entry] of Object.entries(secrets)) {
const encrypted = await mlkemEncrypt(
entry.secret,
[clientPublicKey, serverPublicKey]
);
await store(slot, encrypted, entry.publicAddresses);
}Q: Is ML-KEM production-ready? A: Yes. ML-KEM is standardized by NIST as FIPS 203 (2024) and is considered production-ready for post-quantum cryptography.
Q: What's the performance impact? A: Minimal. ML-KEM operations take ~1-2ms on modern hardware. Storage overhead is ~1.6KB per recipient.
Q: Can I use ML-KEM without a TEE? A: Yes, but you lose the security guarantees. The private key would be accessible to administrators.
Q: Is this compatible with existing systems? A: Yes. ML-KEM uses standard base64 encoding and can be integrated into existing HTTP APIs.
Q: What happens if quantum computers arrive sooner than expected? A: Your data is already protected. ML-KEM provides quantum resistance today.
Q: How do I verify TEE attestation?
A: Compare the measurement field with the published source code hash. Verify the TEE platform signature. (Implementation guide coming soon in w3pk.)
Q: Can the server administrator access my secrets?
A: In TEE deployment: No. The private key is sealed in hardware and cannot be extracted.
A: In local development: Yes. The private key is in .env (for testing only).
Q: What if the server is compromised? A: Clients can decrypt locally using their own ML-KEM keys. The server is not required for decryption.
Q: How do I add a new recipient? A: Re-encrypt the data with the new recipient's public key included in the recipients array.
Q: Can I remove a recipient? A: Re-encrypt without that recipient's public key. The old encrypted data should be deleted.
Q: What's the maximum data size? A: No theoretical limit. The data is encrypted with AES-256-GCM, which handles arbitrary sizes.
Q: How do I rotate keys? A: Generate new ML-KEM keypair, update attestation, re-encrypt all secrets. Old keys should be securely destroyed.
| Operation | Time | Notes |
|---|---|---|
| Generate keypair | ~45ms | One-time |
| Encapsulate (per recipient) | ~0.9ms | Linear with recipients |
| Decapsulate | ~1.1ms | Per secret access |
| AES-256-GCM encrypt | ~0.1ms/KB | Data encryption |
| AES-256-GCM decrypt | ~0.1ms/KB | Data decryption |
| Total encrypt (2 recipients) | ~2.1ms | Client-side |
| Total decrypt | ~1.2ms | Server or client |
| Scenario | Plaintext | Encrypted | Overhead |
|---|---|---|---|
| 1 recipient, 100 bytes | 100 | 1,728 | 17.3x |
| 2 recipients, 100 bytes | 100 | 3,328 | 33.3x |
| 2 recipients, 10 KB | 10,240 | 13,468 | 1.3x |
| 2 recipients, 1 MB | 1,048,576 | 1,051,904 | 1.003x |
Conclusion: Overhead is significant for small secrets (<1KB) but negligible for larger data.
- ML-KEM-1024 encryption/decryption
- Multi-recipient support
- w3pk integration
- Server-side decryption
- Client-side decryption
- Deterministic key derivation (HKDF)
- Documentation
- Testing suite
- TEE attestation verification (w3pk)
- Phala Network deployment
- Example applications
- Hardware key storage (HSM)
- Key rotation automation
- Multi-signature support
- Threshold encryption
- Integration with other TEE platforms (AWS Nitro, Intel TDX)
- NIST FIPS 203: ML-KEM - Official specification
- NIST Post-Quantum Cryptography - PQC project
- RFC 9180: HPKE - Hybrid Public Key Encryption
- mlkem - WASM implementation used in zk-api
- w3pk - Client-side integration
- @phala/dstack-sdk - Phala Network TEE
- Implementation Plan - Development roadmap
- Testing Guide - Testing procedures
- Client Encryption - Client-side guide
- Side-Channel Attacks - Security considerations
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Matrix: #zk-api:matrix.org
Last Updated: 2026-03-22 Version: 1.0.0 Status: Production Ready