Post-quantum encryption gateway: ML-KEM (NIST FIPS 203) + AES-256-GCM, scratch image, keys in RAM
236
Source repository: https://github.com/fdecourt/go-pqc-gatewayβ
The architecture diagrams below are written in Mermaid, which Docker Hub does not render. They display correctly on GitHub: https://github.com/fdecourt/go-pqc-gateway#readmeβ
Pull: docker pull fdecourt/pq-crypto-service:0.0.1
License: Business Source License 1.1 (BUSL-1.1), source-available β see section 11.
π¬π§ Englishβ | π«π· FranΓ§aisβ
This project provides an autonomous post-quantum encryption and key encapsulation gateway, implementing ML-KEM as standardized in NIST FIPS 203, and using algorithms aligned with NSA CNSA 2.0 recommendations.
Important
**Deployment Context & Trust Boundary**: This component is designed exclusively as an internal infrastructure service operating on a **private, isolated, and segmented network** (or via local Unix IPC socket). It is **not intended for direct public exposure to the Internet** without a preceding authentication, authorization, and network filtering layer.
Understanding the security perimeter is essential prior to deployment:
graph TB
subgraph DataPlane ["DATA PLANE (Application Data)"]
direction LR
App["Business Application (PHP / Node / Python / Go)"]
LocalAES["Local Symmetric Encryption<br/>(AES-256-GCM)"]
Storage[("Encrypted Storage / Database")]
App -->|"Plaintext data"| LocalAES
LocalAES -->|"Ciphertext only"| Storage
end
subgraph SecurityPlane ["KEY / SECURITY PLANE (Trust Boundary)"]
direction TB
Gateway["PQC Gateway (Scratch, UID 10001)<br/>ML-KEM Engine in volatile RAM"]
Infisical["Centralized Secret Vault (Infisical)<br/>Secure persistence of master keypairs"]
Gateway <-->|"Master KEM keypair"| Infisical
end
App <-->|"DEK only (32 bytes)<br/>Local Unix Socket or Private Network"| Gateway
/unwrap-key operations. Consequently, network and socket access must be strictly restricted to authorized services.The primary recommended design pattern for databases, large files, and backups is Envelope Encryption using /wrap-key and /unwrap-key.
sequenceDiagram
autonumber
participant App as Business Application
participant GW as PQC Gateway
participant DB as Database / Storage
Note over App: 1. Generate local DEK (32 random bytes)<br/>Encrypt data locally with AES-256-GCM
App->>GW: POST /wrap-key (Raw DEK: 32 bytes)
Note over GW: Wrap under ML-KEM-1024 + AES-256-GCM<br/>(Isolated context: KEY_WRAPPING_DEK)
GW-->>App: 200 OK (Envelope: algorithm, version, suite_id, encapsulated_key, nonce, wrapped_key)
App->>DB: Store encrypted ciphertext + envelope
Note over App: Release DEK reference from memory
Note over App: Subsequent Decryption
App->>DB: Read encrypted payload and envelope
App->>GW: POST /unwrap-key (algorithm, version, suite_id, encapsulated_key, nonce, wrapped_key)
Note over GW: Decapsulate ML-KEM and unwrap DEK
GW-->>App: 200 OK (Raw DEK: 32 bytes)
Note over App: Decrypt data locally, then securely erase/release the DEK where supported by the runtime
| Endpoint | Role | Use Case | Recommendation |
|---|---|---|---|
/wrap-key & /unwrap-key | Envelope Encryption (KMS) | Databases, files, backups, long-term archives | β Recommended Architecture |
/encrypt & /decrypt | Direct hybrid encryption | Small messages (< 10 MB), JSON payloads | Secondary / convenience |
/kem/encapsulate & /kem/decapsulate | Pure KEM Key Agreement | Post-quantum shared secret establishment | Custom protocols |
/generate-key | KMS Key Generation | Generates a new DEK + its wrapped envelope | Initial session setup |
/public-key | Public Key Distribution | Retrieves active post-quantum public key | Asymmetric pipelines |
/health | Liveness & Readiness probe | Docker healthchecks & orchestrator probes | Monitoring |
/metrics | Prometheus text exposition | Request counters, latency histograms, rate-limit rejections | Observability (opt-out via METRICS_ENABLED) |
| Algorithm | Reference Standard | Public Key | Private Key (RAM) | Ciphertext (EncapKey) | Security Objective |
|---|---|---|---|---|---|
| ML-KEM-1024 (default) | NIST FIPS 203 | 1568 bytes | 3168 bytes | 1568 bytes | NIST Category 5 (Aligned with CNSA 2.0 key establishment recommendations) |
| ML-KEM-768 | NIST FIPS 203 | 1184 bytes | 2400 bytes | 1088 bytes | NIST Category 3 |
| DUAL-X25519+ML-KEM-1024 | Classical + PQ Hybrid | 1600 bytes | 3200 bytes | 1600 bytes | Hybrid defense-in-depth / migration strategy |
| DUAL-X25519+ML-KEM-768 | Classical + PQ Hybrid | 1216 bytes | 2432 bytes | 1120 bytes | Hybrid defense-in-depth (Performance / Security) |
DUAL-X25519+ML-KEM-768 and DUAL-X25519+ML-KEM-1024) are project-defined hybrid constructions combining X25519 and ML-KEM through HKDF-SHA256 with transcript binding. The ML-KEM component conforms to FIPS 203; the complete hybrid construction is not itself a NIST-standardized KEM.PQ_KEM_KEYPAIR).key_id, key_version) to support retaining historical private keys for unwrapping archived envelopes is a planned roadmap capability.| Mode | Parameters | Security Properties | Recommended For |
|---|---|---|---|
| Unix IPC Socket (TCP-Disabled Mode) | SOCKET_PATH=/tmp/pq-crypto/pq.sockDISABLE_TCP=true | Local kernel IPC through a Unix Domain Socket whose filesystem entry resides on an ephemeral tmpfs volume. Eliminates network transport exposure on the host. | β Collocated Sidecar deployments |
| Dual Mode (TCP + Socket) | PORT=8080SOCKET_PATH=/tmp/pq-crypto/pq.sockDISABLE_TCP=false | Concurrent local IPC and private network listening. | Progressive migration / hybrid clients |
| Private TCP Network | PORT=8080SOCKET_PATH="" | Intended for distributed architectures over a secure private network (VPC, overlay mesh). No public access. | Multi-server deployments |
The service enforces strict least-privilege principles to reduce the host attack surface:
FROM scratch Image: Autonomous static Go binary (~11 MB), containing no OS distribution, no shell (/bin/sh), and no system utilities (curl, wget).UID 10001): Executes under the dedicated unprivileged pqcrypto account.cap_drop: [ALL]): All elevated Linux capabilities are dropped at container start.read_only: true): The root disk is locked against modifications. Ephemeral files and the IPC socket entry point live strictly in an in-memory tmpfs volume.no-new-privileges: true): Prevents privilege acquisition via SUID executables.ptrace inspection are blocked via PR_SET_DUMPABLE=0 on Linux./var/run/docker.sock): The container does not mount the host Docker socket, possesses no permissions to access it, and operates in an isolated mount namespace.<?php
// 1. Locally encrypt the large document in application memory
$dek = random_bytes(32); // 256-bit local key
$iv = random_bytes(12);
$tag = '';
$ciphertext = openssl_encrypt($documentData, 'aes-256-gcm', $dek, OPENSSL_RAW_DATA, $iv, $tag);
// 2. Gateway wraps ONLY the 32-byte DEK over the local Unix IPC socket
$ch = curl_init('http://localhost/wrap-key');
curl_setopt_array($ch, [
CURLOPT_UNIX_SOCKET_PATH => '/tmp/pq-crypto/pq.sock',
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['plaintext_key' => base64_encode($dek)])
]);
$envelope = json_decode(curl_exec($ch), true);
curl_close($ch);
unset($dek); // Release the DEK reference; PHP does not guarantee secure memory zeroization
// 3. Subsequent unwrapping to read the document
$ch = curl_init('http://localhost/unwrap-key');
curl_setopt_array($ch, [
CURLOPT_UNIX_SOCKET_PATH => '/tmp/pq-crypto/pq.sock',
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'algorithm' => $envelope['algorithm'],
'version' => $envelope['version'],
'suite_id' => $envelope['suite_id'],
'encapsulated_key' => $envelope['encapsulated_key'],
'nonce' => $envelope['nonce'],
'wrapped_key' => $envelope['wrapped_key']
])
]);
$unwrapped = json_decode(curl_exec($ch), true);
curl_close($ch);
$restoredDek = base64_decode($unwrapped['plaintext_key']);
$originalData = openssl_decrypt($ciphertext, 'aes-256-gcm', $restoredDek, OPENSSL_RAW_DATA, $iv, $tag);
Complete implementation available in examples/php/client.phpβ :
require_once __DIR__ . '/examples/php/client.php';
$pq = new PQCryptoClient('http://127.0.0.1:8080');
// 1. Wrap local AES-256-GCM DEK with post-quantum ML-KEM
$envelope = $pq->wrapKey($dek);
// 2. Restore DEK whenever decryption is requested
$restoredDek = $pq->unwrapKey($envelope);
Complete implementation available in examples/node/client.jsβ :
import { PQCryptoClient } from './examples/node/client.js';
const pq = new PQCryptoClient({ baseURL: 'http://127.0.0.1:8080' });
// 1. Wrap local 32-byte DEK with post-quantum ML-KEM
const envelope = await pq.wrapKey(dek);
// 2. Restore DEK whenever decryption is needed
const restoredDek = await pq.unwrapKey(envelope);
The .env file provides configuration tuning for the gateway:
| Variable | Default | Description |
|---|---|---|
ALGORITHM | ML-KEM-1024 | Cryptographic suite: ML-KEM-1024, ML-KEM-768, DUAL-X25519+ML-KEM-1024, DUAL-X25519+ML-KEM-768. |
ENVIRONMENT | production | Environment mode: production, staging, development, test. |
MOCK_MODE | false | In-memory mock engine for CPU-light CI tests. Forbidden in production. |
HOST_BIND_ADDRESS | 127.0.0.1 | Host IP address Docker binds published ports to (loopback default). |
HOST_PORT | 8085 | Port published on the host. |
LISTEN_ADDRESS | 0.0.0.0 | Internal network interface IP for the container process. |
PORT / CONTAINER_PORT | 8080 | Internal container HTTP listening port. |
ACCESS_LOG | false | Access logging per HTTP request (disabled by default for maximum throughput). |
RATE_LIMIT_RPS | 0 | Rate limiter queries per second (0 = disabled). |
RATE_LIMIT_BURST | 100 | Rate limiter token bucket burst capacity. |
LOG_FORMAT | json | Structured log output: json (recommended in production) or text. |
LOG_LEVEL | info | Log verbosity: debug, info, warn, error. |
METRICS_ENABLED | true | Exposes /metrics in Prometheus text format. Restrict to your monitoring network: the endpoint is unauthenticated. |
SOCKET_PATH | (empty) | Local Unix IPC socket path (e.g. /tmp/pq-crypto/pq.sock). |
SOCKET_MODE | 0660 | Unix file permissions for the IPC socket. |
SOCKET_GID | -1 | Group ID for IPC Unix socket (fails closed if set and chown fails). |
DISABLE_TCP | false | When true, disables TCP network listener entirely (IPC socket only). |
TRUST_PROXY | false | Only honor X-Forwarded-For when set to true behind a trusted reverse proxy. |
ALLOW_LEGACY_ENVELOPES | false | Rejects unversioned envelopes if false. |
MAX_PAYLOAD_BYTES | 10485760 (10 MB) | Request body size ceiling (protects against DOS). |
READ_TIMEOUT_SECONDS | 10 | HTTP read timeout (mitigates Slowloris attacks). |
WRITE_TIMEOUT_SECONDS | 10 | HTTP write timeout. |
IDLE_TIMEOUT_SECONDS | 60 | HTTP idle keep-alive timeout. |
INFISICAL_ENABLED | false | Enables keypair synchronization with an Infisical secret vault. |
INFISICAL_URL | http://infisical:8080 | URL for the Infisical instance. |
INFISICAL_SECRET_BUNDLE_NAME | PQ_KEM_KEYPAIR | Secret name for atomic keypair bundle. |
NO_NEW_PRIVILEGES | true | Forbids privilege escalation via Linux SUID binaries. |
READ_ONLY_ROOTFS | true | Mounts root container filesystem in read-only mode. |
TMPFS_MOUNT | /tmp:rw,noexec,nosuid,nodev,size=16m | Volatile in-memory filesystem for ephemeral files. |
HEALTHCHECK_INTERVAL | 10s | Frequency for /usr/local/bin/pq-server -healthcheck. |
CPU_LIMIT / MEM_LIMIT | 1.5 / 256M | Resource ceiling limits. |
Structured logging. All service output is emitted through log/slog. In the default json format each
record is a single JSON object, so the correlation identifier is an indexable field rather than a substring
to parse:
{"time":"2026-09-10T19:46:51Z","level":"INFO","msg":"http_request","correlation_id":"a5ce690a63f5...",
"method":"POST","path":"/wrap-key","status":200,"duration_us":412,"bytes":2104,"remote_addr":"10.0.0.7:52344"}
Every request carries an X-Correlation-ID header, honoured from the inbound X-Correlation-ID or
X-Request-ID when present, generated otherwise. It is propagated to the response, the access log and the
panic records, including on 429 and 500 responses.
Metrics. /metrics serves the Prometheus text exposition format with no third-party dependency:
| Series | Type | Labels |
|---|---|---|
pqc_requests_total | counter | path, status |
pqc_request_duration_seconds | histogram | path |
pqc_rate_limited_total | counter | β |
pqc_panics_recovered_total | counter | β |
pqc_uptime_seconds | gauge | β |
pqc_build_info | gauge | version, algorithm |
Histogram buckets start at 500 Β΅s: a nominal KEM+DEM operation completes well below the millisecond, so coarser buckets would collapse the entire latency profile into a single bar.
Bounded cardinality. The path label only ever carries a route declared by the router. Any other
request path β including probes and scanners β is aggregated under path="other". The series table is
built once at startup and never grows during service, so the size and cost of a scrape are independent of
the traffic received, and a caller cannot turn the exposition endpoint into a memory amplifier. The
instrumentation path itself performs no allocation (immutable route table, atomic counters).
The codebase undergoes rigorous verification and includes a testing roadmap:
go test -race (available via make test-race and automated in CI).govulncheck (available via make vulncheck and automated in CI). The service carries no direct third-party cryptographic dependency: key derivation uses the standard library crypto/hkdf (Go 1.24+), which sits inside the Go FIPS 140-3 module boundary. The only direct dependency performing cryptography is CIRCL, for ML-KEM./health, /metrics, correlation header), extracts the binary that is actually shipped and scans it with govulncheck -mode=binary. The Dockerfile compiles with its own pinned toolchain, distinct from the one used by the other jobs: only a scan of the extracted artifact covers that gap.go test -v)=== RUN TestKAT_NIST_FIPS203_KeyGen
β
NIST FIPS 203 KeyGen [ML-KEM-512]: 25/25 vectors PASS (exact bit-match)
β
NIST FIPS 203 KeyGen [ML-KEM-768]: 25/25 vectors PASS (exact bit-match)
β
NIST FIPS 203 KeyGen [ML-KEM-1024]: 25/25 vectors PASS (exact bit-match)
Total KeyGen NIST FIPS 203 vectors: 75/75 PASS
--- PASS: TestKAT_NIST_FIPS203_KeyGen
=== RUN TestKAT_NIST_FIPS203_EncapDecap
β
NIST FIPS 203 Encap AFT [ML-KEM-512]: 25/25 vectors PASS
β
NIST FIPS 203 Encap AFT [ML-KEM-768]: 25/25 vectors PASS
β
NIST FIPS 203 Encap AFT [ML-KEM-1024]: 25/25 vectors PASS
β
NIST FIPS 203 Decap VAL [ML-KEM-512]: 10/10 vectors PASS
β
NIST FIPS 203 Decap VAL [ML-KEM-768]: 10/10 vectors PASS
β
NIST FIPS 203 Decap VAL [ML-KEM-1024]: 10/10 vectors PASS
Total Encap/Decap NIST FIPS 203 vectors: 105/105 PASS
--- PASS: TestKAT_NIST_FIPS203_EncapDecap
=== RUN TestKAT_NIST_FIPS203_MonteCarlo_StressTest
β
Monte-Carlo [ML-KEM-768]: 1,000 chained cycles PASS (Accumulator: b27f9cc4...)
β
Monte-Carlo [ML-KEM-1024]: 1,000 chained cycles PASS (Accumulator: 2b13db7c...)
--- PASS: TestKAT_NIST_FIPS203_MonteCarlo_StressTest
PASS (All unit, regression, security invariant, and KAT suites passing)
Caution
**Project Status & Absence of Formal Certification**: - This software is a source-available project designed to explore and integrate post-quantum cryptography within private architectures. - **This software has NOT undergone a formal, independent security audit by an accredited third-party testing laboratory.** - **This component is NOT certified under NIST FIPS 140-3 and has NOT been validated by the Cryptographic Module Validation Program (CMVP).** - References to "FIPS 203" or "CNSA 2.0" denote the **underlying mathematical algorithms** and specifications implemented, not an official government or regulatory certification. - For critical production environments, integrators must perform their own threat modeling and risk assessment, and enforce defense-in-depth isolation measures.
Released under the Business Source License 1.1β (SPDX: BUSL-1.1) β Copyright (c) 2026 fdecourt.
The source code is public, but this is not an open-source license. In short:
| Use | Allowed |
|---|---|
| Read, audit, copy, modify, redistribute, non-production use | Yes |
| Production use for the internal operations of your organization | Yes (Additional Use Grant) |
| Offering the gateway, or a service exposing it, to third parties (hosted, managed, embedded, white-label) | No β requires a commercial license |
| Selling, sublicensing or distributing it for a fee | No β requires a commercial license |
On the Change Date (2030-09-11), this version automatically becomes available under the Apache License 2.0. The LICENSEβ file is authoritative; this table is only a summary. For a commercial license, contact the Licensor through github.com/fdecourtβ .
The license is independent of the disclaimer above: the absence of formal certification remains in force whatever the license permits.
The components shipped with the service keep their own licenses. Their full texts and notices are in THIRD_PARTY_LICENSESβ , which is also embedded in the container image at /THIRD_PARTY_LICENSES.
| Component | License | Shipped as |
|---|---|---|
| Go standard library & runtime | BSD-3-Clause | compiled into pq-server |
golang.org/x/sys | BSD-3-Clause | compiled into pq-server |
golang.org/x/time | BSD-3-Clause | compiled into pq-server |
github.com/cloudflare/circl (ML-KEM) | BSD-3-Clause | compiled into pq-server |
Mozilla CA certificate bundle (Alpine ca-certificates-bundle) | MPL-2.0 AND MIT | /etc/ssl/certs/ca-certificates.crt in the image |
Content type
Image
Digest
sha256:c0318f27eβ¦
Size
3.3 MB
Last updated
14 days ago
docker pull fdecourt/pq-crypto-service