This project exposes a relay in front of a set of PQKD (Post-Quantum Key Distribution) nodes. It speaks the ETSI REST interface towards clients while coordinating multi-hop key forwarding between PQKD proxies. The binary starts an ETSI-compatible façade per configured PQKD node and a relay endpoint that tunnels keys along a configured mesh topology.
- Architecture
- Quick start
- Configuration
- Runtime behaviour
- HTTP interfaces
- Observability
- Development
- License
The binary runs two families of HTTP servers:
- ETSI façade (
EtsiServer) – for each PQKD entry a local server is bound and the ETSI endpoints are proxied to the actual PQKD KME. When a client requestsenc_keysfor a remote SAE, the façade forwards the request to the primary PQKD and then distributes the resulting keys along alternative routes. - Relay endpoint (
RelayServer) – a shared/info_keysendpoint where neighbouring relays exchangeDataKeys. Incoming messages are either persisted locally (when the relay is the final hop) or forwarded towards the next relay after optionally mixing in fresh PQKD entropy.
The relay graph is described by a mesh topology file. For a given request the code discovers two node-disjoint paths between the origin relay and the destination relay to improve resilience.
- Install Rust (stable toolchain, edition 2021). See https://rustup.rs/.
- Prepare configuration files. Examples are available under
config.example.tomlandtopology.example.toml. - Run the binary:
The process starts one ETSI façade per PQKD in the configuration and a relay endpoint listening on the relay
cargo run --release -- \ -c ./local/config-a.toml \ -t ./local/topology.toml
port.
The relay configuration lists the locally hosted PQKD proxies together with the remote peer information.
id = "relay-1" # Relay identifier; must match an entry in the topology file.
port = 4000 # TCP port for the relay `/info_keys` endpoint.
[telemetry]
enabled = true # Enable telemetry websocket sender.
server_ws_url = "ws://127.0.0.1:8080/ingest" # Telemetry backend ingest endpoint.
interval_sec = 10 # Heartbeat interval (seconds). Default: 10.
[[pqkds]]
port = 3000 # ETSI façade listen port.
sae_id = "Test_1SAE" # Local SAE identifier.
remote_sae_id = "Test_2SAE" # SAE id of the remote partner served by this proxy.
remote_proxy_address= "http://127.0.0.1:4001" # Upstream relay URL.
kme_address = "http://172.16.0.154:8082" # Base URL of the underlying PQKD KME.
[[pqkds]]
port = 3001
sae_id = "BobSAE"
remote_sae_id = "Debina_1SAE"
remote_proxy_address = "http://127.0.0.1:4003"
kme_address = "https://31.182.67.96:8082"
ca_cert = "./tmp/qbck-ca.crt" # Optional CA bundle for TLS to the KME.
client_cert = "./tmp/client.crt" # Optional client certificate (PKCS#8 expected).
client_key = "./tmp/client.key" # Optional client key.Notes:
- Every
[[pqkds]]entry results in a local ETSI façade listening on0.0.0.0:<port>. - TLS material is optional. When all three files are present, the façade builds a mutual TLS connector for the proxied KME calls.
remote_proxy_addressmust point to the neighbour relay that will accept/info_keysPOSTs.telemetry.server_ws_urlmust usews://orwss://.
The topology file dictates how relays connect and which SAEs are attached to each relay.
network_id = "pqkd-local-network-1"
[[relay]]
id = "relay-1"
pqkds = ["Test_1SAE", "BobSAE"]
[[relay]]
id = "relay-2"
pqkds = ["Debina_1SAE", "Test_1SAE"]
[[connection]]
first = "relay-1"
second = "relay-2"
first_sae = "Test_1SAE"
second_sae = "Test_2SAE"For each relay:
idmust be unique and match theConfig.idof the relay instance.pqkdslists SAE identifiers hosted on the relay.connectionentries describe relay-level edges and corresponding SAE pairs for that edge.network_idis optional but recommended; use the same value on all relays belonging to the same mesh.
- ETSI façades proxy
status,enc_keys, anddec_keysrequests straight to the configured KME whenever the target SAE is the direct partner (remote_sae_id). - For remote SAEs, the façade:
- Asks the local KME for fresh
enc_keys. - Builds two node-disjoint relay paths using the mesh definition.
- Ships the returned keys (or XOR-combined variants) to the next relay in each path through
/info_keys.
- Asks the local KME for fresh
- The relay endpoint accepts
DataKeyspayloads and either stores the keys locally (once the final hop is reached) or forwards them to the next relay, optionally masking the payload with keys fetched from its own PQKD partner. - Received keys are cached in-memory (per SAE) until two identical copies are present, allowing the façade to serve
dec_keysresponses.
All endpoints are rooted at http://<host>:<pqkd.port>/api/v1/keys/{sae_id}.
| Method | Path | Description |
|---|---|---|
| GET | /status |
Proxies status checks to the local KME. |
| GET | /enc_keys |
When sae_id matches the direct peer, forwards the call to the KME. Otherwise orchestrates multi-hop distribution along alternative paths. |
| POST | /enc_keys |
Same as GET but forwards body payload to the KME. |
| GET | /dec_keys |
Returns locally cached keys for the requested key_ID query parameter. |
| POST | /dec_keys |
Accepts a JSON body with key_IDs array; returns the available keys. |
Responses mirror whatever the underlying PQKD node returns. Errors are logged with tracing.
POST /info_keys – accepts a JSON DataKeys payload:
{
"from": "Relay_00",
"to": "Relay_01",
"path": ["Relay_00", "Relay_10", "Relay_01"],
"keys": [
{
"key_id": "abc",
"key_id_xor": "aux-id",
"key": "base64-or-raw"
}
]
}The relay either stores the supplied keys locally or forwards a transformed payload to the next hop based on path.
- Logging is powered by
tracing+tracing-subscriber. SetRUST_LOG=pqkd-relay=debug,tower_http=debug(or similar) to tune verbosity. - Each HTTP server includes a
TraceLayerthat logs method, matched path, status codes, and errors. - Telemetry (when enabled) sends websocket JSON events to
/ingest:pqkd-relay.registeronce after connectpqkd-relay.heartbeateveryinterval_sec
Example telemetry payloads:
pqkd-relay.register — sent once on connect, includes topology:
{
"type": "pqkd-relay.register",
"network_id": "pqkd-local-network-1",
"relay_id": "relay-1",
"pqkds": [
{ "sae_id": "Test_1SAE", "paired_with": "Test_2SAE", "status": "ok" },
{ "sae_id": "Test_5SAE", "paired_with": "Test_6SAE", "status": "unknown" }
],
"connections": [
{ "first": "relay-1", "second": "relay-2" }
],
"timestamp_utc": "2026-05-22T13:20:20.123+00:00"
}pqkd-relay.heartbeat — sent every interval_sec:
{
"type": "pqkd-relay.heartbeat",
"network_id": "pqkd-local-network-1",
"relay_id": "relay-1",
"pqkds": [
{ "sae_id": "Test_1SAE", "paired_with": "Test_2SAE", "status": "ok" },
{ "sae_id": "Test_5SAE", "paired_with": "Test_6SAE", "status": "error" }
],
"timestamp_utc": "2026-05-22T13:20:20.123+00:00"
}status values: "ok" (PQKD reachable), "error" (unreachable), "unknown" (health check not yet run).
- Build:
cargo build - Lint/check:
cargo fmt --checkandcargo clippy - Run tests:
cargo test - Example configs:
config.example.toml,topology.example.toml - Local 3-relay test setup:
local/config-{a,b,c}.toml,local/topology.toml,local/run.sh(requires Python 3 for the mock PQKD)
- Configuration and topology inconsistencies currently cause panics because many lookups use
unwrap(). Harden the error handling before deploying to production. - ETSI error responses return bare strings instead of structured JSON.
- Key caches live entirely in-memory and will be lost on restart.
Licensed under the terms of the LICENSE file located in the repository root.