Skip to the content.

Back to main page

Developer guide

This page is intended for software developers; it contains a high-level overview of the implementation.

For an overview of what Distributed Symmetric Key Establishment (DSKE) is and what problem it solves see the introduction.

For a detailed description of the DSKE protocol, see the protocol guide.

If you just want hands-on instructions on how to get started running the code and generating keys with a minimum of background information see the getting started guide. Or, for more details see the user guide.

Technology stack

The code technology stack includes:

The development toolchain includes:

Proof of concept

The code is intended to be a proof-of-concept to study the DSKE protocol; it is not suitable for production deployments for numerous reasons (e.g. state is not persisted and the protocol needs to start from scratch every time a daemon restarts, we have made no effort to prevent side-channel attacks, etc.)

Check-and-test script

The check-and-test bash script in the scripts directory does the following:

A Github action workflow runs this script on every push to our repository.

The --help option explains it’s usage:

$ scripts/check-and-test --help
Usage: check-and-test [OPTIONS] [ACTION]

Positional arguments:
  ACTION:
    lint           Lint the code
    format-check   Check code formatting
    test           Run unit tests (including code coverage)

OPTIONS:
  -h, --help: Display this help message
  -v, --verbose: Verbose output

When the script finishes it reports whether or not all checks and tests passed. It also reports a code coverage report summary. You can open a detailed code coverage report in your browser:

$ open htmlcov/index.html

API endpoints

For full and up-to-date documentation of the API endpoints, each network node provides OpenAPI documentation. To view the documentation go to URL http://localhost:PORT/docs where PORT is the port number for the network node as reported when the network is started.

Here we provide a summary of the API endpoints and their purpose.

API naming conventions

The API endpoints belong to one of the following groups:

API endpoint URL Purpose
.../dske/... DSKE protocol.
.../dske/oob/... The out-of-band (OOB) portion of the DSKE protocol.
.../dske/api/... The in-band portion of the DSKE protocol.
../mgmt/... Used to manage the nodes.

All API endpoints include the node type and the node name at the start of the URL path. For example:

Node type API endpoint URL
Hub /hub/HUB_NAME/...
Client /client/CLIENT_NAME/...

Including the node name in the URL is currently not really necessary since each node runs in its own process on a different HTTP port. But if this code is run as a cloud-based service (similar to QuKayDee for QKD) sitting behind a proxy (e.g Nginx) we can use URL-based routing to dispatch each request to the correct node process.

All API endpoints are versioned (currently v1).

Putting all of this together, an example of a complete URL for one of the API endpoints is: /hub/HUB_NAME/dske/api/v1/key-share

Hub API endpoints

The hubs provides the following API endpoints:

Method URL Purpose Authenticated
PUT /hub/HUB_NAME /dske/oob/v1 /registration Register a client with a hub. No
GET /hub/HUB_NAME /dske/oob/v1 /psrd A client gets a block of Pre-Shared Random Data (PSRD) from the hub. No
POST /hub/HUB_NAME /dske/api/v1 /key-share An initiator client adds a key share to the hub. The share can later be retrieved by the responder client. Yes
GET /hub/HUB_NAME /dske/api/v1 /key-share A responder client retrieves a key share from the hub. The share was previously added by the initiator client. Yes
GET /hub/HUB_NAME /mgmt/v1 /status Get the management status of the hub. No
POST /hub/HUB_NAME /mgmt/v1 /stop Stop the hub. No

Clients API endpoints

The clients provides the following API endpoints:

Method URL Purpose Authenticated
GET /client/CLIENT_NAME /etsi/api/v1 /keys/SLAVE_SAE_ID/enc_keys An initiator encryptor gets a key from a client. No
GET /client/CLIENT_NAME /etsi/api/v1 /keys/MASTER_SAE_ID/dec_keys ?key_ID=KEY_ID A responder encryptor gets a key with key ID from a client. No
GET /client/CLIENT_NAME /etsi/api/v1 /keys/SLAVE_SAE_ID/status An encryptor gets the QKD link status from client. No
GET /hub/HUB_NAME /mgmt/v1/status Get the management status of the client. No
POST /hub/HUB_NAME /mgmt/v1/stop Stop the club. No

Authentication

Only the in-band DSKE protocol API endpoints (.../dske/api/...) are authenticated using the authentication mechanism described in the protocol guide

