otdf-python 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- otdf_python/__init__.py +1 -2
- otdf_python/__main__.py +1 -2
- otdf_python/address_normalizer.py +8 -10
- otdf_python/aesgcm.py +8 -0
- otdf_python/assertion_config.py +21 -0
- otdf_python/asym_crypto.py +18 -22
- otdf_python/auth_headers.py +7 -6
- otdf_python/autoconfigure_utils.py +21 -7
- otdf_python/cli.py +5 -5
- otdf_python/collection_store.py +13 -1
- otdf_python/collection_store_impl.py +5 -0
- otdf_python/config.py +13 -0
- otdf_python/connect_client.py +1 -0
- otdf_python/constants.py +2 -0
- otdf_python/crypto_utils.py +4 -0
- otdf_python/dpop.py +3 -5
- otdf_python/ecc_constants.py +12 -14
- otdf_python/ecc_mode.py +7 -2
- otdf_python/ecdh.py +24 -31
- otdf_python/eckeypair.py +5 -0
- otdf_python/header.py +5 -0
- otdf_python/invalid_zip_exception.py +6 -2
- otdf_python/kas_client.py +66 -55
- otdf_python/kas_connect_rpc_client.py +75 -38
- otdf_python/kas_info.py +4 -3
- otdf_python/kas_key_cache.py +10 -9
- otdf_python/key_type.py +4 -0
- otdf_python/key_type_constants.py +4 -11
- otdf_python/manifest.py +24 -0
- otdf_python/nanotdf.py +30 -28
- otdf_python/nanotdf_ecdsa_struct.py +5 -11
- otdf_python/nanotdf_type.py +13 -1
- otdf_python/policy_binding_serializer.py +6 -4
- otdf_python/policy_info.py +6 -0
- otdf_python/policy_object.py +8 -0
- otdf_python/policy_stub.py +2 -0
- otdf_python/resource_locator.py +22 -13
- otdf_python/sdk.py +51 -73
- otdf_python/sdk_builder.py +60 -47
- otdf_python/sdk_exceptions.py +11 -1
- otdf_python/symmetric_and_payload_config.py +6 -0
- otdf_python/tdf.py +47 -10
- otdf_python/tdf_reader.py +10 -13
- otdf_python/tdf_writer.py +5 -0
- otdf_python/token_source.py +4 -3
- otdf_python/version.py +5 -0
- otdf_python/zip_reader.py +10 -2
- otdf_python/zip_writer.py +11 -0
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/METADATA +3 -2
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/RECORD +81 -72
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/WHEEL +1 -1
- otdf_python_proto/__init__.py +2 -6
- otdf_python_proto/authorization/__init__.py +10 -0
- otdf_python_proto/authorization/authorization_connect.py +250 -0
- otdf_python_proto/authorization/v2/authorization_connect.py +315 -0
- otdf_python_proto/entityresolution/__init__.py +10 -0
- otdf_python_proto/entityresolution/entity_resolution_connect.py +185 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_connect.py +185 -0
- otdf_python_proto/kas/__init__.py +2 -2
- otdf_python_proto/kas/kas_connect.py +259 -0
- otdf_python_proto/policy/actions/__init__.py +11 -0
- otdf_python_proto/policy/actions/actions_connect.py +380 -0
- otdf_python_proto/policy/attributes/__init__.py +11 -0
- otdf_python_proto/policy/attributes/attributes_connect.py +1310 -0
- otdf_python_proto/policy/kasregistry/__init__.py +11 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_connect.py +912 -0
- otdf_python_proto/policy/keymanagement/__init__.py +11 -0
- otdf_python_proto/policy/keymanagement/key_management_connect.py +380 -0
- otdf_python_proto/policy/namespaces/__init__.py +11 -0
- otdf_python_proto/policy/namespaces/namespaces_connect.py +648 -0
- otdf_python_proto/policy/registeredresources/__init__.py +11 -0
- otdf_python_proto/policy/registeredresources/registered_resources_connect.py +770 -0
- otdf_python_proto/policy/resourcemapping/__init__.py +11 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_connect.py +790 -0
- otdf_python_proto/policy/subjectmapping/__init__.py +11 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_connect.py +851 -0
- otdf_python_proto/policy/unsafe/__init__.py +11 -0
- otdf_python_proto/policy/unsafe/unsafe_connect.py +705 -0
- otdf_python_proto/wellknownconfiguration/__init__.py +10 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_connect.py +124 -0
- otdf_python_proto/authorization/authorization_pb2_connect.py +0 -191
- otdf_python_proto/authorization/v2/authorization_pb2_connect.py +0 -233
- otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +0 -149
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +0 -149
- otdf_python_proto/kas/kas_pb2_connect.py +0 -192
- otdf_python_proto/policy/actions/actions_pb2_connect.py +0 -275
- otdf_python_proto/policy/attributes/attributes_pb2_connect.py +0 -863
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +0 -611
- otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +0 -275
- otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +0 -443
- otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +0 -527
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +0 -527
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +0 -569
- otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +0 -485
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +0 -107
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/licenses/LICENSE +0 -0
otdf_python/kas_client.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
KASClient: Handles communication with the Key Access Service (KAS).
|
|
3
|
-
"""
|
|
1
|
+
"""KASClient: Handles communication with the Key Access Service (KAS)."""
|
|
4
2
|
|
|
5
3
|
import base64
|
|
6
4
|
import hashlib
|
|
@@ -22,6 +20,8 @@ from .sdk_exceptions import SDKException
|
|
|
22
20
|
|
|
23
21
|
@dataclass
|
|
24
22
|
class KeyAccess:
|
|
23
|
+
"""Key access response from KAS."""
|
|
24
|
+
|
|
25
25
|
url: str
|
|
26
26
|
wrapped_key: str
|
|
27
27
|
ephemeral_public_key: str | None = None
|
|
@@ -29,6 +29,8 @@ class KeyAccess:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class KASClient:
|
|
32
|
+
"""Client for communicating with the Key Access Service (KAS)."""
|
|
33
|
+
|
|
32
34
|
def __init__(
|
|
33
35
|
self,
|
|
34
36
|
kas_url=None,
|
|
@@ -37,6 +39,7 @@ class KASClient:
|
|
|
37
39
|
use_plaintext=False,
|
|
38
40
|
verify_ssl=True,
|
|
39
41
|
):
|
|
42
|
+
"""Initialize KAS client."""
|
|
40
43
|
self.kas_url = kas_url
|
|
41
44
|
self.token_source = token_source
|
|
42
45
|
self.cache = cache or KASKeyCache()
|
|
@@ -62,15 +65,33 @@ class KASClient:
|
|
|
62
65
|
self._dpop_public_key
|
|
63
66
|
)
|
|
64
67
|
|
|
65
|
-
def
|
|
68
|
+
def __enter__(self):
|
|
69
|
+
"""Enter context manager."""
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
73
|
+
"""Exit context manager and clean up resources."""
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""Close the KAS client and release resources.
|
|
78
|
+
|
|
79
|
+
This method should be called when the client is no longer needed
|
|
80
|
+
to properly clean up resources. It's also called automatically
|
|
81
|
+
when using the client as a context manager.
|
|
66
82
|
"""
|
|
67
|
-
|
|
83
|
+
if self.connect_rpc_client:
|
|
84
|
+
self.connect_rpc_client.close()
|
|
85
|
+
|
|
86
|
+
def _normalize_kas_url(self, url: str) -> str:
|
|
87
|
+
"""Normalize KAS URLs based on client security settings.
|
|
68
88
|
|
|
69
89
|
Args:
|
|
70
90
|
url: The KAS URL to normalize
|
|
71
91
|
|
|
72
92
|
Returns:
|
|
73
93
|
Normalized URL with appropriate protocol and port
|
|
94
|
+
|
|
74
95
|
"""
|
|
75
96
|
from urllib.parse import urlparse
|
|
76
97
|
|
|
@@ -78,7 +99,7 @@ class KASClient:
|
|
|
78
99
|
# Parse the URL
|
|
79
100
|
parsed = urlparse(url)
|
|
80
101
|
except Exception as e:
|
|
81
|
-
raise SDKException(f"error trying to parse URL [{url}]"
|
|
102
|
+
raise SDKException(f"error trying to parse URL [{url}]: {e}") from e
|
|
82
103
|
|
|
83
104
|
# Check if we have a host or if this is likely a hostname:port combination
|
|
84
105
|
if parsed.hostname is None:
|
|
@@ -100,10 +121,10 @@ class KASClient:
|
|
|
100
121
|
try:
|
|
101
122
|
port = int(port_str)
|
|
102
123
|
return f"{scheme}://{host}:{port}"
|
|
103
|
-
except ValueError:
|
|
124
|
+
except ValueError as err:
|
|
104
125
|
raise SDKException(
|
|
105
126
|
f"error trying to create URL for host and port [{url}]"
|
|
106
|
-
)
|
|
127
|
+
) from err
|
|
107
128
|
else:
|
|
108
129
|
# Hostname with or without path, add default port
|
|
109
130
|
if "/" in url:
|
|
@@ -116,7 +137,7 @@ class KASClient:
|
|
|
116
137
|
except Exception as e:
|
|
117
138
|
raise SDKException(
|
|
118
139
|
f"error trying to create URL for host and port [{url}]", e
|
|
119
|
-
)
|
|
140
|
+
) from e
|
|
120
141
|
|
|
121
142
|
def _handle_existing_scheme(self, parsed) -> str:
|
|
122
143
|
"""Handle URLs with existing scheme by normalizing protocol and port."""
|
|
@@ -138,17 +159,17 @@ class KASClient:
|
|
|
138
159
|
logging.debug(f"normalized url [{parsed.geturl()}] to [{normalized_url}]")
|
|
139
160
|
return normalized_url
|
|
140
161
|
except Exception as e:
|
|
141
|
-
raise SDKException("error creating KAS address"
|
|
162
|
+
raise SDKException(f"error creating KAS address: {e}") from e
|
|
142
163
|
|
|
143
164
|
def _get_wrapped_key_base64(self, key_access):
|
|
144
|
-
"""
|
|
145
|
-
Extract and normalize the wrapped key to base64-encoded string.
|
|
165
|
+
"""Extract and normalize the wrapped key to base64-encoded string.
|
|
146
166
|
|
|
147
167
|
Args:
|
|
148
168
|
key_access: KeyAccess object
|
|
149
169
|
|
|
150
170
|
Returns:
|
|
151
171
|
Base64-encoded wrapped key string
|
|
172
|
+
|
|
152
173
|
"""
|
|
153
174
|
wrapped_key = getattr(key_access, "wrappedKey", None) or getattr(
|
|
154
175
|
key_access, "wrapped_key", None
|
|
@@ -166,14 +187,14 @@ class KASClient:
|
|
|
166
187
|
return wrapped_key
|
|
167
188
|
|
|
168
189
|
def _build_key_access_dict(self, key_access):
|
|
169
|
-
"""
|
|
170
|
-
Build key access dictionary from KeyAccess object, handling both old and new field names.
|
|
190
|
+
"""Build key access dictionary from KeyAccess object, handling both old and new field names.
|
|
171
191
|
|
|
172
192
|
Args:
|
|
173
193
|
key_access: KeyAccess object
|
|
174
194
|
|
|
175
195
|
Returns:
|
|
176
196
|
Dictionary with key access information
|
|
197
|
+
|
|
177
198
|
"""
|
|
178
199
|
wrapped_key = self._get_wrapped_key_base64(key_access)
|
|
179
200
|
|
|
@@ -197,12 +218,12 @@ class KASClient:
|
|
|
197
218
|
return key_access_dict
|
|
198
219
|
|
|
199
220
|
def _add_optional_fields(self, key_access_dict, key_access):
|
|
200
|
-
"""
|
|
201
|
-
Add optional fields to key access dictionary.
|
|
221
|
+
"""Add optional fields to key access dictionary.
|
|
202
222
|
|
|
203
223
|
Args:
|
|
204
224
|
key_access_dict: Dictionary to add fields to
|
|
205
225
|
key_access: KeyAccess object to extract fields from
|
|
226
|
+
|
|
206
227
|
"""
|
|
207
228
|
# Policy binding
|
|
208
229
|
policy_binding = getattr(key_access, "policyBinding", None) or getattr(
|
|
@@ -244,14 +265,14 @@ class KASClient:
|
|
|
244
265
|
key_access_dict["header"] = base64.b64encode(header).decode("utf-8")
|
|
245
266
|
|
|
246
267
|
def _get_algorithm_from_session_key_type(self, session_key_type):
|
|
247
|
-
"""
|
|
248
|
-
Convert session key type to algorithm string for KAS.
|
|
268
|
+
"""Convert session key type to algorithm string for KAS.
|
|
249
269
|
|
|
250
270
|
Args:
|
|
251
271
|
session_key_type: Session key type (EC_KEY_TYPE or RSA_KEY_TYPE)
|
|
252
272
|
|
|
253
273
|
Returns:
|
|
254
274
|
Algorithm string or None
|
|
275
|
+
|
|
255
276
|
"""
|
|
256
277
|
if session_key_type == EC_KEY_TYPE:
|
|
257
278
|
return "ec:secp256r1" # Default EC curve for NanoTDF
|
|
@@ -262,8 +283,7 @@ class KASClient:
|
|
|
262
283
|
def _build_rewrap_request(
|
|
263
284
|
self, policy_json, client_public_key, key_access_dict, algorithm, has_header
|
|
264
285
|
):
|
|
265
|
-
"""
|
|
266
|
-
Build the unsigned rewrap request structure.
|
|
286
|
+
"""Build the unsigned rewrap request structure.
|
|
267
287
|
|
|
268
288
|
Args:
|
|
269
289
|
policy_json: Policy JSON string
|
|
@@ -274,6 +294,7 @@ class KASClient:
|
|
|
274
294
|
|
|
275
295
|
Returns:
|
|
276
296
|
Dictionary with unsigned rewrap request
|
|
297
|
+
|
|
277
298
|
"""
|
|
278
299
|
import json
|
|
279
300
|
|
|
@@ -316,8 +337,7 @@ class KASClient:
|
|
|
316
337
|
def _create_signed_request_jwt(
|
|
317
338
|
self, policy_json, client_public_key, key_access, session_key_type=None
|
|
318
339
|
):
|
|
319
|
-
"""
|
|
320
|
-
Create a signed JWT for the rewrap request.
|
|
340
|
+
"""Create a signed JWT for the rewrap request.
|
|
321
341
|
The JWT is signed with the DPoP private key.
|
|
322
342
|
|
|
323
343
|
Args:
|
|
@@ -325,6 +345,7 @@ class KASClient:
|
|
|
325
345
|
client_public_key: Client public key PEM string
|
|
326
346
|
key_access: KeyAccess object
|
|
327
347
|
session_key_type: Optional session key type (RSA_KEY_TYPE or EC_KEY_TYPE)
|
|
348
|
+
|
|
328
349
|
"""
|
|
329
350
|
# Build key access dictionary handling both old and new field names
|
|
330
351
|
key_access_dict = self._build_key_access_dict(key_access)
|
|
@@ -354,8 +375,7 @@ class KASClient:
|
|
|
354
375
|
return jwt.encode(payload, self._dpop_private_key_pem, algorithm="RS256")
|
|
355
376
|
|
|
356
377
|
def _create_connect_rpc_signed_token(self, key_access, policy_json):
|
|
357
|
-
"""
|
|
358
|
-
Create a signed token specifically for Connect RPC requests.
|
|
378
|
+
"""Create a signed token specifically for Connect RPC requests.
|
|
359
379
|
For now, this delegates to the existing JWT creation method.
|
|
360
380
|
"""
|
|
361
381
|
return self._create_signed_request_jwt(
|
|
@@ -363,8 +383,7 @@ class KASClient:
|
|
|
363
383
|
)
|
|
364
384
|
|
|
365
385
|
def _create_dpop_proof(self, method, url, access_token=None):
|
|
366
|
-
"""
|
|
367
|
-
Create a DPoP proof JWT as per RFC 9449.
|
|
386
|
+
"""Create a DPoP proof JWT as per RFC 9449.
|
|
368
387
|
|
|
369
388
|
Args:
|
|
370
389
|
method: HTTP method (e.g., "POST")
|
|
@@ -373,6 +392,7 @@ class KASClient:
|
|
|
373
392
|
|
|
374
393
|
Returns:
|
|
375
394
|
DPoP proof JWT string
|
|
395
|
+
|
|
376
396
|
"""
|
|
377
397
|
now = int(time.time())
|
|
378
398
|
|
|
@@ -424,8 +444,7 @@ class KASClient:
|
|
|
424
444
|
)
|
|
425
445
|
|
|
426
446
|
def get_public_key(self, kas_info):
|
|
427
|
-
"""
|
|
428
|
-
Get KAS public key using Connect RPC.
|
|
447
|
+
"""Get KAS public key using Connect RPC.
|
|
429
448
|
Checks cache first if available.
|
|
430
449
|
"""
|
|
431
450
|
try:
|
|
@@ -448,10 +467,7 @@ class KASClient:
|
|
|
448
467
|
raise
|
|
449
468
|
|
|
450
469
|
def _get_public_key_with_connect_rpc(self, kas_info):
|
|
451
|
-
"""
|
|
452
|
-
Get KAS public key using Connect RPC.
|
|
453
|
-
"""
|
|
454
|
-
|
|
470
|
+
"""Get KAS public key using Connect RPC."""
|
|
455
471
|
# Get access token for authentication if token source is available
|
|
456
472
|
access_token = None
|
|
457
473
|
if self.token_source:
|
|
@@ -483,17 +499,17 @@ class KASClient:
|
|
|
483
499
|
f"Connect RPC public key request failed: {type(e).__name__}: {e}"
|
|
484
500
|
)
|
|
485
501
|
logging.error(f"Full traceback: {error_details}")
|
|
486
|
-
raise SDKException(f"Connect RPC public key request failed: {e}")
|
|
502
|
+
raise SDKException(f"Connect RPC public key request failed: {e}") from e
|
|
487
503
|
|
|
488
504
|
def _normalize_session_key_type(self, session_key_type):
|
|
489
|
-
"""
|
|
490
|
-
Normalize session key type to the appropriate enum value.
|
|
505
|
+
"""Normalize session key type to the appropriate enum value.
|
|
491
506
|
|
|
492
507
|
Args:
|
|
493
508
|
session_key_type: Type of the session key (KeyType enum or string "RSA"/"EC")
|
|
494
509
|
|
|
495
510
|
Returns:
|
|
496
511
|
Normalized key type enum
|
|
512
|
+
|
|
497
513
|
"""
|
|
498
514
|
if isinstance(session_key_type, str):
|
|
499
515
|
if session_key_type.upper() == "RSA":
|
|
@@ -511,14 +527,14 @@ class KASClient:
|
|
|
511
527
|
return session_key_type
|
|
512
528
|
|
|
513
529
|
def _prepare_ec_keypair(self, session_key_type):
|
|
514
|
-
"""
|
|
515
|
-
Prepare EC key pair for unwrapping.
|
|
530
|
+
"""Prepare EC key pair for unwrapping.
|
|
516
531
|
|
|
517
532
|
Args:
|
|
518
533
|
session_key_type: EC key type with curve information
|
|
519
534
|
|
|
520
535
|
Returns:
|
|
521
536
|
ECKeyPair instance and client public key
|
|
537
|
+
|
|
522
538
|
"""
|
|
523
539
|
from .eckeypair import ECKeyPair
|
|
524
540
|
|
|
@@ -528,12 +544,12 @@ class KASClient:
|
|
|
528
544
|
return ec_key_pair, client_public_key
|
|
529
545
|
|
|
530
546
|
def _prepare_rsa_keypair(self):
|
|
531
|
-
"""
|
|
532
|
-
Prepare RSA key pair for unwrapping, reusing if possible.
|
|
547
|
+
"""Prepare RSA key pair for unwrapping, reusing if possible.
|
|
533
548
|
Uses separate ephemeral keys for encryption (not DPoP keys).
|
|
534
549
|
|
|
535
550
|
Returns:
|
|
536
551
|
Client public key PEM for the ephemeral encryption key
|
|
552
|
+
|
|
537
553
|
"""
|
|
538
554
|
if self.decryptor is None:
|
|
539
555
|
# Generate ephemeral keys for encryption (separate from DPoP keys)
|
|
@@ -543,8 +559,7 @@ class KASClient:
|
|
|
543
559
|
return self.client_public_key
|
|
544
560
|
|
|
545
561
|
def _unwrap_with_ec(self, wrapped_key, ec_key_pair, response_data):
|
|
546
|
-
"""
|
|
547
|
-
Unwrap a key using EC cryptography.
|
|
562
|
+
"""Unwrap a key using EC cryptography.
|
|
548
563
|
|
|
549
564
|
Args:
|
|
550
565
|
wrapped_key: The wrapped key to decrypt
|
|
@@ -553,6 +568,7 @@ class KASClient:
|
|
|
553
568
|
|
|
554
569
|
Returns:
|
|
555
570
|
Unwrapped key as bytes
|
|
571
|
+
|
|
556
572
|
"""
|
|
557
573
|
if ec_key_pair is None:
|
|
558
574
|
raise SDKException(
|
|
@@ -581,9 +597,7 @@ class KASClient:
|
|
|
581
597
|
return gcm.decrypt(wrapped_key)
|
|
582
598
|
|
|
583
599
|
def _ensure_client_keypair(self, session_key_type):
|
|
584
|
-
"""
|
|
585
|
-
Ensure client keypair is generated and stored.
|
|
586
|
-
"""
|
|
600
|
+
"""Ensure client keypair is generated and stored."""
|
|
587
601
|
if session_key_type == RSA_KEY_TYPE:
|
|
588
602
|
if self.decryptor is None:
|
|
589
603
|
private_key, public_key = CryptoUtils.generate_rsa_keypair()
|
|
@@ -600,15 +614,13 @@ class KASClient:
|
|
|
600
614
|
self.client_public_key = CryptoUtils.get_rsa_public_key_pem(public_key)
|
|
601
615
|
|
|
602
616
|
def _parse_and_decrypt_response(self, response):
|
|
603
|
-
"""
|
|
604
|
-
Parse JSON response and decrypt the wrapped key.
|
|
605
|
-
"""
|
|
617
|
+
"""Parse JSON response and decrypt the wrapped key."""
|
|
606
618
|
try:
|
|
607
619
|
response_data = response.json()
|
|
608
620
|
except Exception as e:
|
|
609
621
|
logging.error(f"Failed to parse JSON response: {e}")
|
|
610
622
|
logging.error(f"Raw response content: {response.content}")
|
|
611
|
-
raise SDKException(f"Invalid JSON response from KAS: {e}")
|
|
623
|
+
raise SDKException(f"Invalid JSON response from KAS: {e}") from e
|
|
612
624
|
|
|
613
625
|
entity_wrapped_key = response_data.get("entityWrappedKey")
|
|
614
626
|
if not entity_wrapped_key:
|
|
@@ -621,8 +633,7 @@ class KASClient:
|
|
|
621
633
|
return self.decryptor.decrypt(encrypted_key)
|
|
622
634
|
|
|
623
635
|
def unwrap(self, key_access, policy_json, session_key_type=None) -> bytes:
|
|
624
|
-
"""
|
|
625
|
-
Unwrap a key using Connect RPC.
|
|
636
|
+
"""Unwrap a key using Connect RPC.
|
|
626
637
|
|
|
627
638
|
Args:
|
|
628
639
|
key_access: Key access information
|
|
@@ -631,6 +642,7 @@ class KASClient:
|
|
|
631
642
|
|
|
632
643
|
Returns:
|
|
633
644
|
Unwrapped key bytes
|
|
645
|
+
|
|
634
646
|
"""
|
|
635
647
|
# Default to RSA if not specified
|
|
636
648
|
if session_key_type is None:
|
|
@@ -655,15 +667,14 @@ class KASClient:
|
|
|
655
667
|
def _unwrap_with_connect_rpc(
|
|
656
668
|
self, key_access, signed_token, session_key_type=None
|
|
657
669
|
) -> bytes:
|
|
658
|
-
"""
|
|
659
|
-
Connect RPC method for unwrapping keys.
|
|
670
|
+
"""Connect RPC method for unwrapping keys.
|
|
660
671
|
|
|
661
672
|
Args:
|
|
662
673
|
key_access: KeyAccess object
|
|
663
674
|
signed_token: Signed JWT token
|
|
664
675
|
session_key_type: Optional session key type (RSA_KEY_TYPE or EC_KEY_TYPE)
|
|
665
|
-
"""
|
|
666
676
|
|
|
677
|
+
"""
|
|
667
678
|
# Get access token for authentication if token source is available
|
|
668
679
|
access_token = None
|
|
669
680
|
if self.token_source:
|
|
@@ -702,8 +713,8 @@ class KASClient:
|
|
|
702
713
|
|
|
703
714
|
except Exception as e:
|
|
704
715
|
logging.error(f"Connect RPC rewrap failed: {e}")
|
|
705
|
-
raise SDKException(f"Connect RPC rewrap failed: {e}")
|
|
716
|
+
raise SDKException(f"Connect RPC rewrap failed: {e}") from e
|
|
706
717
|
|
|
707
718
|
def get_key_cache(self) -> KASKeyCache:
|
|
708
|
-
"""
|
|
719
|
+
"""Return the KAS key cache used for storing and retrieving encryption keys."""
|
|
709
720
|
return self.cache
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
"""
|
|
2
|
-
KASConnectRPCClient: Handles Connect RPC communication with the Key Access Service (KAS).
|
|
1
|
+
"""KASConnectRPCClient: Handles Connect RPC communication with the Key Access Service (KAS).
|
|
3
2
|
This class encapsulates all interactions with otdf_python_proto.
|
|
4
3
|
"""
|
|
5
4
|
|
|
6
5
|
import logging
|
|
7
6
|
|
|
8
|
-
import
|
|
7
|
+
import httpx
|
|
9
8
|
from otdf_python_proto.kas import kas_pb2
|
|
10
|
-
from otdf_python_proto.kas.
|
|
9
|
+
from otdf_python_proto.kas.kas_connect import AccessServiceClientSync
|
|
11
10
|
|
|
12
11
|
from otdf_python.auth_headers import AuthHeaders
|
|
13
12
|
|
|
@@ -15,45 +14,78 @@ from .sdk_exceptions import SDKException
|
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class KASConnectRPCClient:
|
|
18
|
-
"""
|
|
19
|
-
Handles Connect RPC communication with KAS service using otdf_python_proto.
|
|
20
|
-
"""
|
|
17
|
+
"""Handles Connect RPC communication with KAS service using otdf_python_proto."""
|
|
21
18
|
|
|
22
19
|
def __init__(self, use_plaintext=False, verify_ssl=True):
|
|
23
|
-
"""
|
|
24
|
-
Initialize the Connect RPC client.
|
|
20
|
+
"""Initialize the Connect RPC client.
|
|
25
21
|
|
|
26
22
|
Args:
|
|
27
23
|
use_plaintext: Whether to use plaintext (HTTP) connections
|
|
28
24
|
verify_ssl: Whether to verify SSL certificates
|
|
25
|
+
|
|
29
26
|
"""
|
|
30
27
|
self.use_plaintext = use_plaintext
|
|
31
28
|
self.verify_ssl = verify_ssl
|
|
29
|
+
self._http_client = None
|
|
32
30
|
|
|
33
|
-
def
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
"""Enter context manager and create HTTP client."""
|
|
33
|
+
self._http_client = self._create_http_client()
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
37
|
+
"""Exit context manager and close HTTP client."""
|
|
38
|
+
self.close()
|
|
39
|
+
|
|
40
|
+
def close(self):
|
|
41
|
+
"""Close HTTP client and release resources.
|
|
42
|
+
|
|
43
|
+
This method should be called when the client is no longer needed
|
|
44
|
+
to properly clean up resources. It's also called automatically
|
|
45
|
+
when using the client as a context manager.
|
|
34
46
|
"""
|
|
35
|
-
|
|
47
|
+
if self._http_client is not None:
|
|
48
|
+
self._http_client.close()
|
|
49
|
+
self._http_client = None
|
|
50
|
+
|
|
51
|
+
def _create_http_client(self):
|
|
52
|
+
"""Create HTTP client with SSL verification configuration.
|
|
36
53
|
|
|
37
54
|
Returns:
|
|
38
|
-
|
|
55
|
+
httpx.Client configured for SSL verification settings
|
|
56
|
+
|
|
39
57
|
"""
|
|
40
58
|
if self.verify_ssl:
|
|
41
59
|
logging.info("Using SSL verification enabled HTTP client")
|
|
42
|
-
return
|
|
60
|
+
return httpx.Client()
|
|
43
61
|
else:
|
|
44
62
|
logging.info("Using SSL verification disabled HTTP client")
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
return httpx.Client(verify=False)
|
|
64
|
+
|
|
65
|
+
def _get_http_client(self):
|
|
66
|
+
"""Get HTTP client, creating one if needed for backward compatibility.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
httpx.Client instance
|
|
47
70
|
|
|
48
|
-
def _prepare_connect_rpc_url(self, kas_url):
|
|
49
71
|
"""
|
|
50
|
-
|
|
72
|
+
if self._http_client is None:
|
|
73
|
+
logging.warning(
|
|
74
|
+
"KASConnectRPCClient is being used without a context manager. "
|
|
75
|
+
"Consider using 'with KASConnectRPCClient(...) as client:' to ensure proper resource cleanup."
|
|
76
|
+
)
|
|
77
|
+
self._http_client = self._create_http_client()
|
|
78
|
+
return self._http_client
|
|
79
|
+
|
|
80
|
+
def _prepare_connect_rpc_url(self, kas_url):
|
|
81
|
+
"""Prepare the base URL for Connect RPC client.
|
|
51
82
|
|
|
52
83
|
Args:
|
|
53
84
|
kas_url: The normalized KAS URL
|
|
54
85
|
|
|
55
86
|
Returns:
|
|
56
87
|
Base URL for Connect RPC client (without /kas suffix)
|
|
88
|
+
|
|
57
89
|
"""
|
|
58
90
|
connect_rpc_base_url = kas_url
|
|
59
91
|
# Remove /kas suffix, if present
|
|
@@ -61,14 +93,14 @@ class KASConnectRPCClient:
|
|
|
61
93
|
return connect_rpc_base_url
|
|
62
94
|
|
|
63
95
|
def _prepare_auth_headers(self, access_token):
|
|
64
|
-
"""
|
|
65
|
-
Prepare authentication headers if access token is available.
|
|
96
|
+
"""Prepare authentication headers if access token is available.
|
|
66
97
|
|
|
67
98
|
Args:
|
|
68
99
|
access_token: Bearer token for authentication
|
|
69
100
|
|
|
70
101
|
Returns:
|
|
71
102
|
Dictionary with authentication headers or None
|
|
103
|
+
|
|
72
104
|
"""
|
|
73
105
|
if access_token:
|
|
74
106
|
auth_headers = AuthHeaders(
|
|
@@ -79,8 +111,7 @@ class KASConnectRPCClient:
|
|
|
79
111
|
return None
|
|
80
112
|
|
|
81
113
|
def get_public_key(self, normalized_kas_url, kas_info, access_token=None):
|
|
82
|
-
"""
|
|
83
|
-
Get KAS public key using Connect RPC.
|
|
114
|
+
"""Get KAS public key using Connect RPC.
|
|
84
115
|
|
|
85
116
|
Args:
|
|
86
117
|
normalized_kas_url: The normalized KAS URL
|
|
@@ -89,6 +120,7 @@ class KASConnectRPCClient:
|
|
|
89
120
|
|
|
90
121
|
Returns:
|
|
91
122
|
Updated kas_info with public_key and kid
|
|
123
|
+
|
|
92
124
|
"""
|
|
93
125
|
logging.info(
|
|
94
126
|
f"KAS Connect RPC client settings for public key retrieval: "
|
|
@@ -96,8 +128,6 @@ class KASConnectRPCClient:
|
|
|
96
128
|
f"kas_url={kas_info.url}"
|
|
97
129
|
)
|
|
98
130
|
|
|
99
|
-
http_client = self._create_http_client()
|
|
100
|
-
|
|
101
131
|
try:
|
|
102
132
|
connect_rpc_base_url = self._prepare_connect_rpc_url(normalized_kas_url)
|
|
103
133
|
|
|
@@ -106,9 +136,13 @@ class KASConnectRPCClient:
|
|
|
106
136
|
f"for public key retrieval"
|
|
107
137
|
)
|
|
108
138
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
# Get or create HTTP client
|
|
140
|
+
http_client = self._get_http_client()
|
|
141
|
+
|
|
142
|
+
# Create Connect RPC client with configured HTTP client
|
|
143
|
+
client = AccessServiceClientSync(
|
|
144
|
+
address=connect_rpc_base_url, session=http_client
|
|
145
|
+
)
|
|
112
146
|
|
|
113
147
|
# Create public key request
|
|
114
148
|
algorithm = getattr(kas_info, "algorithm", "") or ""
|
|
@@ -119,10 +153,10 @@ class KASConnectRPCClient:
|
|
|
119
153
|
)
|
|
120
154
|
|
|
121
155
|
# Prepare headers with authentication if available
|
|
122
|
-
|
|
156
|
+
headers = self._prepare_auth_headers(access_token)
|
|
123
157
|
|
|
124
158
|
# Make the public key call with authentication headers
|
|
125
|
-
response = client.public_key(request,
|
|
159
|
+
response = client.public_key(request, headers=headers)
|
|
126
160
|
|
|
127
161
|
# Update kas_info with response
|
|
128
162
|
kas_info.public_key = response.public_key
|
|
@@ -138,13 +172,12 @@ class KASConnectRPCClient:
|
|
|
138
172
|
f"Connect RPC public key request failed: {type(e).__name__}: {e}"
|
|
139
173
|
)
|
|
140
174
|
logging.error(f"Full traceback: {error_details}")
|
|
141
|
-
raise SDKException(f"Connect RPC public key request failed: {e}")
|
|
175
|
+
raise SDKException(f"Connect RPC public key request failed: {e}") from e
|
|
142
176
|
|
|
143
177
|
def unwrap_key(
|
|
144
178
|
self, normalized_kas_url, key_access, signed_token, access_token=None
|
|
145
179
|
):
|
|
146
|
-
"""
|
|
147
|
-
Unwrap a key using Connect RPC.
|
|
180
|
+
"""Unwrap a key using Connect RPC.
|
|
148
181
|
|
|
149
182
|
Args:
|
|
150
183
|
normalized_kas_url: The normalized KAS URL
|
|
@@ -154,6 +187,7 @@ class KASConnectRPCClient:
|
|
|
154
187
|
|
|
155
188
|
Returns:
|
|
156
189
|
Unwrapped key bytes from the response
|
|
190
|
+
|
|
157
191
|
"""
|
|
158
192
|
logging.info(
|
|
159
193
|
f"KAS Connect RPC client settings for unwrap: "
|
|
@@ -161,8 +195,6 @@ class KASConnectRPCClient:
|
|
|
161
195
|
f"kas_url={key_access.url}"
|
|
162
196
|
)
|
|
163
197
|
|
|
164
|
-
http_client = self._create_http_client()
|
|
165
|
-
|
|
166
198
|
try:
|
|
167
199
|
kas_service_url = self._prepare_connect_rpc_url(normalized_kas_url)
|
|
168
200
|
|
|
@@ -170,8 +202,13 @@ class KASConnectRPCClient:
|
|
|
170
202
|
f"Creating Connect RPC client for base URL: {kas_service_url}, for unwrap"
|
|
171
203
|
)
|
|
172
204
|
|
|
173
|
-
#
|
|
174
|
-
|
|
205
|
+
# Get or create HTTP client
|
|
206
|
+
http_client = self._get_http_client()
|
|
207
|
+
|
|
208
|
+
# Create Connect RPC client with configured HTTP client
|
|
209
|
+
client = AccessServiceClientSync(
|
|
210
|
+
address=kas_service_url, session=http_client
|
|
211
|
+
)
|
|
175
212
|
|
|
176
213
|
# Create rewrap request
|
|
177
214
|
request = kas_pb2.RewrapRequest(
|
|
@@ -182,10 +219,10 @@ class KASConnectRPCClient:
|
|
|
182
219
|
logging.info(f"Connect RPC signed token: {signed_token}")
|
|
183
220
|
|
|
184
221
|
# Prepare headers with authentication if available
|
|
185
|
-
|
|
222
|
+
headers = self._prepare_auth_headers(access_token)
|
|
186
223
|
|
|
187
224
|
# Make the rewrap call with authentication headers
|
|
188
|
-
response = client.rewrap(request,
|
|
225
|
+
response = client.rewrap(request, headers=headers)
|
|
189
226
|
|
|
190
227
|
# Extract the entity wrapped key from v2 response structure
|
|
191
228
|
# The v2 response has responses[] array with results[] for each policy
|
|
@@ -210,4 +247,4 @@ class KASConnectRPCClient:
|
|
|
210
247
|
|
|
211
248
|
except Exception as e:
|
|
212
249
|
logging.error(f"Connect RPC rewrap failed: {e}")
|
|
213
|
-
raise SDKException(f"Connect RPC rewrap failed: {e}")
|
|
250
|
+
raise SDKException(f"Connect RPC rewrap failed: {e}") from e
|
otdf_python/kas_info.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
"""Key Access Service information and configuration."""
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
@dataclass
|
|
5
7
|
class KASInfo:
|
|
6
|
-
"""
|
|
7
|
-
Configuration for Key Access Server (KAS) information.
|
|
8
|
+
"""Configuration for Key Access Server (KAS) information.
|
|
8
9
|
This class stores details about a Key Access Server including its URL,
|
|
9
10
|
public key, key ID, default status, and cryptographic algorithm.
|
|
10
11
|
"""
|
|
@@ -16,7 +17,7 @@ class KASInfo:
|
|
|
16
17
|
algorithm: str | None = None
|
|
17
18
|
|
|
18
19
|
def clone(self):
|
|
19
|
-
"""
|
|
20
|
+
"""Create a copy of this KASInfo object."""
|
|
20
21
|
from copy import copy
|
|
21
22
|
|
|
22
23
|
return copy(self)
|