Skip to content

Commit 48d0be8

Browse files
committed
Introduce real SAE IDs
1 parent 51a7a98 commit 48d0be8

25 files changed

Lines changed: 528 additions & 211 deletions

TODO

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ TODO: Make dske.yaml the default configuration file, and add a --config option t
44

55
TODO: Cleanup Shamir's Secret Sharing (SSS) code (no back-and-forth conversion to RawShare)
66

7-
TODO: Better error response if authentication key is bad (and return to pool if allocated)
8-
- Bad block uuid
9-
- Start and end of range
10-
- Already allocated
11-
- Etc.
12-
137
TODO: Move code coverage data files to subdirectory
148

159
TODO: Stop all nodes after each test (to make sure they are stopped if test case fails)
@@ -18,23 +12,11 @@ TODO: Add a test cases with a netcat (nc) listener squatting the port
1812

1913
TODO: Make sure key has unique key UUID
2014

21-
TODO: Key initiator: Select N peer hub (initially: all peer hubs)
22-
23-
TODO: Allow a client to register itself again (after a restart). Done; just need to test.
24-
25-
TODO: Add statistics management API and topology command to query it.
26-
27-
TODO: More unit test cases to get code coverage up
28-
2915
TODO: Add Sphinx documentation
3016
Use https://github.com/tox-dev/sphinx-autodoc-typehints
3117

3218
TODO: Cleanup share after all clients have retrieved it, or after timeout.
3319

34-
TODO: Request new PSRD blocks when they run low (have a threshold)
35-
36-
TODO: Better way to avoid circular imports that then current # type: ignore
37-
3820
TODO: Make the swagger docs also work off-line
3921

4022
TODO: Type annotations everywhere
@@ -49,18 +31,11 @@ TODO: Put .out files in a sub-directory
4931

5032
TODO: OpenAPI (Swagger) documentation should show proper type for request and response data
5133

52-
TODO: Error if get-key-with-key-ids if the API is invoked on client which is not actually one of the
53-
slaves for that key.
54-
5534
TODO: Use data (instead of value) in Fragment for consistency. Also in documentation (including
5635
figures)
5736

5837
TODO: Avoid usage: __main__.py in --help output for client / hub
5938

60-
TODO: Add site map to Google Search Console
61-
62-
TODO: Consistently use httpx for client side (not requests for synchronous calls)
63-
6439
TODO: Add test case: start hub later than client (registration retry logic in client)
6540

6641
TODO: Forbid extra query parameters for API calls
@@ -72,12 +47,6 @@ TODO: Fix crash in shamir code when key size 8 is used (don't allow too small ke
7247

7348
TODO: Time-out shares on hubs and remove them if they have not been retrieved after the time-out
7449

75-
TODO: When I ask for a 20,000 bytes key, it fails (as expected). But the attempted allocation for
76-
the encryption key of each share is only 2,500 bytes which seems wrong:
77-
5 hubs x 2,500 bytes per share = 12,500 bytes (I expected at least 20,000)
78-
Note: it's because key size is in bits and pool size is in bytes. Add size_in_bits where
79-
appropriate.
80-
8150
TODO: Have an option to use PSRD data as the key instead of a random number generator.
8251
Explain pros and cons of each approach (forward secrecy if PSRD is compromised) in docs.
8352

@@ -90,9 +59,6 @@ TODO: Testcase for invalid signature
9059
- Signature is incorrect
9160
- Make sure bytes taken from pool are returned (to defend against DOS attack)
9261

93-
TODO: Do we need the owner field in class Block? We known the owner by virtue of which pool
94-
it is in.
95-
96-
TODO: If signature validation fails, return fragment to pool
62+
TODO: Test case for: if signature validation fails, return fragment to pool
9763

98-
TODO: Test case for this
64+
TODO: Error message if --client or --hub does not exist in topology

client/__main__.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import contextlib
77
import os
88
import signal
9+
from typing import Annotated
910
import fastapi
1011
import uvicorn
1112
from common import configuration
1213
from common import utils
13-
from common.exceptions import DSKEException
14+
from common.exceptions import DSKEException, MissingAuthorizationHeaderError
1415
from .client import Client
1516

1617

@@ -29,6 +30,12 @@ def parse_command_line_arguments():
2930
type=str,
3031
help=f"Base URLs for hubs (e.g., http://127.0.0.1:{configuration.DEFAULT_BASE_PORT})",
3132
)
33+
parser.add_argument(
34+
"--encryptors",
35+
nargs="+",
36+
type=str,
37+
help="Names (SAE IDs) of encryptors consuming keys from this client (KME).",
38+
)
3239
args = parser.parse_args()
3340
return args
3441

