Skip to content

Commit 9d181b1

Browse files
committed
SAE progress
1 parent 5cfba86 commit 9d181b1

9 files changed

Lines changed: 145 additions & 21 deletions

File tree

client/__main__.py

Lines changed: 40 additions & 7 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, EncryptorNotRegisteredForClientError
1415
from .client import Client
1516

1617

@@ -71,30 +72,42 @@ async def dske_exception_handler(_request: fastapi.Request, exc: DSKEException):
7172

7273

7374
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{slave_sae_id}}/status")
74-
async def get_etsi_status(slave_sae_id: str):
75+
async def get_etsi_status(
76+
slave_sae_id: str,
77+
authorization_header: Annotated[str | None, fastapi.Header()] = None,
78+
):
7579
"""
7680
ETSI QKD 014 API: Status.
7781
"""
78-
return await _CLIENT.etsi_status(slave_sae_id)
82+
master_sae_id = calling_sae_id(authorization_header)
83+
return await _CLIENT.etsi_status(master_sae_id, slave_sae_id)
7984

8085

8186
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{slave_sae_id}}/enc_keys")
82-
async def get_etsi_get_key(slave_sae_id: str, size: int | None = None):
87+
async def get_etsi_get_key(
88+
slave_sae_id: str,
89+
size: int | None = None,
90+
authorization_header: Annotated[str | None, fastapi.Header()] = None,
91+
):
8392
"""
8493
ETSI QKD 014 API: Get Key.
8594
"""
86-
master_sae_id = _CLIENT.name # Currently, SAE names are the same as client names.
95+
master_sae_id = calling_sae_id(authorization_header)
8796
return await _CLIENT.etsi_get_key(master_sae_id, slave_sae_id, size)
8897

8998

9099
@_APP.get(f"/client/{_CLIENT.name}/etsi/api/v1/keys/{{master_sae_id}}/dec_keys")
91-
async def get_eti_get_key_with_key_ids(master_sae_id: str, key_ID: str):
100+
async def get_eti_get_key_with_key_ids(
101+
master_sae_id: str,
102+
key_ID: str,
103+
authorization_header: Annotated[str | None, fastapi.Header()] = None,
104+
):
92105
"""
93106
ETSI QKD 014 API: Get Key with Key IDs.
94107
"""
95108
# ETSI QKD 014 says that ID in key_ID has to be upper case, which lint doesn't like.
96109
# pylint: disable=invalid-name
97-
slave_sae_id = _CLIENT.name # Currently, SAE names are the same as client names.
110+
slave_sae_id = calling_sae_id(authorization_header)
98111
return await _CLIENT.etsi_get_key_with_key_ids(master_sae_id, slave_sae_id, key_ID)
99112

100113

@@ -116,6 +129,26 @@ async def post_mgmt_stop():
116129
return {"result": "Client stopped"}
117130

118131

132+
def calling_sae_id(authorization_header: str | None) -> str:
133+
"""
134+
Get the SAE ID of the calling entity from the request headers.
135+
"""
136+
if authorization_header is None:
137+
if len(_CLIENT.encryptor_names) == 1:
138+
# If the client has exactly one encryptor, we assume that this is the one.
139+
# This makes it easier for users to use curl for testing in simple topologies.
140+
master_sae_id = _CLIENT.encryptor_names[0]
141+
else:
142+
# There is more than one encryptor, so we cannot guess which one is calling.
143+
raise EncryptorNotRegisteredForClientError(
144+
client_name=_CLIENT.name,
145+
encryptor_name=authorization_header,
146+
)
147+
else:
148+
master_sae_id = authorization_header.strip()
149+
return master_sae_id
150+
151+
119152
def main():
120153
"""
121154
Main entry point for the hub package.

client/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
A DSKE client, or just client for short.
33
"""
44

5+
import traceback
56
import asyncio
67
from uuid import UUID
78
from common import exceptions
@@ -64,15 +65,15 @@ def to_mgmt(self):
6465
"peer_hubs": peer_hubs_status,
6566
}
6667

67-
async def etsi_status(self, slave_sae_id: str):
68+
async def etsi_status(self, master_sae_id: str, slave_sae_id: str):
6869
"""
6970
ETSI QKD 014 V1.1.1 Status API.
7071
"""
7172
# See remark about ETSI QKD API in file TODO
7273
return {
7374
"source_kme_id": self._name,
74-
"target_kme_id": slave_sae_id,
75-
"master_sae_id": self._name,
75+
"target_kme_id": "TODO", # TODO: Determine slave KME ID from slave SAE ID
76+
"master_sae_id": master_sae_id,
7677
"slave_sae_id": slave_sae_id,
7778
"key_size": self._default_key_size_in_bits,
7879
"stored_key_count": 25000, # TODO

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,

common/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,19 @@ def __init__(self, encoded_fragment: str):
317317
message="Invalid encoded fragment.",
318318
details={"encoded_fragment": encoded_fragment},
319319
)
320+
321+
322+
class EncryptorNotRegisteredForClientError(DSKEException):
323+
"""
324+
Exception raised when an encryptor is not registered for a client.
325+
"""
326+
327+
def __init__(self, client_name: str, encryptor_name: str):
328+
super().__init__(
329+
status_code=status.HTTP_400_BAD_REQUEST,
330+
message="Encryptor is not registered for client.",
331+
details={
332+
"client_name": client_name,
333+
"encryptor_name": encryptor_name,
334+
},
335+
)

