otdf-python 0.3.5__py3-none-any.whl → 0.4.1__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 +22 -6
- otdf_python/cli.py +5 -5
- otdf_python/collection_store.py +13 -0
- 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 -25
- 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 +48 -55
- otdf_python/kas_connect_rpc_client.py +16 -19
- 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 +34 -24
- otdf_python/nanotdf_ecdsa_struct.py +5 -9
- otdf_python/nanotdf_type.py +12 -0
- 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 +49 -57
- otdf_python/sdk_builder.py +58 -41
- 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.3.5.dist-info → otdf_python-0.4.1.dist-info}/METADATA +19 -2
- {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/RECORD +52 -52
- {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/WHEEL +1 -1
- {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.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()
|
|
@@ -63,14 +66,14 @@ class KASClient:
|
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
def _normalize_kas_url(self, url: str) -> str:
|
|
66
|
-
"""
|
|
67
|
-
Normalize KAS URLs based on client security settings.
|
|
69
|
+
"""Normalize KAS URLs based on client security settings.
|
|
68
70
|
|
|
69
71
|
Args:
|
|
70
72
|
url: The KAS URL to normalize
|
|
71
73
|
|
|
72
74
|
Returns:
|
|
73
75
|
Normalized URL with appropriate protocol and port
|
|
76
|
+
|
|
74
77
|
"""
|
|
75
78
|
from urllib.parse import urlparse
|
|
76
79
|
|
|
@@ -78,7 +81,7 @@ class KASClient:
|
|
|
78
81
|
# Parse the URL
|
|
79
82
|
parsed = urlparse(url)
|
|
80
83
|
except Exception as e:
|
|
81
|
-
raise SDKException(f"error trying to parse URL [{url}]"
|
|
84
|
+
raise SDKException(f"error trying to parse URL [{url}]: {e}") from e
|
|
82
85
|
|
|
83
86
|
# Check if we have a host or if this is likely a hostname:port combination
|
|
84
87
|
if parsed.hostname is None:
|
|
@@ -100,10 +103,10 @@ class KASClient:
|
|
|
100
103
|
try:
|
|
101
104
|
port = int(port_str)
|
|
102
105
|
return f"{scheme}://{host}:{port}"
|
|
103
|
-
except ValueError:
|
|
106
|
+
except ValueError as err:
|
|
104
107
|
raise SDKException(
|
|
105
108
|
f"error trying to create URL for host and port [{url}]"
|
|
106
|
-
)
|
|
109
|
+
) from err
|
|
107
110
|
else:
|
|
108
111
|
# Hostname with or without path, add default port
|
|
109
112
|
if "/" in url:
|
|
@@ -116,7 +119,7 @@ class KASClient:
|
|
|
116
119
|
except Exception as e:
|
|
117
120
|
raise SDKException(
|
|
118
121
|
f"error trying to create URL for host and port [{url}]", e
|
|
119
|
-
)
|
|
122
|
+
) from e
|
|
120
123
|
|
|
121
124
|
def _handle_existing_scheme(self, parsed) -> str:
|
|
122
125
|
"""Handle URLs with existing scheme by normalizing protocol and port."""
|
|
@@ -138,17 +141,17 @@ class KASClient:
|
|
|
138
141
|
logging.debug(f"normalized url [{parsed.geturl()}] to [{normalized_url}]")
|
|
139
142
|
return normalized_url
|
|
140
143
|
except Exception as e:
|
|
141
|
-
raise SDKException("error creating KAS address"
|
|
144
|
+
raise SDKException(f"error creating KAS address: {e}") from e
|
|
142
145
|
|
|
143
146
|
def _get_wrapped_key_base64(self, key_access):
|
|
144
|
-
"""
|
|
145
|
-
Extract and normalize the wrapped key to base64-encoded string.
|
|
147
|
+
"""Extract and normalize the wrapped key to base64-encoded string.
|
|
146
148
|
|
|
147
149
|
Args:
|
|
148
150
|
key_access: KeyAccess object
|
|
149
151
|
|
|
150
152
|
Returns:
|
|
151
153
|
Base64-encoded wrapped key string
|
|
154
|
+
|
|
152
155
|
"""
|
|
153
156
|
wrapped_key = getattr(key_access, "wrappedKey", None) or getattr(
|
|
154
157
|
key_access, "wrapped_key", None
|
|
@@ -166,14 +169,14 @@ class KASClient:
|
|
|
166
169
|
return wrapped_key
|
|
167
170
|
|
|
168
171
|
def _build_key_access_dict(self, key_access):
|
|
169
|
-
"""
|
|
170
|
-
Build key access dictionary from KeyAccess object, handling both old and new field names.
|
|
172
|
+
"""Build key access dictionary from KeyAccess object, handling both old and new field names.
|
|
171
173
|
|
|
172
174
|
Args:
|
|
173
175
|
key_access: KeyAccess object
|
|
174
176
|
|
|
175
177
|
Returns:
|
|
176
178
|
Dictionary with key access information
|
|
179
|
+
|
|
177
180
|
"""
|
|
178
181
|
wrapped_key = self._get_wrapped_key_base64(key_access)
|
|
179
182
|
|
|
@@ -197,12 +200,12 @@ class KASClient:
|
|
|
197
200
|
return key_access_dict
|
|
198
201
|
|
|
199
202
|
def _add_optional_fields(self, key_access_dict, key_access):
|
|
200
|
-
"""
|
|
201
|
-
Add optional fields to key access dictionary.
|
|
203
|
+
"""Add optional fields to key access dictionary.
|
|
202
204
|
|
|
203
205
|
Args:
|
|
204
206
|
key_access_dict: Dictionary to add fields to
|
|
205
207
|
key_access: KeyAccess object to extract fields from
|
|
208
|
+
|
|
206
209
|
"""
|
|
207
210
|
# Policy binding
|
|
208
211
|
policy_binding = getattr(key_access, "policyBinding", None) or getattr(
|
|
@@ -244,14 +247,14 @@ class KASClient:
|
|
|
244
247
|
key_access_dict["header"] = base64.b64encode(header).decode("utf-8")
|
|
245
248
|
|
|
246
249
|
def _get_algorithm_from_session_key_type(self, session_key_type):
|
|
247
|
-
"""
|
|
248
|
-
Convert session key type to algorithm string for KAS.
|
|
250
|
+
"""Convert session key type to algorithm string for KAS.
|
|
249
251
|
|
|
250
252
|
Args:
|
|
251
253
|
session_key_type: Session key type (EC_KEY_TYPE or RSA_KEY_TYPE)
|
|
252
254
|
|
|
253
255
|
Returns:
|
|
254
256
|
Algorithm string or None
|
|
257
|
+
|
|
255
258
|
"""
|
|
256
259
|
if session_key_type == EC_KEY_TYPE:
|
|
257
260
|
return "ec:secp256r1" # Default EC curve for NanoTDF
|
|
@@ -262,8 +265,7 @@ class KASClient:
|
|
|
262
265
|
def _build_rewrap_request(
|
|
263
266
|
self, policy_json, client_public_key, key_access_dict, algorithm, has_header
|
|
264
267
|
):
|
|
265
|
-
"""
|
|
266
|
-
Build the unsigned rewrap request structure.
|
|
268
|
+
"""Build the unsigned rewrap request structure.
|
|
267
269
|
|
|
268
270
|
Args:
|
|
269
271
|
policy_json: Policy JSON string
|
|
@@ -274,6 +276,7 @@ class KASClient:
|
|
|
274
276
|
|
|
275
277
|
Returns:
|
|
276
278
|
Dictionary with unsigned rewrap request
|
|
279
|
+
|
|
277
280
|
"""
|
|
278
281
|
import json
|
|
279
282
|
|
|
@@ -316,8 +319,7 @@ class KASClient:
|
|
|
316
319
|
def _create_signed_request_jwt(
|
|
317
320
|
self, policy_json, client_public_key, key_access, session_key_type=None
|
|
318
321
|
):
|
|
319
|
-
"""
|
|
320
|
-
Create a signed JWT for the rewrap request.
|
|
322
|
+
"""Create a signed JWT for the rewrap request.
|
|
321
323
|
The JWT is signed with the DPoP private key.
|
|
322
324
|
|
|
323
325
|
Args:
|
|
@@ -325,6 +327,7 @@ class KASClient:
|
|
|
325
327
|
client_public_key: Client public key PEM string
|
|
326
328
|
key_access: KeyAccess object
|
|
327
329
|
session_key_type: Optional session key type (RSA_KEY_TYPE or EC_KEY_TYPE)
|
|
330
|
+
|
|
328
331
|
"""
|
|
329
332
|
# Build key access dictionary handling both old and new field names
|
|
330
333
|
key_access_dict = self._build_key_access_dict(key_access)
|
|
@@ -354,8 +357,7 @@ class KASClient:
|
|
|
354
357
|
return jwt.encode(payload, self._dpop_private_key_pem, algorithm="RS256")
|
|
355
358
|
|
|
356
359
|
def _create_connect_rpc_signed_token(self, key_access, policy_json):
|
|
357
|
-
"""
|
|
358
|
-
Create a signed token specifically for Connect RPC requests.
|
|
360
|
+
"""Create a signed token specifically for Connect RPC requests.
|
|
359
361
|
For now, this delegates to the existing JWT creation method.
|
|
360
362
|
"""
|
|
361
363
|
return self._create_signed_request_jwt(
|
|
@@ -363,8 +365,7 @@ class KASClient:
|
|
|
363
365
|
)
|
|
364
366
|
|
|
365
367
|
def _create_dpop_proof(self, method, url, access_token=None):
|
|
366
|
-
"""
|
|
367
|
-
Create a DPoP proof JWT as per RFC 9449.
|
|
368
|
+
"""Create a DPoP proof JWT as per RFC 9449.
|
|
368
369
|
|
|
369
370
|
Args:
|
|
370
371
|
method: HTTP method (e.g., "POST")
|
|
@@ -373,6 +374,7 @@ class KASClient:
|
|
|
373
374
|
|
|
374
375
|
Returns:
|
|
375
376
|
DPoP proof JWT string
|
|
377
|
+
|
|
376
378
|
"""
|
|
377
379
|
now = int(time.time())
|
|
378
380
|
|
|
@@ -424,8 +426,7 @@ class KASClient:
|
|
|
424
426
|
)
|
|
425
427
|
|
|
426
428
|
def get_public_key(self, kas_info):
|
|
427
|
-
"""
|
|
428
|
-
Get KAS public key using Connect RPC.
|
|
429
|
+
"""Get KAS public key using Connect RPC.
|
|
429
430
|
Checks cache first if available.
|
|
430
431
|
"""
|
|
431
432
|
try:
|
|
@@ -448,10 +449,7 @@ class KASClient:
|
|
|
448
449
|
raise
|
|
449
450
|
|
|
450
451
|
def _get_public_key_with_connect_rpc(self, kas_info):
|
|
451
|
-
"""
|
|
452
|
-
Get KAS public key using Connect RPC.
|
|
453
|
-
"""
|
|
454
|
-
|
|
452
|
+
"""Get KAS public key using Connect RPC."""
|
|
455
453
|
# Get access token for authentication if token source is available
|
|
456
454
|
access_token = None
|
|
457
455
|
if self.token_source:
|
|
@@ -483,17 +481,17 @@ class KASClient:
|
|
|
483
481
|
f"Connect RPC public key request failed: {type(e).__name__}: {e}"
|
|
484
482
|
)
|
|
485
483
|
logging.error(f"Full traceback: {error_details}")
|
|
486
|
-
raise SDKException(f"Connect RPC public key request failed: {e}")
|
|
484
|
+
raise SDKException(f"Connect RPC public key request failed: {e}") from e
|
|
487
485
|
|
|
488
486
|
def _normalize_session_key_type(self, session_key_type):
|
|
489
|
-
"""
|
|
490
|
-
Normalize session key type to the appropriate enum value.
|
|
487
|
+
"""Normalize session key type to the appropriate enum value.
|
|
491
488
|
|
|
492
489
|
Args:
|
|
493
490
|
session_key_type: Type of the session key (KeyType enum or string "RSA"/"EC")
|
|
494
491
|
|
|
495
492
|
Returns:
|
|
496
493
|
Normalized key type enum
|
|
494
|
+
|
|
497
495
|
"""
|
|
498
496
|
if isinstance(session_key_type, str):
|
|
499
497
|
if session_key_type.upper() == "RSA":
|
|
@@ -511,14 +509,14 @@ class KASClient:
|
|
|
511
509
|
return session_key_type
|
|
512
510
|
|
|
513
511
|
def _prepare_ec_keypair(self, session_key_type):
|
|
514
|
-
"""
|
|
515
|
-
Prepare EC key pair for unwrapping.
|
|
512
|
+
"""Prepare EC key pair for unwrapping.
|
|
516
513
|
|
|
517
514
|
Args:
|
|
518
515
|
session_key_type: EC key type with curve information
|
|
519
516
|
|
|
520
517
|
Returns:
|
|
521
518
|
ECKeyPair instance and client public key
|
|
519
|
+
|
|
522
520
|
"""
|
|
523
521
|
from .eckeypair import ECKeyPair
|
|
524
522
|
|
|
@@ -528,12 +526,12 @@ class KASClient:
|
|
|
528
526
|
return ec_key_pair, client_public_key
|
|
529
527
|
|
|
530
528
|
def _prepare_rsa_keypair(self):
|
|
531
|
-
"""
|
|
532
|
-
Prepare RSA key pair for unwrapping, reusing if possible.
|
|
529
|
+
"""Prepare RSA key pair for unwrapping, reusing if possible.
|
|
533
530
|
Uses separate ephemeral keys for encryption (not DPoP keys).
|
|
534
531
|
|
|
535
532
|
Returns:
|
|
536
533
|
Client public key PEM for the ephemeral encryption key
|
|
534
|
+
|
|
537
535
|
"""
|
|
538
536
|
if self.decryptor is None:
|
|
539
537
|
# Generate ephemeral keys for encryption (separate from DPoP keys)
|
|
@@ -543,8 +541,7 @@ class KASClient:
|
|
|
543
541
|
return self.client_public_key
|
|
544
542
|
|
|
545
543
|
def _unwrap_with_ec(self, wrapped_key, ec_key_pair, response_data):
|
|
546
|
-
"""
|
|
547
|
-
Unwrap a key using EC cryptography.
|
|
544
|
+
"""Unwrap a key using EC cryptography.
|
|
548
545
|
|
|
549
546
|
Args:
|
|
550
547
|
wrapped_key: The wrapped key to decrypt
|
|
@@ -553,6 +550,7 @@ class KASClient:
|
|
|
553
550
|
|
|
554
551
|
Returns:
|
|
555
552
|
Unwrapped key as bytes
|
|
553
|
+
|
|
556
554
|
"""
|
|
557
555
|
if ec_key_pair is None:
|
|
558
556
|
raise SDKException(
|
|
@@ -581,9 +579,7 @@ class KASClient:
|
|
|
581
579
|
return gcm.decrypt(wrapped_key)
|
|
582
580
|
|
|
583
581
|
def _ensure_client_keypair(self, session_key_type):
|
|
584
|
-
"""
|
|
585
|
-
Ensure client keypair is generated and stored.
|
|
586
|
-
"""
|
|
582
|
+
"""Ensure client keypair is generated and stored."""
|
|
587
583
|
if session_key_type == RSA_KEY_TYPE:
|
|
588
584
|
if self.decryptor is None:
|
|
589
585
|
private_key, public_key = CryptoUtils.generate_rsa_keypair()
|
|
@@ -600,15 +596,13 @@ class KASClient:
|
|
|
600
596
|
self.client_public_key = CryptoUtils.get_rsa_public_key_pem(public_key)
|
|
601
597
|
|
|
602
598
|
def _parse_and_decrypt_response(self, response):
|
|
603
|
-
"""
|
|
604
|
-
Parse JSON response and decrypt the wrapped key.
|
|
605
|
-
"""
|
|
599
|
+
"""Parse JSON response and decrypt the wrapped key."""
|
|
606
600
|
try:
|
|
607
601
|
response_data = response.json()
|
|
608
602
|
except Exception as e:
|
|
609
603
|
logging.error(f"Failed to parse JSON response: {e}")
|
|
610
604
|
logging.error(f"Raw response content: {response.content}")
|
|
611
|
-
raise SDKException(f"Invalid JSON response from KAS: {e}")
|
|
605
|
+
raise SDKException(f"Invalid JSON response from KAS: {e}") from e
|
|
612
606
|
|
|
613
607
|
entity_wrapped_key = response_data.get("entityWrappedKey")
|
|
614
608
|
if not entity_wrapped_key:
|
|
@@ -621,8 +615,7 @@ class KASClient:
|
|
|
621
615
|
return self.decryptor.decrypt(encrypted_key)
|
|
622
616
|
|
|
623
617
|
def unwrap(self, key_access, policy_json, session_key_type=None) -> bytes:
|
|
624
|
-
"""
|
|
625
|
-
Unwrap a key using Connect RPC.
|
|
618
|
+
"""Unwrap a key using Connect RPC.
|
|
626
619
|
|
|
627
620
|
Args:
|
|
628
621
|
key_access: Key access information
|
|
@@ -631,6 +624,7 @@ class KASClient:
|
|
|
631
624
|
|
|
632
625
|
Returns:
|
|
633
626
|
Unwrapped key bytes
|
|
627
|
+
|
|
634
628
|
"""
|
|
635
629
|
# Default to RSA if not specified
|
|
636
630
|
if session_key_type is None:
|
|
@@ -655,15 +649,14 @@ class KASClient:
|
|
|
655
649
|
def _unwrap_with_connect_rpc(
|
|
656
650
|
self, key_access, signed_token, session_key_type=None
|
|
657
651
|
) -> bytes:
|
|
658
|
-
"""
|
|
659
|
-
Connect RPC method for unwrapping keys.
|
|
652
|
+
"""Connect RPC method for unwrapping keys.
|
|
660
653
|
|
|
661
654
|
Args:
|
|
662
655
|
key_access: KeyAccess object
|
|
663
656
|
signed_token: Signed JWT token
|
|
664
657
|
session_key_type: Optional session key type (RSA_KEY_TYPE or EC_KEY_TYPE)
|
|
665
|
-
"""
|
|
666
658
|
|
|
659
|
+
"""
|
|
667
660
|
# Get access token for authentication if token source is available
|
|
668
661
|
access_token = None
|
|
669
662
|
if self.token_source:
|
|
@@ -702,8 +695,8 @@ class KASClient:
|
|
|
702
695
|
|
|
703
696
|
except Exception as e:
|
|
704
697
|
logging.error(f"Connect RPC rewrap failed: {e}")
|
|
705
|
-
raise SDKException(f"Connect RPC rewrap failed: {e}")
|
|
698
|
+
raise SDKException(f"Connect RPC rewrap failed: {e}") from e
|
|
706
699
|
|
|
707
700
|
def get_key_cache(self) -> KASKeyCache:
|
|
708
|
-
"""
|
|
701
|
+
"""Return the KAS key cache used for storing and retrieving encryption keys."""
|
|
709
702
|
return self.cache
|
|
@@ -1,5 +1,4 @@
|
|
|
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
|
|
|
@@ -15,27 +14,25 @@ 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
|
|
32
29
|
|
|
33
30
|
def _create_http_client(self):
|
|
34
|
-
"""
|
|
35
|
-
Create HTTP client with SSL verification configuration.
|
|
31
|
+
"""Create HTTP client with SSL verification configuration.
|
|
36
32
|
|
|
37
33
|
Returns:
|
|
38
34
|
urllib3.PoolManager configured for SSL verification settings
|
|
35
|
+
|
|
39
36
|
"""
|
|
40
37
|
if self.verify_ssl:
|
|
41
38
|
logging.info("Using SSL verification enabled HTTP client")
|
|
@@ -46,14 +43,14 @@ class KASConnectRPCClient:
|
|
|
46
43
|
return urllib3.PoolManager(cert_reqs="CERT_NONE")
|
|
47
44
|
|
|
48
45
|
def _prepare_connect_rpc_url(self, kas_url):
|
|
49
|
-
"""
|
|
50
|
-
Prepare the base URL for Connect RPC client.
|
|
46
|
+
"""Prepare the base URL for Connect RPC client.
|
|
51
47
|
|
|
52
48
|
Args:
|
|
53
49
|
kas_url: The normalized KAS URL
|
|
54
50
|
|
|
55
51
|
Returns:
|
|
56
52
|
Base URL for Connect RPC client (without /kas suffix)
|
|
53
|
+
|
|
57
54
|
"""
|
|
58
55
|
connect_rpc_base_url = kas_url
|
|
59
56
|
# Remove /kas suffix, if present
|
|
@@ -61,14 +58,14 @@ class KASConnectRPCClient:
|
|
|
61
58
|
return connect_rpc_base_url
|
|
62
59
|
|
|
63
60
|
def _prepare_auth_headers(self, access_token):
|
|
64
|
-
"""
|
|
65
|
-
Prepare authentication headers if access token is available.
|
|
61
|
+
"""Prepare authentication headers if access token is available.
|
|
66
62
|
|
|
67
63
|
Args:
|
|
68
64
|
access_token: Bearer token for authentication
|
|
69
65
|
|
|
70
66
|
Returns:
|
|
71
67
|
Dictionary with authentication headers or None
|
|
68
|
+
|
|
72
69
|
"""
|
|
73
70
|
if access_token:
|
|
74
71
|
auth_headers = AuthHeaders(
|
|
@@ -79,8 +76,7 @@ class KASConnectRPCClient:
|
|
|
79
76
|
return None
|
|
80
77
|
|
|
81
78
|
def get_public_key(self, normalized_kas_url, kas_info, access_token=None):
|
|
82
|
-
"""
|
|
83
|
-
Get KAS public key using Connect RPC.
|
|
79
|
+
"""Get KAS public key using Connect RPC.
|
|
84
80
|
|
|
85
81
|
Args:
|
|
86
82
|
normalized_kas_url: The normalized KAS URL
|
|
@@ -89,6 +85,7 @@ class KASConnectRPCClient:
|
|
|
89
85
|
|
|
90
86
|
Returns:
|
|
91
87
|
Updated kas_info with public_key and kid
|
|
88
|
+
|
|
92
89
|
"""
|
|
93
90
|
logging.info(
|
|
94
91
|
f"KAS Connect RPC client settings for public key retrieval: "
|
|
@@ -138,13 +135,12 @@ class KASConnectRPCClient:
|
|
|
138
135
|
f"Connect RPC public key request failed: {type(e).__name__}: {e}"
|
|
139
136
|
)
|
|
140
137
|
logging.error(f"Full traceback: {error_details}")
|
|
141
|
-
raise SDKException(f"Connect RPC public key request failed: {e}")
|
|
138
|
+
raise SDKException(f"Connect RPC public key request failed: {e}") from e
|
|
142
139
|
|
|
143
140
|
def unwrap_key(
|
|
144
141
|
self, normalized_kas_url, key_access, signed_token, access_token=None
|
|
145
142
|
):
|
|
146
|
-
"""
|
|
147
|
-
Unwrap a key using Connect RPC.
|
|
143
|
+
"""Unwrap a key using Connect RPC.
|
|
148
144
|
|
|
149
145
|
Args:
|
|
150
146
|
normalized_kas_url: The normalized KAS URL
|
|
@@ -154,6 +150,7 @@ class KASConnectRPCClient:
|
|
|
154
150
|
|
|
155
151
|
Returns:
|
|
156
152
|
Unwrapped key bytes from the response
|
|
153
|
+
|
|
157
154
|
"""
|
|
158
155
|
logging.info(
|
|
159
156
|
f"KAS Connect RPC client settings for unwrap: "
|
|
@@ -210,4 +207,4 @@ class KASConnectRPCClient:
|
|
|
210
207
|
|
|
211
208
|
except Exception as e:
|
|
212
209
|
logging.error(f"Connect RPC rewrap failed: {e}")
|
|
213
|
-
raise SDKException(f"Connect RPC rewrap failed: {e}")
|
|
210
|
+
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)
|
otdf_python/kas_key_cache.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"""
|
|
2
|
-
KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info.
|
|
3
|
-
"""
|
|
1
|
+
"""KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info."""
|
|
4
2
|
|
|
5
3
|
import threading
|
|
6
4
|
from typing import Any
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class KASKeyCache:
|
|
8
|
+
"""In-memory cache for KAS public keys and information."""
|
|
9
|
+
|
|
10
10
|
def __init__(self):
|
|
11
|
+
"""Initialize KAS key cache."""
|
|
11
12
|
self._cache = {}
|
|
12
13
|
self._lock = threading.Lock()
|
|
13
14
|
|
|
14
15
|
def get(self, url: str, algorithm: str | None = None) -> Any | None:
|
|
15
|
-
"""
|
|
16
|
-
Gets a KASInfo object from the cache based on URL and algorithm.
|
|
16
|
+
"""Get a KASInfo object from cache based on URL and algorithm.
|
|
17
17
|
|
|
18
18
|
Args:
|
|
19
19
|
url: The URL of the KAS
|
|
@@ -21,17 +21,18 @@ class KASKeyCache:
|
|
|
21
21
|
|
|
22
22
|
Returns:
|
|
23
23
|
The cached KASInfo object, or None if not found
|
|
24
|
+
|
|
24
25
|
"""
|
|
25
26
|
cache_key = self._make_key(url, algorithm)
|
|
26
27
|
with self._lock:
|
|
27
28
|
return self._cache.get(cache_key)
|
|
28
29
|
|
|
29
30
|
def store(self, kas_info) -> None:
|
|
30
|
-
"""
|
|
31
|
-
Stores a KASInfo object in the cache.
|
|
31
|
+
"""Store a KASInfo object in cache.
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
34
|
kas_info: The KASInfo object to store
|
|
35
|
+
|
|
35
36
|
"""
|
|
36
37
|
cache_key = self._make_key(kas_info.url, getattr(kas_info, "algorithm", None))
|
|
37
38
|
with self._lock:
|
|
@@ -43,10 +44,10 @@ class KASKeyCache:
|
|
|
43
44
|
self._cache[key] = value
|
|
44
45
|
|
|
45
46
|
def clear(self):
|
|
46
|
-
"""
|
|
47
|
+
"""Clear the cache."""
|
|
47
48
|
with self._lock:
|
|
48
49
|
self._cache.clear()
|
|
49
50
|
|
|
50
51
|
def _make_key(self, url: str, algorithm: str | None = None) -> str:
|
|
51
|
-
"""
|
|
52
|
+
"""Create a cache key from URL and algorithm."""
|
|
52
53
|
return f"{url}:{algorithm or ''}"
|
otdf_python/key_type.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Constants for session key types used in the KAS client.
|
|
1
|
+
"""Constants for session key types used in the KAS client.
|
|
3
2
|
This matches the Java SDK's KeyType enum pattern.
|
|
4
3
|
"""
|
|
5
4
|
|
|
@@ -7,9 +6,7 @@ from enum import Enum, auto
|
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class KeyType(Enum):
|
|
10
|
-
"""
|
|
11
|
-
Enum for key types used in the KAS client.
|
|
12
|
-
"""
|
|
9
|
+
"""Enum for key types used in the KAS client."""
|
|
13
10
|
|
|
14
11
|
RSA2048 = auto()
|
|
15
12
|
EC_P256 = auto()
|
|
@@ -18,16 +15,12 @@ class KeyType(Enum):
|
|
|
18
15
|
|
|
19
16
|
@property
|
|
20
17
|
def is_ec(self):
|
|
21
|
-
"""
|
|
22
|
-
Returns True if this key type is an EC key, False otherwise.
|
|
23
|
-
"""
|
|
18
|
+
"""Returns True if this key type is an EC key, False otherwise."""
|
|
24
19
|
return self in [KeyType.EC_P256, KeyType.EC_P384, KeyType.EC_P521]
|
|
25
20
|
|
|
26
21
|
@property
|
|
27
22
|
def curve_name(self):
|
|
28
|
-
"""
|
|
29
|
-
Returns the curve name for EC keys.
|
|
30
|
-
"""
|
|
23
|
+
"""Returns the curve name for EC keys."""
|
|
31
24
|
if self == KeyType.EC_P256:
|
|
32
25
|
return "P-256"
|
|
33
26
|
elif self == KeyType.EC_P384:
|