Two independent HTTPS endpoints simulate the ETSI GS QKD 014 V1.1.1 key delivery API without QKD hardware. Both endpoints derive matching keys from the same pre-shared key (PSK), providing computational rather than information-theoretic security.
Contents
- Quickstart on Windows
- Quickstart on Linux
- Provisioning your own endpoints
- Building and testing from source
- Key IDs and derivation
- Certificate-based SAE authorization
Main idea. Give both stubs the same pre-shared key (PSK). Each key ID is a UUID that encrypts the authorized SAE pair, a random seed, the key length, and a checksum. Both stubs derive the same key from the PSK and the full UUID, without communicating with each other or storing a shared key pool. Without the PSK, the UUID reveals neither the embedded SAE pair nor the key.
Retrieval requires a client certificate mapped to the UUID's receiving SAE, and the request must name its sending SAE. An attacker holding a valid certificate for a different SAE cannot retrieve the key through the stub using that UUID. The UUID alone is insufficient to calculate the key without the PSK.
Two independent Rust HTTPS servers simulate the ETSI GS QKD 014 V1.1.1 key delivery API. Application A obtains a key and UUID from its local stub, sends the UUID to application B, and B retrieves the same key from its own stub. The stubs never contact each other and need no shared database or synchronized clock.
Both servers use the same 32-byte pre-shared key (PSK) to independently derive identical key material from a UUID. Client certificates identify each application as a Secure Application Entity (SAE), and the UUID binds the key to the sending and receiving SAEs. Both servers must assign the same numeric codes to those SAEs.
The PSK and client-certificate SAE authorization are mandatory. Anyone with the PSK can reconstruct keys and decrypt their UUID metadata. This is a software test stub, not real QKD, and PSK-derived keys do not have forward secrecy.
Computational security without QKD. QKD is not required for this design's computational key secrecy: a securely provisioned, uniformly random 256-bit PSK and HKDF-HMAC-SHA-512 provide strong computational protection for derived keys, under the assumptions in the security argument. This claim requires secret PSKs, protected endpoints, authenticated TLS, SAE authorization, and suitably long output keys (at least 128 bits). It is conditional key secrecy, not information-theoretic security or a proof of the whole service. The wiki proof sketch explains why HKDF pseudorandomness, rather than SHA preimage resistance alone, is the relevant assumption.
Download the signed qkd-stub.exe and qkd-stub-gui.exe from
GitHub Releases (signature
and checksum checks: Windows signing). No OpenSSL, Python,
Rust, or source checkout is needed. In PowerShell, from the directory containing them:
.\qkd-stub.exe demo init
.\qkd-stub.exe --config qkd-demo/a.toml serve
# In a second terminal:
.\qkd-stub.exe --config qkd-demo/b.toml serve
# In a third terminal:
.\qkd-stub.exe demo verifydemo init creates a qkd-demo directory in the current working directory
(--dir changes it; an existing directory is refused) with a test CA, two server
configurations (ports 8443 and 8444), a shared PSK, and client certificates for SAEs
A and B. It holds private keys, so run it from a private location. demo verify
retrieves matching keys in both directions over mTLS. Stop a server with Ctrl-C or
Ctrl-Break. Windows' bundled Schannel curl may not accept PEM client certificates,
so use demo verify rather than the curl example below.
qkd-stub-gui.exe is a dashboard that serves a configuration and shows
client activity: .\qkd-stub-gui.exe --config qkd-demo/a.toml --start. Provisioning
stays in the command-line tool. More: Windows quickstart.
Download qkd-stub-x86_64-linux.tar.gz, a static build that runs on any x86-64
distribution, from GitHub Releases
and verify it as described in Linux releases. Or build with
cargo build --release --locked (target/release/qkd-stub). No OpenSSL, Python, or
source checkout is needed to run or provision endpoints. The tarball contains the
command-line tool only; the dashboard is Windows-only in releases and is built from
source with --features gui.
tar -xzf qkd-stub-x86_64-linux.tar.gz
./qkd-stub demo init
./qkd-stub --config qkd-demo/a.toml serve
# In a second terminal:
./qkd-stub --config qkd-demo/b.toml serve
# In a third terminal:
./qkd-stub demo verifydemo init creates the qkd-demo directory exactly as described for Windows above.
Stop a server with Ctrl-C or SIGTERM. A requests a key for B at the first server,
then B retrieves it at the second:
curl --fail --cacert qkd-demo/ca.pem \
--cert qkd-demo/client-a.pem --key qkd-demo/client-a.key.pem \
'https://localhost:8443/api/v1/keys/B/enc_keys' > issued.json
KEY_ID=$(python3 -c 'import json; print(json.load(open("issued.json"))["keys"][0]["key_ID"])')
curl --fail --cacert qkd-demo/ca.pem \
--cert qkd-demo/client-b.pem --key qkd-demo/client-b.key.pem \
"https://localhost:8444/api/v1/keys/A/dec_keys?key_ID=$KEY_ID"For device-style provisioning with your own CA, start with qkd-stub configure.
It generates a local private key and CSR, saves the configuration, and can resume
after your CA signs the request. Use check to validate setup and serve to start
the endpoint. Scriptable commands cover TLS certificates/trust, PSKs, and SAE
mappings. See standalone provisioning for the full workflow and
command reference.
Separate machines: copy the same PSK and the same SAE code assignments to both machines. Each server may use its own TLS certificate and client CA. Details: Deployment notes.
Windows: Windows quickstart. GitHub Actions runs the full test
suite on Windows and Linux. The
signed Windows build workflow produces an x64
qkd-stub.exe and qkd-stub-gui.exe, with tagged builds stored in GitHub Releases
after publication. Build the dashboard from source with
cargo build --release --locked --features gui.
On Linux: Rust 1.89+, a C compiler, and, for the integration tests, OpenSSL 3, Python 3.11+, and curl. On Ubuntu 24.04 (verified on a fresh container):
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates gcc libc6-dev curl openssl python3 rustc-1.91 cargo-1.91
export PATH=/usr/lib/rust-1.91/bin:$PATH
cargo build --locked
cargo test --locked
for t in setup https mtls psk; do python3 tests/$t.py; doneOther versions, rustup and the version notes: Ubuntu prerequisites.
The Python tests use scripts/gen-*.sh to create OpenSSL test credentials.
Key IDs are AES-256 ciphertext formatted with UUIDv4 version/variant bits. The encrypted payload holds the key length, SAE pair, 72 random bits and a one-byte checksum. Generation retries with fresh randomness until the ciphertext itself has the required six bits (64 attempts on average); no ciphertext bits are overwritten. This retains approximately 66 bits of randomness per fixed SAE pair and key length. Retrieval needs one decryption.
Authorized repeat retrieval of the same ID returns the same key while the PSK remains unchanged, including after a restart. This supports retries; retrieval does not consume the ID, and IDs do not expire.
The PSK hides the embedded pair and length from observers of the ID. It does not
hide network endpoints or traffic patterns, or authenticate issuance. The checksum
remains an 8-bit configuration/error check, not a security boundary. See encrypted ID format for the exact layout,
derivation and limits. The editable three-page diagram
contains the current exchange flow, ID layout,
and key derivation. Older qkd-uuid-layout.png
and qkd-key-derivation.png images describe the previous format.
Both configurations need a trusted client CA bundle (tls trust add) and an SAE
registry (sae add). A client must present a certificate signed by that CA. Its subject DN or a SAN is matched against the
registry. Invalid certificates fail the TLS handshake; unmapped or ambiguous ones
get HTTP 401.
The registry is a TOML file (sae_map in the configuration). An example is in
examples/sae-map.toml:
[[sae]]
id = "A"
code = 1
identities = [{ subject_dn = "CN=client-a" }]
[[sae]]
id = "B"
code = 2
identities = [{ san_uri = "urn:qkd:sae:B" }]Each identity is a table with exactly one selector (subject_dn, san_dns,
san_uri, san_email or san_ip). Both servers must assign the same unique
code (1–65535) to each SAE ID, and the codes must stay stable while any key ID
may still be used. Remote-only entries may omit identities. The file is read at
startup.
On enc_keys, the master is the certificate's SAE and the slave is the SAE in the
URL; both codes go into the UUID. On dec_keys, the certificate's SAE must be the
UUID's slave and the URL's SAE its master, otherwise HTTP 401 for the whole batch.
Request-SAE-ID cannot impersonate another SAE. Details:
SAE authorization details.
To get exact selector values from a client certificate (metadata only, trust is
not verified), use cert inspect; sae add --cert FILE --selector N records one,
or paste a selector into an [[sae]] entry:
qkd-stub cert inspect path/to/client.crtCommand line. Saved configuration and provisioning: configure, serve, check, tls,
cert, psk, sae, and demo. Use --config FILE to select a configuration
(default qkd-stub-data/config.toml); paths inside it are configuration-relative.
See command reference.
HTTP API. HTTPS only (TLS 1.2 and 1.3). Routes and JSON follow ETSI GS QKD 014 V1.1.1 but
the stub is not fully compliant. All paths begin with /api/v1/keys/{SAE_ID}. All calls require a
mapped client certificate and SAE IDs in the URL must be in the registry.
| Method | Suffix | Parameters |
|---|---|---|
| GET | /status |
Peer slave SAE in the path; master from the client certificate (Request-SAE-ID is ignored) |
| GET | /enc_keys |
Optional number and size query parameters |
| POST | /enc_keys |
JSON object with optional number, size, extension fields |
| GET | /dec_keys |
Required key_ID query parameter |
| POST | /dec_keys |
JSON {"key_IDs":[{"key_ID":"UUID"}, ...]} |
Examples, using the demo init credentials (A is client-a):
A="--cacert qkd-demo/ca.pem --cert qkd-demo/client-a.pem --key qkd-demo/client-a.key.pem"
curl $A 'https://localhost:8443/api/v1/keys/B/status'
curl $A 'https://localhost:8443/api/v1/keys/B/enc_keys?number=2&size=256'
curl $A -H 'Content-Type: application/json' \
-d '{"number":2,"size":512}' 'https://localhost:8443/api/v1/keys/B/enc_keys'Parameters, defaults, response format, errors and limits: API details. The wiki also covers key derivation, SAE authorization rules, deployment notes, and testing.