common/signature.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
The signature for a DSKE in-band protocol message.
33
"""
44

5+
from typing import Optional
56
from .utils import bytes_to_str, str_to_bytes
67

78
HEADER_NAME = "DSKE-Signature"
@@ -74,12 +75,13 @@ def add_to_headers(self, headers: dict[str, str]):
7475
headers[HEADER_NAME] = self.to_enc_str()
7576

7677
@classmethod
77-
def from_headers(cls, headers: dict[str, str]) -> "Signature":
78+
def from_headers(cls, headers: dict[str, str]) -> Optional["Signature"]:
7879
"""
79-
Create a Signature from the DSKE-Signature header in a dictionary of HTTP headers.
80+
Create a Signature from the DSKE-Signature header in a dictionary of HTTP headers. Returns
81+
None if the header is not present.
8082
"""
8183
signature_enc_str = headers.get(LOWER_HEADER_NAME, None)
8284
if signature_enc_str is None:
83-
assert False # TODO: Raise an exception instead
85+
return None
8486
signature = cls.from_enc_str(signature_enc_str)
8587
return signature

common/signing_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def extract_from_headers(cls, headers: dict[str, str]) -> "SigningKey":
113113
"""
114114
signing_key_enc_str = headers.get(LOWER_HEADER_NAME, None)
115115
if signing_key_enc_str is None:
116-
assert False # TODO: Raise an exception instead
116+
return None
117117
signing_key = cls.from_enc_str(signing_key_enc_str)
118118
del headers[LOWER_HEADER_NAME]
119119
return signing_key