@@ -37,7 +44,11 @@ def parse_command_line_arguments():
3744
peer_hub_urls = _ARGS.hubs
3845
if peer_hub_urls is None:
3946
peer_hub_urls = []
40-
_CLIENT = Client(_ARGS.name, peer_hub_urls)
47+
if _ARGS.encryptors is None:
48+
encryptor_names = []
49+
else:
50+
encryptor_names = _ARGS.encryptors
51+
_CLIENT = Client(_ARGS.name, encryptor_names, peer_hub_urls)
4152

4253

4354
@contextlib.asynccontextmanager
@@ -65,29 +76,43 @@ async def dske_exception_handler(_request: fastapi.Request, exc: DSKEException):
6576

6677

6778
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{slave_sae_id}}/status")
68-
async def get_etsi_status(slave_sae_id: str):
79+
async def get_etsi_status(
80+
slave_sae_id: str,
81+
authorization: Annotated[str | None, fastapi.Header()] = None,
82+
):
6983
"""
7084
ETSI QKD 014 API: Status.
7185
"""
72-
return await _CLIENT.etsi_status(slave_sae_id)
86+
master_sae_id = calling_sae_id(authorization)
87+
return await _CLIENT.etsi_status(master_sae_id, slave_sae_id)
7388

7489

7590
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{slave_sae_id}}/enc_keys")
76-
async def get_etsi_get_key(slave_sae_id: str, size: int | None = None):
91+
async def get_etsi_get_key(
92+
slave_sae_id: str,
93+
size: int | None = None,
94+
authorization: Annotated[str | None, fastapi.Header()] = None,
95+
):
7796
"""
7897
ETSI QKD 014 API: Get Key.
7998
"""
80-
return await _CLIENT.etsi_get_key(slave_sae_id, size=size)
99+
master_sae_id = calling_sae_id(authorization)
100+
return await _CLIENT.etsi_get_key(master_sae_id, slave_sae_id, size)
81101

82102

83103
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{master_sae_id}}/dec_keys")
84-
async def get_eti_get_key_with_key_ids(master_sae_id: str, key_ID: str):
104+
async def get_eti_get_key_with_key_ids(
105+
master_sae_id: str,
106+
key_ID: str,
107+
authorization: Annotated[str | None, fastapi.Header()] = None,
108+
):
85109
"""
86110
ETSI QKD 014 API: Get Key with Key IDs.
87111
"""
88112
# ETSI QKD 014 says that ID in key_ID has to be upper case, which lint doesn't like.
89113
# pylint: disable=invalid-name
90-
return await _CLIENT.etsi_get_key_with_key_ids(master_sae_id, key_ID)
114+
slave_sae_id = calling_sae_id(authorization)
115+
return await _CLIENT.etsi_get_key_with_key_ids(master_sae_id, slave_sae_id, key_ID)
91116

92117

93118
@_APP.get(f"/client/{_CLIENT.name}/mgmt/v1/status")
@@ -108,9 +133,31 @@ async def post_mgmt_stop():
108133
return {"result": "Client stopped"}
109134

110135