The out-of-band DSKE protocol API endpoints (.../dske/oob/...) are not authenticated. They only exist for the purpose of automated testing, simulating actions that would be some secure out-of-band physical distribution mechanism in real life.

The management API endpoints (../mgmt/...) are also not authenticated because this implementation is not intended for production deployment.

And finally, the key delivery API endpoints (.../etsi/api/...) are also not authenticated because we only implement a simplified subset of the ETSI QKD 014 key delivery interface.

Pre-Shared Random Data (PSRD) management

Pre-Shared Random Data (PSRD) is a central concept in DSKE. This section summarizes how PSRD is implemented in the code.

PSRD management is implemented using the classes Block, Pool, Fragment, and Allocation. We describe each of these classes below. The relationship between these classes are shown in the following figure:

PSRD Management Classes

Class Block

The class Block represents a block of PSRD bytes that the hub sends to the client.

A client requests one block of PSRD from the hub sending a GET request to the /hub/HUB_NAME/dske/oob/v1/psrd API endpoint.

The Block class has the following attributes:

Attribute Type Purpose
block_uuid UUID Uniquely identifies the block.
size int Size of the block in bytes.
data bytes The bytes in the block.
used bitarray A bit for each byte in the block to indicate whether the byte is used (allocated).

Once data is used (allocated), it is copied to a Fragment object; the original data is zeroed out in the Block. In some rare circumstances (having to do with error handling) a Fragment object can be put back into a Block in which case the data is copied back.

Class Pool

The class Pool represents a pool of Pre-Shared Random Data (PSRD) from which the DSKE code make do allocations (see class Allocation below). Each pool has an owner (local or remote); the concept of pool ownership is explained below. The pools implemented as a sequence of PSRD blocks (Block objects).

The Pool class has the following attributes:

Attribute Type Purpose
name str The name of the pool (for debugging purposes).
blocks List[Block] A list of blocks in the pool.
owner local or remote The owner of the pool (explained below).

Class Fragment

The class Fragment represents a contiguous sequence of bytes within a block that have been allocated from that block.

The Fragment class has the following attributes:

Attribute Type Purpose
block Block A reference to the block from which the fragment was allocated.
start int The index of the byte within the block for the first byte in the fragment.
size int The number of bytes in the fragment.
data bytes A copy of the bytes in the block that have been allocated to the fragment.

Class Allocation

The class Allocation represents an allocation of bytes from a PSRD pool (Pool object). It is implemented as a sequence of fragments (Fragment objects). This is needed because the number of bytes that need to be allocated may not be available as a contiguous sequence of unused (non-allocated) bytes in any block in the pool. In that case, the allocation algorithm gathers the needed number of bytes using multiple fragments, each fragment representing a contiguous sequence of bytes.

the Allocation class has the following attributes:

Attribute Type Purpose
fragments List[Fragment] The list of fragments that the allocation is composed of.

The concept of block ownership

When a hub and a client share a block of Pre-Shared Random Data (PSRD) there is a Block object on the hub side and a Block object with the same random data on the client side. The hub and the client use their blocks to allocate secrets that are shared with their peer. These shared secrets are used for encryption and authentication of DSKE protocol messages.

Both the client-side and the hub-side do allocate Allocation objects from Pool objects for the purpose of authenticating messages (HTTP requests and responses) and for encrypting key share data.

When one party sends a message to another party, the sender makes an allocation from it’s local pool of PSRD. The sender uses the allocation to sign and possibly encrypt part of the message. The sender also sends the meta-data of the allocation to the receiver. does the allocation and sends When the receiver receives the meta-data, it allocates the exact same bytes from the receiver’s copy of the PSRD. The receiver then uses the allocation to verify the signature and potentially decrypt part of the message.

In certain scenarios, there can be a race condition where the client and the hub both try to send a message at roughly the same time. Typically, the client sends a new HTTP request when the hub is still sending a response for the previous HTTP request. In such a race condition there is a risk of the client and the hub trying to allocate the exact same byte in the PSRD for two different purposes.

We introduced the concent of pool ownership to solve this problem. Each node has two pools: a locally owned pool and a pool owned by the peer. Each node always makes allocations from the locally owned pool for sent messages. And each node always makes allocations from the pool owned by the peer for received messages.

Message authentication

The DSKE protocol runs over HTTP and not over HTTPS; it uses its own authentication and encryption mechanisms instead of relying on TLS.

In-band DSKE protocol messages are signed using PSRD data for authentication.