hub/__main__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from common import configuration
1010
from common import utils
1111
from common.block import APIBlock
12+
from common.exceptions import DSKEException
1213
from common.share_api import APIGetShareResponse, APIPostShareRequest
1314
from common.signing_key import MiddlewareSigningKey
1415
from common.registration_api import (
@@ -67,6 +68,11 @@ async def middleware_add_response_signature(
6768
Add a signature to the response.
6869
"""
6970
signing_key = MiddlewareSigningKey.extract_from_headers(response.headers)
71+
if signing_key is None:
72+
# Don't sign if the signing key is missing. This could happen, for example, in
73+
# error responses. The other side can always reject the response if it doesn't like
74+
# the missing signature.
75+
return response
7076
chunks = []
7177
async for chunk in response.body_iterator:
7278
chunks.append(chunk)
@@ -82,6 +88,18 @@ async def middleware_add_response_signature(
8288
return signed_response
8389

8490

91+
@_APP.exception_handler(DSKEException)
92+
async def dske_exception_handler(_request: fastapi.Request, exc: DSKEException):
93+
"""
94+
Handle DSKE exceptions.
95+
"""
96+
# Error responses are not signed.
97+
return fastapi.responses.JSONResponse(
98+
status_code=exc.status_code,
99+
content={"message": exc.message, "details": exc.details},
100+
)
101+
102+
85103
@_APP.put(f"/hub/{_HUB.name}/dske/oob/v1/registration")
86104
async def put_oob_client_registration(
87105
registration_request: APIPutRegistrationRequest,
@@ -106,8 +124,6 @@ async def get_oob_psrd(
106124
"""
107125
DSKE Out of band: Get a block of Pre-Shared Random Data (PSRD).
108126
"""
109-
# TODO: Error if the client was not peer.
110-
# TODO: Allow size to be None (use default size decided by hub).
111127
block = _HUB.generate_block_for_client(client_name, pool_owner, size)
112128
return block.to_api()
113129

hub/hub.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from common.allocation import Allocation
1414
from common.block import Block
1515
from common.encryption_key import EncryptionKey
16+
from common.exceptions import EncryptorNotRegisteredForClientError
17+
from common.logging import LOGGER
1618
from common.pool import Pool
1719
from common.share import Share
1820
from common.share_api import APIGetShareResponse, APIPostShareRequest
@@ -75,6 +77,7 @@ def generate_block_for_client(
7577
Generate a block of PSRD for a peer client.
7678
"""
7779
if client_name not in self._peer_clients:
80+
LOGGER.warning(f"Peer client '{client_name}' not found")
7881
raise exceptions.ClientNotRegisteredError(client_name)
7982
peer_client = self._peer_clients[client_name]
8083
match pool_owner_str.lower():
@@ -83,6 +86,9 @@ def generate_block_for_client(
8386
case "hub":
8487
pool_owner = Pool.Owner.LOCAL
8588
case _:
89+
LOGGER.warning(
90+
f"Invalid pool owner {pool_owner_str} for peer client {client_name}"
91+
)
8692
raise exceptions.InvalidPoolOwnerError(pool_owner_str)
8793
block = peer_client.create_random_block(pool_owner, size)
8894
return block
@@ -96,11 +102,22 @@ async def store_share_received_from_client(
96102
"""
97103
Store a key share posted by a client.
98104
"""
105+
# Lookup the peer client
99106
client_name = api_post_share_request.master_client_name
100107
if client_name not in self._peer_clients:
108+
LOGGER.warning(f"Peer client {client_name} not found")
101109
raise exceptions.ClientNotRegisteredError(client_name)
102110
peer_client = self._peer_clients[client_name]
111+
# Verify the request signature
103112
await peer_client.check_request_signature(raw_request)
113+
# Check that the master encryptor (SAE) is one that was registered for the client
114+
master_sae_id = api_post_share_request.master_sae_id
115+
if master_sae_id not in peer_client.encryptor_names:
116+
LOGGER.warning(
117+
f"Encryptor {master_sae_id} not registered for client {client_name}"
118+
)
119+
raise EncryptorNotRegisteredForClientError(client_name, master_sae_id)
120+
# Decrypt the share value
104121
encryption_key_allocation = Allocation.from_api(
105122
api_post_share_request.encryption_key_allocation, peer_client.peer_pool
106123
)
@@ -120,6 +137,7 @@ async def store_share_received_from_client(
120137
# TODO: Check if the key UUID is already present, and if so, do something sensible
121138
self._shares[share.user_key_id] = share
122139
peer_client.add_dske_signing_key_header_to_response(headers_temp_response)
140+
# Clean up fully used blocks
123141
peer_client.delete_fully_used_blocks()
124142

125143
async def get_share_requested_by_client(
@@ -132,25 +150,35 @@ async def get_share_requested_by_client(
132150
"""
133151
Get a key share.
134152
"""
153+
# Lookup the peer client
154+
if client_name not in self._peer_clients:
155+
LOGGER.warning(f"Peer client {client_name} not found")
156+
raise exceptions.ClientNotRegisteredError(client_name)
157+
peer_client = self._peer_clients[client_name]
158+
# Verify the request signature
159+
await peer_client.check_request_signature(raw_request)
160+
# Lookup the share
135161
try:
136162
key_id = UUID(key_id_str)
137163
except ValueError as exc:
164+
LOGGER.warning(f"Invalid key ID {key_id_str}")
138165
raise exceptions.InvalidKeyIDError(key_id_str) from exc
139-
# TODO: Error handling: share is not in the store
140166
try:
141167
share = self._shares[key_id]
142168
except KeyError as exc:
169+
LOGGER.warning(f"No share for key ID {key_id_str}")
143170
raise exceptions.UnknownKeyIDError(key_id) from exc
144-
peer_client = self._peer_clients[client_name]
145-
await peer_client.check_request_signature(raw_request)
171+
# Encrypt the share value
146172
encryption_key = EncryptionKey.from_pool(peer_client.local_pool, share.size)
147173
encrypted_share_value = encryption_key.encrypt(share.value)
174+
# Prepare the response
148175
response = APIGetShareResponse(
149176
share_index=share.share_index,
150177
encryption_key_allocation=encryption_key.allocation.to_api(),
151178
encrypted_share_value=bytes_to_str(encrypted_share_value),
152179
)
153180
peer_client.add_dske_signing_key_header_to_response(headers_temp_response)
181+
# Clean up fully used blocks
154182
peer_client.delete_fully_used_blocks()
155183
return response
156184

hub/peer_client.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from common.allocation import Allocation
88
from common.block import Block
99
from common.exceptions import InvalidSignatureError
10+
from common.logging import LOGGER
1011
from common.pool import Pool
1112
from common.signature import Signature
1213
from common.signing_key import SigningKey
@@ -27,7 +28,20 @@ def __init__(self, client_name: str, encryptor_names: List[str]):
2728
self._encryptor_names = encryptor_names
2829
self._local_pool = Pool(client_name, Pool.Owner.LOCAL)
2930
self._peer_pool = Pool(client_name, Pool.Owner.PEER)
30-
self._shares = {}
31+
32+
@property
33+
def client_name(self) -> str:
34+
"""
35+
Get the client name.
36+
"""
37+
return self._client_name
38+
39+
@property
40+
def encryptor_names(self) -> List[str]:
41+
"""
42+
Get the list of encryptor names registered for this client.
43+
"""
44+
return self._encryptor_names
3145

3246
@property
3347
def local_pool(self) -> Pool:
@@ -93,6 +107,9 @@ async def check_request_signature(self, raw_request: fastapi.Request):
93107
signature_ok = received_signature.same_as(computed_signature)
94108
if not signature_ok:
95109
# TODO: Give allocation back to pool
110+
LOGGER.warning(
111+
f"Invalid signature received from peer client '{self._client_name}'"
112+
)
96113
raise InvalidSignatureError()
97114

98115
def delete_fully_used_blocks(self) -> None:

0 commit comments

Comments
 (0)