136+
def calling_sae_id(authorization_header: str | None) -> str:
137+
"""
138+
Get the SAE ID of the calling entity from the request headers.
139+
"""
140+
if authorization_header is None:
141+
if len(_CLIENT.encryptor_names) == 1:
142+
# If the client has exactly one encryptor, we assume that this is the one.
143+
# This makes it easier for users to use curl for testing in simple topologies.
144+
# It's okay because we only implement a subset of ETSI QKD 014: we are not really
145+
# authenticating the clients, we are just using the Authorization header to
146+
# determine which encryptor is calling.
147+
master_sae_id = _CLIENT.encryptor_names[0]
148+
else:
149+
# There is more than one encryptor, so we cannot guess which one is calling.
150+
raise MissingAuthorizationHeaderError(
151+
client_name=_CLIENT.name,
152+
)
153+
else:
154+
master_sae_id = authorization_header.strip()
155+
return master_sae_id
156+
157+
111158
def main():
112159
"""
113-
Main entry point for the hub package.
160+
Main entry point for the client package.
114161
"""
115162
utils.create_pid_file("client", _CLIENT.name)
116163
config = uvicorn.Config(app=_APP, port=_ARGS.port)

client/client.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ class Client:
2828
_max_keys_per_request = 1 # TODO: Allow more than one
2929

3030
_name: str
31+
_encryptor_names: list[str]
3132
_peer_hubs: list[PeerHub]
3233

