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.
Files changed (96) hide show
  1. otdf_python/__init__.py +1 -2
  2. otdf_python/__main__.py +1 -2
  3. otdf_python/address_normalizer.py +8 -10
  4. otdf_python/aesgcm.py +8 -0
  5. otdf_python/assertion_config.py +21 -0
  6. otdf_python/asym_crypto.py +18 -22
  7. otdf_python/auth_headers.py +7 -6
  8. otdf_python/autoconfigure_utils.py +21 -7
  9. otdf_python/cli.py +5 -5
  10. otdf_python/collection_store.py +13 -1
  11. otdf_python/collection_store_impl.py +5 -0
  12. otdf_python/config.py +13 -0
  13. otdf_python/connect_client.py +1 -0
  14. otdf_python/constants.py +2 -0
  15. otdf_python/crypto_utils.py +4 -0
  16. otdf_python/dpop.py +3 -5
  17. otdf_python/ecc_constants.py +12 -14
  18. otdf_python/ecc_mode.py +7 -2
  19. otdf_python/ecdh.py +24 -31
  20. otdf_python/eckeypair.py +5 -0
  21. otdf_python/header.py +5 -0
  22. otdf_python/invalid_zip_exception.py +6 -2
  23. otdf_python/kas_client.py +66 -55
  24. otdf_python/kas_connect_rpc_client.py +75 -38
  25. otdf_python/kas_info.py +4 -3
  26. otdf_python/kas_key_cache.py +10 -9
  27. otdf_python/key_type.py +4 -0
  28. otdf_python/key_type_constants.py +4 -11
  29. otdf_python/manifest.py +24 -0
  30. otdf_python/nanotdf.py +30 -28
  31. otdf_python/nanotdf_ecdsa_struct.py +5 -11
  32. otdf_python/nanotdf_type.py +13 -1
  33. otdf_python/policy_binding_serializer.py +6 -4
  34. otdf_python/policy_info.py +6 -0
  35. otdf_python/policy_object.py +8 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +22 -13
  38. otdf_python/sdk.py +51 -73
  39. otdf_python/sdk_builder.py +60 -47
  40. otdf_python/sdk_exceptions.py +11 -1
  41. otdf_python/symmetric_and_payload_config.py +6 -0
  42. otdf_python/tdf.py +47 -10
  43. otdf_python/tdf_reader.py +10 -13
  44. otdf_python/tdf_writer.py +5 -0
  45. otdf_python/token_source.py +4 -3
  46. otdf_python/version.py +5 -0
  47. otdf_python/zip_reader.py +10 -2
  48. otdf_python/zip_writer.py +11 -0
  49. {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/METADATA +3 -2
  50. {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/RECORD +81 -72
  51. {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/WHEEL +1 -1
  52. otdf_python_proto/__init__.py +2 -6
  53. otdf_python_proto/authorization/__init__.py +10 -0
  54. otdf_python_proto/authorization/authorization_connect.py +250 -0
  55. otdf_python_proto/authorization/v2/authorization_connect.py +315 -0
  56. otdf_python_proto/entityresolution/__init__.py +10 -0
  57. otdf_python_proto/entityresolution/entity_resolution_connect.py +185 -0
  58. otdf_python_proto/entityresolution/v2/entity_resolution_connect.py +185 -0
  59. otdf_python_proto/kas/__init__.py +2 -2
  60. otdf_python_proto/kas/kas_connect.py +259 -0
  61. otdf_python_proto/policy/actions/__init__.py +11 -0
  62. otdf_python_proto/policy/actions/actions_connect.py +380 -0
  63. otdf_python_proto/policy/attributes/__init__.py +11 -0
  64. otdf_python_proto/policy/attributes/attributes_connect.py +1310 -0
  65. otdf_python_proto/policy/kasregistry/__init__.py +11 -0
  66. otdf_python_proto/policy/kasregistry/key_access_server_registry_connect.py +912 -0
  67. otdf_python_proto/policy/keymanagement/__init__.py +11 -0
  68. otdf_python_proto/policy/keymanagement/key_management_connect.py +380 -0
  69. otdf_python_proto/policy/namespaces/__init__.py +11 -0
  70. otdf_python_proto/policy/namespaces/namespaces_connect.py +648 -0
  71. otdf_python_proto/policy/registeredresources/__init__.py +11 -0
  72. otdf_python_proto/policy/registeredresources/registered_resources_connect.py +770 -0
  73. otdf_python_proto/policy/resourcemapping/__init__.py +11 -0
  74. otdf_python_proto/policy/resourcemapping/resource_mapping_connect.py +790 -0
  75. otdf_python_proto/policy/subjectmapping/__init__.py +11 -0
  76. otdf_python_proto/policy/subjectmapping/subject_mapping_connect.py +851 -0
  77. otdf_python_proto/policy/unsafe/__init__.py +11 -0
  78. otdf_python_proto/policy/unsafe/unsafe_connect.py +705 -0
  79. otdf_python_proto/wellknownconfiguration/__init__.py +10 -0
  80. otdf_python_proto/wellknownconfiguration/wellknown_configuration_connect.py +124 -0
  81. otdf_python_proto/authorization/authorization_pb2_connect.py +0 -191
  82. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +0 -233
  83. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +0 -149
  84. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +0 -149
  85. otdf_python_proto/kas/kas_pb2_connect.py +0 -192
  86. otdf_python_proto/policy/actions/actions_pb2_connect.py +0 -275
  87. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +0 -863
  88. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +0 -611
  89. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +0 -275
  90. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +0 -443
  91. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +0 -527
  92. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +0 -527
  93. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +0 -569
  94. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +0 -485
  95. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +0 -107
  96. {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 _normalize_kas_url(self, url: str) -> str:
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
- Normalize KAS URLs based on client security settings.
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}]", e)
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", e)
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
- """Returns the KAS key cache used for storing and retrieving encryption keys."""
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 urllib3
7
+ import httpx
9
8
  from otdf_python_proto.kas import kas_pb2
10
- from otdf_python_proto.kas.kas_pb2_connect import AccessServiceClient
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 _create_http_client(self):
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
- Create HTTP client with SSL verification configuration.
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
- urllib3.PoolManager configured for SSL verification settings
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 urllib3.PoolManager()
60
+ return httpx.Client()
43
61
  else:
44
62
  logging.info("Using SSL verification disabled HTTP client")
45
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
46
- return urllib3.PoolManager(cert_reqs="CERT_NONE")
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
- Prepare the base URL for Connect RPC client.
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
- # Create Connect RPC client with configured HTTP client using Connect protocol
110
- # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
111
- client = AccessServiceClient(connect_rpc_base_url, http_client=http_client)
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
- extra_headers = self._prepare_auth_headers(access_token)
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, extra_headers=extra_headers)
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
- # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
174
- client = AccessServiceClient(kas_service_url, http_client=http_client)
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
- extra_headers = self._prepare_auth_headers(access_token)
222
+ headers = self._prepare_auth_headers(access_token)
186
223
 
187
224
  # Make the rewrap call with authentication headers
188
- response = client.rewrap(request, extra_headers=extra_headers)
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
- """Creates a copy of this KASInfo object."""
20
+ """Create a copy of this KASInfo object."""
20
21
  from copy import copy
21
22
 
22
23
  return copy(self)