The client signs outgoing HTTP request messages and the hub verifies the signature on incoming HTTP request messages. The hub signs outgoing HTTP response message and the client verifies the signature on incoming HTTP response messages.

The sender of an HTTP message signs outgoing HTTP messages as follows:

The received of an HTTP message verifies the signature on incoming HTTP messages as follows:

The meta-data of the signing key is encoded into the DSKE-Signature header as follows:

Example of an encoded signature:

9ad7f620-5a0c-4979-8a2d-66142e06fd79:0:20,cda527e9-5ca7-40d6-a842-0b606597611c:0:12;V12fwNZiooqE1x0beAdQ4fo2YXnOCTPbUgqG38eUQc4=

The part after the semi-colon is the base64-encoded signature:

V12fwNZiooqE1x0beAdQ4fo2YXnOCTPbUgqG38eUQc4=

The part before the semi-colon is the sequence of two fragments:

9ad7f620-5a0c-4979-8a2d-66142e06fd79:0:20,cda527e9-5ca7-40d6-a842-0b606597611c:0:12

The first fragment is bytes 0 through 19 of block 9ad7f620-5a0c-4979-8a2d-66142e06fd79:

9ad7f620-5a0c-4979-8a2d-66142e06fd79:0:20

And the second fragment is bytes 0 through 11 of block cda527e9-5ca7-40d6-a842-0b606597611c:

cda527e9-5ca7-40d6-a842-0b606597611c:0:12

The signatures have to be computed over the body of the HTTP message, exactly as it is encoded in the HTTP message. This was non-trivial to implement in the code.

On the client side, we use the Python httpx module. On the server (hub) side, we use the Python FastAPI module. Both modules try to make life easier for the developer by allowing the code to use Python objects (instead of raw bytes or strings) to represent the body of a message. However, to compute the signature we needed the exact encoded bytes in the HTTP message, not its representation as a Python object. We could have tried to work with canonical encoders/decoders, but that would be complex and error-prone. Instead, we used a different mechanism to get access to the encoded message body and compute/verify the signature over it.

On the client (httpx) side, we use the per-request authentication mechanism to register a async_auth_flow callback. This callback is called after the message is encoded but before it is sent. This allows us to compute the signature and add the DSKE-Signature header on the outgoing HTTP request.

On the hub (server, FastAPI) side, we use the Starlette middleware mechanism. We register the dske_authentication function as a middleware for HTTP. This gives us access to the body in a request before decoding and the body in a response before encoding.

However, when we are in the middleware code, it is too late to allocate a signing key from the PSRD pool. For this reason, the signing key is allocated in the main application logic code and passed to the middleware using a temporary HTTP header DSKE-Signing-Key. The middleware extracts the signing key from the temporary header, uses it to compute the signature, adds the DSKE-Signature header, and removes the temporary DSKE-Signing-Key header.

Share encryption

When a client POSTs a key share to a hub, the share is in the POST request:

When a client GETs a key share from a hub, the share is in the GET response:

## Future enhancements

In this section we list some potential future enhancements.

Clients register with a subset of the hubs

In our current implementation and also in the IETF draft, it is required that the client registers itself with all of the hubs in the network when the client is onboarded. As the network grows and the number of hubs becomes very large and dynamic, this may become a problem. In the future, it would be useful to enhance the protocol so that a client can register itself with only a subset of the hubs. This, however, introduces another challenge: when two clients want to establish a key, they have to agree on a set of hubs that they have in common that can act as relays. Or, alternatively, a mechanism could be introduced to do multi-hop relaying of key shares across a series of multiple hubs.

Add support for key multicast

The current implementation only allows a master SAE (encryptor) to establish a key with a single slave SAE (encryptor). ETSI GS QKD 014 V1.1.1 (2019-02) allows a master SAE to establish a key with more than one slave SAE; this is referred to as key multicast.

Full implementation of ETSI GS QKD 014 API

The repository contains a subset implementation of ETSI GS QKD 014 V1.1.1 (2019-02) for the purpose of delivering keys to encryptors. Since the purpose of this repository is to provide proof-of-concept for the DSKE protocol and not to have a full standards-compliant implementation of ETSI GS QKD 014, many simplifications have been made including:

### Unregistration

When a client starts, it registers itself with all hubs, using the PUT registration API. Currently, client never unregister. We can add an unregistration mechanism by adding a DELETE registration API and calling it when a client shuts down orderly.