33-
def __init__(self, name: str, peer_hub_urls: list[str]):
34+
def __init__(self, name: str, encryptor_names: list[str], peer_hub_urls: list[str]):
3435
self._name = name
36+
self._encryptor_names = encryptor_names
3537
self._peer_hubs = []
3638
for peer_hub_url in peer_hub_urls:
3739
peer_hub = PeerHub(self, peer_hub_url)
@@ -44,22 +46,33 @@ def name(self):
4446
"""
4547
return self._name
4648

49+
@property
50+
def encryptor_names(self):
51+
"""
52+
Get the encryptor names.
53+
"""
54+
return self._encryptor_names
55+
4756
def to_mgmt(self):
4857
"""
4958
Get the management status.
5059
"""
5160
peer_hubs_status = [peer_hub.to_mgmt() for peer_hub in self._peer_hubs]
52-
return {"name": self._name, "peer_hubs": peer_hubs_status}
61+
return {
62+
"name": self._name,
63+
"encryptor_names": self._encryptor_names,
64+
"peer_hubs": peer_hubs_status,
65+
}
5366

54-
async def etsi_status(self, slave_sae_id: str):
67+
async def etsi_status(self, master_sae_id: str, slave_sae_id: str):
5568
"""
5669
ETSI QKD 014 V1.1.1 Status API.
5770
"""
5871
# See remark about ETSI QKD API in file TODO
5972
return {
6073
"source_kme_id": self._name,
61-
"target_kme_id": slave_sae_id,
62-
"master_sae_id": self._name,
74+
"target_kme_id": "TODO", # TODO: Determine slave KME ID from slave SAE ID
75+
"master_sae_id": master_sae_id,
6376
"slave_sae_id": slave_sae_id,
6477
"key_size": self._default_key_size_in_bits,
6578
"stored_key_count": 25000, # TODO
@@ -70,7 +83,12 @@ async def etsi_status(self, slave_sae_id: str):
7083
"max_sae_id_count": 0,
7184
}
7285

73-
async def etsi_get_key(self, _slave_sae_id: str, size: int | None = None):
86+
async def etsi_get_key(
87+
self,
88+
master_sae_id: str,
89+
slave_sae_id: str,
90+
size: int | None = None,
91+
):
7492
"""
7593
ETSI QKD 014 V1.1.1 Get key API.
7694
"""
@@ -89,25 +107,25 @@ async def etsi_get_key(self, _slave_sae_id: str, size: int | None = None):
89107
)
90108
size_in_bytes = size // 8
91109
key = UserKey.create_random_key(size_in_bytes)
92-
await self.scatter_key_amongst_peer_hubs(key)
110+
await self.scatter_key_amongst_peer_hubs(master_sae_id, slave_sae_id, key)
93111
return {
94112
"keys": {
95113
"key_ID": key.key_id,
96114
"key": utils.bytes_to_str(key.value),
97115
}
98116
}
99117

100-
async def etsi_get_key_with_key_ids(self, _master_sae_id: str, key_id: str):
118+
async def etsi_get_key_with_key_ids(
119+
self, master_sae_id: str, slave_sae_id: str, key_id: str
120+
):
101121
"""
102122
ETSI QKD 014 V1.1.1 Get key with key IDs API.
103123
"""
104-
# TODO: Pass the master SAE ID and the slave SAE ID along with the relayed shares
105-
# and check that they match here.
106124
try:
107125
key_id = UUID(key_id)
108126
except ValueError as exc:
109127
raise exceptions.InvalidKeyIDError(key_id) from exc
110-
key = await self.gather_key_from_peer_hubs(key_id)
128+
key = await self.gather_key_from_peer_hubs(master_sae_id, slave_sae_id, key_id)
111129
return {
112130
"keys": [
113131
{
@@ -124,15 +142,22 @@ def start_all_peer_hubs(self) -> None:
124142
for peer_hub in self._peer_hubs:
125143
peer_hub.start_register_task()
126144

127-
async def scatter_key_amongst_peer_hubs(self, key: UserKey) -> None:
145+
async def scatter_key_amongst_peer_hubs(
146+
self,
147+
master_sae_id: str,
148+
slave_sae_id: str,
149+
key: UserKey,
150+
) -> None:
128151
"""
129152
Split the key into key shares, and send each key share to a peer hub.
130153
"""
131154
nr_shares = len(self._peer_hubs)
132-
shares = key.split_into_shares(nr_shares, _MIN_NR_SHARES)
155+
shares = key.split_into_shares(
156+
master_sae_id, slave_sae_id, nr_shares, _MIN_NR_SHARES
157+
)
133158
assert len(shares) == nr_shares
134159
coroutines = [
135-
peer_hub.post_share(share)
160+
peer_hub.post_share(master_sae_id, slave_sae_id, share)
136161
for peer_hub, share in zip(self._peer_hubs, shares)
137162
]
138163
results = await asyncio.gather(*coroutines, return_exceptions=True)
@@ -152,13 +177,21 @@ async def scatter_key_amongst_peer_hubs(self, key: UserKey) -> None:
152177
key.key_id, nr_shares_successfully_scattered, _MIN_NR_SHARES, causes
153178
)
154179

155-
async def gather_key_from_peer_hubs(self, key_id: UUID) -> UserKey:
180+
async def gather_key_from_peer_hubs(
181+
self,
182+
master_sae_id: str,
183+
slave_sae_id: str,
184+
key_id: UUID,
185+
) -> UserKey:
156186
"""
157187
Gather key shares from the peer hubs, and reconstruct the key out of (a subset of)
158188
the key shares.
159189
"""
160190
nr_shares_attempted_to_gather = len(self._peer_hubs)
161-
coroutines = [peer_hub.get_share(key_id) for peer_hub in self._peer_hubs]
191+
coroutines = [
192+
peer_hub.get_share(master_sae_id, slave_sae_id, key_id)
193+
for peer_hub in self._peer_hubs
194+
]
162195
results = await asyncio.gather(*coroutines, return_exceptions=True)
163196
shares = [result for result in results if not isinstance(result, Exception)]
164197
nr_shares_successfully_gathered = len(shares)

client/http_client.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ async def async_auth_flow(self, request):
4141
signature.add_to_headers(request.headers)
4242
response = yield request
4343
received_signature = Signature.from_headers(response.headers)
44+
if received_signature is None:
45+
# If the signature is missing on an error response, keep the original error response
46+
# instead of raising an InvalidSignatureError which wipes out any useful error info.
47+
if response.status_code < 400:
48+
raise InvalidSignatureError()
49+
return
4450
allocation = Allocation.from_enc_str(
4551
received_signature.signing_key_allocation_enc_str, self._peer_pool
4652
)
@@ -166,7 +172,12 @@ async def _put_or_post(
166172
exception=str(exc),
167173
) from exc
168174
if response.status_code != 200:
169-
LOGGER.error(f"Call {method} {url} {response.status_code}")
175+
message = ""
176+
try:
177+
message = " " + response.json().get("message")
178+
except Exception: # pylint: disable=broad-except
179+
pass
180+
LOGGER.error(f"Call {method} {url} {response.status_code}{message}")
170181
raise exceptions.HTTPError(
171182
method=method,
172183
url=url,

0 commit comments

Comments
 (0)