otdf-python 0.4.1__py3-none-any.whl → 0.4.3__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 (61) hide show
  1. otdf_python/autoconfigure_utils.py +0 -2
  2. otdf_python/cli.py +50 -21
  3. otdf_python/collection_store.py +0 -1
  4. otdf_python/ecdh.py +0 -6
  5. otdf_python/kas_allowlist.py +182 -0
  6. otdf_python/kas_client.py +44 -2
  7. otdf_python/kas_connect_rpc_client.py +59 -19
  8. otdf_python/nanotdf.py +4 -14
  9. otdf_python/nanotdf_ecdsa_struct.py +0 -2
  10. otdf_python/nanotdf_type.py +1 -1
  11. otdf_python/sdk.py +31 -15
  12. otdf_python/sdk_builder.py +88 -8
  13. otdf_python/tdf.py +2 -2
  14. {otdf_python-0.4.1.dist-info → otdf_python-0.4.3.dist-info}/METADATA +3 -2
  15. {otdf_python-0.4.1.dist-info → otdf_python-0.4.3.dist-info}/RECORD +46 -36
  16. otdf_python_proto/__init__.py +2 -6
  17. otdf_python_proto/authorization/__init__.py +10 -0
  18. otdf_python_proto/authorization/authorization_connect.py +250 -0
  19. otdf_python_proto/authorization/v2/authorization_connect.py +315 -0
  20. otdf_python_proto/entityresolution/__init__.py +10 -0
  21. otdf_python_proto/entityresolution/entity_resolution_connect.py +185 -0
  22. otdf_python_proto/entityresolution/v2/entity_resolution_connect.py +185 -0
  23. otdf_python_proto/kas/__init__.py +2 -2
  24. otdf_python_proto/kas/kas_connect.py +259 -0
  25. otdf_python_proto/policy/actions/__init__.py +11 -0
  26. otdf_python_proto/policy/actions/actions_connect.py +380 -0
  27. otdf_python_proto/policy/attributes/__init__.py +11 -0
  28. otdf_python_proto/policy/attributes/attributes_connect.py +1310 -0
  29. otdf_python_proto/policy/kasregistry/__init__.py +11 -0
  30. otdf_python_proto/policy/kasregistry/key_access_server_registry_connect.py +912 -0
  31. otdf_python_proto/policy/keymanagement/__init__.py +11 -0
  32. otdf_python_proto/policy/keymanagement/key_management_connect.py +380 -0
  33. otdf_python_proto/policy/namespaces/__init__.py +11 -0
  34. otdf_python_proto/policy/namespaces/namespaces_connect.py +648 -0
  35. otdf_python_proto/policy/registeredresources/__init__.py +11 -0
  36. otdf_python_proto/policy/registeredresources/registered_resources_connect.py +770 -0
  37. otdf_python_proto/policy/resourcemapping/__init__.py +11 -0
  38. otdf_python_proto/policy/resourcemapping/resource_mapping_connect.py +790 -0
  39. otdf_python_proto/policy/subjectmapping/__init__.py +11 -0
  40. otdf_python_proto/policy/subjectmapping/subject_mapping_connect.py +851 -0
  41. otdf_python_proto/policy/unsafe/__init__.py +11 -0
  42. otdf_python_proto/policy/unsafe/unsafe_connect.py +705 -0
  43. otdf_python_proto/wellknownconfiguration/__init__.py +10 -0
  44. otdf_python_proto/wellknownconfiguration/wellknown_configuration_connect.py +124 -0
  45. otdf_python_proto/authorization/authorization_pb2_connect.py +0 -191
  46. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +0 -233
  47. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +0 -149
  48. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +0 -149
  49. otdf_python_proto/kas/kas_pb2_connect.py +0 -192
  50. otdf_python_proto/policy/actions/actions_pb2_connect.py +0 -275
  51. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +0 -863
  52. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +0 -611
  53. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +0 -275
  54. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +0 -443
  55. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +0 -527
  56. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +0 -527
  57. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +0 -569
  58. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +0 -485
  59. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +0 -107
  60. {otdf_python-0.4.1.dist-info → otdf_python-0.4.3.dist-info}/WHEEL +0 -0
  61. {otdf_python-0.4.1.dist-info → otdf_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
@@ -39,8 +39,6 @@ class KeySplitStep:
39
39
  class AutoConfigureException(Exception):
40
40
  """Exception for auto-configuration errors."""
41
41
 
42
- pass
43
-
44
42
 
45
43
  class AttributeNameFQN:
46
44
  """Fully qualified attribute name."""
otdf_python/cli.py CHANGED
@@ -112,33 +112,14 @@ def load_client_credentials(creds_file_path: str) -> tuple[str, str]:
112
112
  ) from e
113
113
 
114
114
 
115
- def build_sdk(args) -> SDK:
116
- """Build SDK instance from CLI arguments."""
117
- builder = SDKBuilder()
118
-
119
- if args.platform_url:
120
- builder.set_platform_endpoint(args.platform_url)
121
-
122
- # Auto-detect HTTP URLs and enable plaintext mode
123
- if args.platform_url.startswith("http://") and (
124
- not hasattr(args, "plaintext") or not args.plaintext
125
- ):
126
- logger.debug(
127
- f"Auto-detected HTTP URL {args.platform_url}, enabling plaintext mode"
128
- )
129
- builder.use_insecure_plaintext_connection(True)
130
-
131
- if args.oidc_endpoint:
132
- builder.set_issuer_endpoint(args.oidc_endpoint)
133
-
115
+ def _configure_auth(builder: SDKBuilder, args) -> None:
116
+ """Configure authentication on the SDK builder."""
134
117
  if args.client_id and args.client_secret:
135
118
  builder.client_secret(args.client_id, args.client_secret)
136
119
  elif hasattr(args, "with_client_creds_file") and args.with_client_creds_file:
137
- # Load credentials from file
138
120
  client_id, client_secret = load_client_credentials(args.with_client_creds_file)
139
121
  builder.client_secret(client_id, client_secret)
140
122
  elif hasattr(args, "auth") and args.auth:
141
- # Parse combined auth string (clientId:clientSecret) - legacy support
142
123
  auth_parts = args.auth.split(":")
143
124
  if len(auth_parts) != 2:
144
125
  raise CLIError(
@@ -152,12 +133,49 @@ def build_sdk(args) -> SDK:
152
133
  "Authentication required: provide --with-client-creds-file OR --client-id and --client-secret",
153
134
  )
154
135
 
136
+
137
+ def _configure_kas_allowlist(builder: SDKBuilder, args) -> None:
138
+ """Configure KAS allowlist on the SDK builder."""
139
+ if hasattr(args, "ignore_kas_allowlist") and args.ignore_kas_allowlist:
140
+ logger.warning(
141
+ "KAS allowlist validation is disabled. This may leak credentials "
142
+ "to malicious servers if decrypting untrusted TDF files."
143
+ )
144
+ builder.with_ignore_kas_allowlist(True)
145
+ elif hasattr(args, "kas_allowlist") and args.kas_allowlist:
146
+ kas_urls = [url.strip() for url in args.kas_allowlist.split(",") if url.strip()]
147
+ logger.debug(f"Using KAS allowlist: {kas_urls}")
148
+ builder.with_kas_allowlist(kas_urls)
149
+
150
+
151
+ def build_sdk(args) -> SDK:
152
+ """Build SDK instance from CLI arguments."""
153
+ builder = SDKBuilder()
154
+
155
+ if args.platform_url:
156
+ builder.set_platform_endpoint(args.platform_url)
157
+ # Auto-detect HTTP URLs and enable plaintext mode
158
+ if args.platform_url.startswith("http://") and (
159
+ not hasattr(args, "plaintext") or not args.plaintext
160
+ ):
161
+ logger.debug(
162
+ f"Auto-detected HTTP URL {args.platform_url}, enabling plaintext mode"
163
+ )
164
+ builder.use_insecure_plaintext_connection(True)
165
+
166
+ if args.oidc_endpoint:
167
+ builder.set_issuer_endpoint(args.oidc_endpoint)
168
+
169
+ _configure_auth(builder, args)
170
+
155
171
  if hasattr(args, "plaintext") and args.plaintext:
156
172
  builder.use_insecure_plaintext_connection(True)
157
173
 
158
174
  if args.insecure:
159
175
  builder.use_insecure_skip_verify(True)
160
176
 
177
+ _configure_kas_allowlist(builder, args)
178
+
161
179
  return builder.build()
162
180
 
163
181
 
@@ -476,6 +494,17 @@ Where creds.json contains:
476
494
  security_group.add_argument(
477
495
  "--insecure", action="store_true", help="Skip TLS verification"
478
496
  )
497
+ security_group.add_argument(
498
+ "--kas-allowlist",
499
+ help="Comma-separated list of trusted KAS URLs. "
500
+ "By default, only the platform URL's KAS endpoint is trusted.",
501
+ )
502
+ security_group.add_argument(
503
+ "--ignore-kas-allowlist",
504
+ action="store_true",
505
+ help="WARNING: Disable KAS allowlist validation. This is insecure and "
506
+ "should only be used for testing. May leak credentials to malicious servers.",
507
+ )
479
508
 
480
509
  # Subcommands
481
510
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
@@ -28,7 +28,6 @@ class NoOpCollectionStore(CollectionStore):
28
28
 
29
29
  def store(self, header, key: CollectionKey):
30
30
  """Discard key operation (no-op)."""
31
- pass
32
31
 
33
32
  def get_key(self, header) -> CollectionKey:
34
33
  return self.NO_PRIVATE_KEY
otdf_python/ecdh.py CHANGED
@@ -35,20 +35,14 @@ NANOTDF_HKDF_SALT = bytes.fromhex(
35
35
  class ECDHError(Exception):
36
36
  """Base exception for ECDH operations."""
37
37
 
38
- pass
39
-
40
38
 
41
39
  class UnsupportedCurveError(ECDHError):
42
40
  """Raised when an unsupported curve is specified."""
43
41
 
44
- pass
45
-
46
42
 
47
43
  class InvalidKeyError(ECDHError):
48
44
  """Raised when a key is invalid or malformed."""
49
45
 
50
- pass
51
-
52
46
 
53
47
  def get_curve(curve_name: str) -> ec.EllipticCurve:
54
48
  """Get the cryptography curve object for a given curve name.
@@ -0,0 +1,182 @@
1
+ """KAS Allowlist: Validates KAS URLs against a list of trusted hosts.
2
+
3
+ This module provides protection against SSRF attacks where malicious TDF files
4
+ could contain attacker-controlled KAS URLs to steal OIDC credentials.
5
+ """
6
+
7
+ import logging
8
+ from urllib.parse import urlparse
9
+
10
+
11
+ class KASAllowlist:
12
+ """Validates KAS URLs against an allowlist of trusted hosts.
13
+
14
+ This class prevents credential theft by ensuring the SDK only sends
15
+ authentication tokens to trusted KAS endpoints.
16
+
17
+ Example:
18
+ allowlist = KASAllowlist(["https://kas.example.com"])
19
+ allowlist.is_allowed("https://kas.example.com/kas") # True
20
+ allowlist.is_allowed("https://evil.com/kas") # False
21
+
22
+ """
23
+
24
+ def __init__(self, allowed_urls: list[str] | None = None, allow_all: bool = False):
25
+ """Initialize the KAS allowlist.
26
+
27
+ Args:
28
+ allowed_urls: List of trusted KAS URLs. Each URL is normalized to
29
+ its origin (scheme://host:port) for comparison.
30
+ allow_all: If True, all URLs are allowed. Use only for testing.
31
+ A warning is logged when this is enabled.
32
+
33
+ """
34
+ self._allowed_origins: set[str] = set()
35
+ self._allow_all = allow_all
36
+
37
+ if allow_all:
38
+ logging.warning(
39
+ "KAS allowlist is disabled (allow_all=True). "
40
+ "This is insecure and should only be used for testing."
41
+ )
42
+
43
+ if allowed_urls:
44
+ for url in allowed_urls:
45
+ self.add(url)
46
+
47
+ def add(self, url: str) -> None:
48
+ """Add a URL to the allowlist.
49
+
50
+ The URL is normalized to its origin (scheme://host:port) before storage.
51
+ Paths and query strings are stripped.
52
+
53
+ Args:
54
+ url: The KAS URL to allow. Can include path components which
55
+ will be stripped for origin comparison.
56
+
57
+ """
58
+ origin = self._get_origin(url)
59
+ self._allowed_origins.add(origin)
60
+ logging.debug(f"Added KAS origin to allowlist: {origin}")
61
+
62
+ def is_allowed(self, url: str) -> bool:
63
+ """Check if a URL is allowed by the allowlist.
64
+
65
+ Args:
66
+ url: The KAS URL to check.
67
+
68
+ Returns:
69
+ True if the URL's origin is in the allowlist or allow_all is True.
70
+ False otherwise.
71
+
72
+ """
73
+ if self._allow_all:
74
+ logging.debug(f"KAS URL allowed (allow_all=True): {url}")
75
+ return True
76
+
77
+ if not self._allowed_origins:
78
+ logging.debug(f"KAS URL rejected (empty allowlist): {url}")
79
+ return False
80
+
81
+ origin = self._get_origin(url)
82
+ allowed = origin in self._allowed_origins
83
+ if allowed:
84
+ logging.debug(f"KAS URL allowed: {url} (origin: {origin})")
85
+ else:
86
+ logging.debug(
87
+ f"KAS URL rejected: {url} (origin: {origin}, "
88
+ f"allowed: {self._allowed_origins})"
89
+ )
90
+ return allowed
91
+
92
+ def validate(self, url: str) -> None:
93
+ """Validate a URL against the allowlist, raising an exception if not allowed.
94
+
95
+ Args:
96
+ url: The KAS URL to validate.
97
+
98
+ Raises:
99
+ SDK.KasAllowlistException: If the URL is not in the allowlist.
100
+
101
+ """
102
+ if not self.is_allowed(url):
103
+ # Import here to avoid circular imports
104
+ from .sdk import SDK
105
+
106
+ raise SDK.KasAllowlistException(url, self._allowed_origins)
107
+
108
+ @property
109
+ def allowed_origins(self) -> set[str]:
110
+ """Return the set of allowed origins (read-only copy)."""
111
+ return self._allowed_origins.copy()
112
+
113
+ @property
114
+ def allow_all(self) -> bool:
115
+ """Return whether all URLs are allowed."""
116
+ return self._allow_all
117
+
118
+ @staticmethod
119
+ def _get_origin(url: str) -> str:
120
+ """Extract the origin (scheme://host:port) from a URL.
121
+
122
+ This normalizes URLs for comparison by stripping paths and query strings.
123
+ Default ports (80 for http, 443 for https) are included explicitly.
124
+
125
+ Args:
126
+ url: The URL to extract the origin from.
127
+
128
+ Returns:
129
+ Normalized origin string in format scheme://host:port
130
+
131
+ """
132
+ # Add scheme if missing
133
+ if "://" not in url:
134
+ url = "https://" + url
135
+
136
+ try:
137
+ parsed = urlparse(url)
138
+ except Exception as e:
139
+ logging.warning(f"Failed to parse URL {url}: {e}")
140
+ # Return the URL as-is if parsing fails
141
+ return url.lower()
142
+
143
+ scheme = (parsed.scheme or "https").lower()
144
+ hostname = (parsed.hostname or "").lower()
145
+
146
+ if not hostname:
147
+ # URL might be malformed, return as-is
148
+ logging.warning(f"Could not extract hostname from URL: {url}")
149
+ return url.lower()
150
+
151
+ # Determine port (use explicit port or default for scheme)
152
+ if parsed.port:
153
+ port = parsed.port
154
+ elif scheme == "http":
155
+ port = 80
156
+ else:
157
+ port = 443
158
+
159
+ return f"{scheme}://{hostname}:{port}"
160
+
161
+ @classmethod
162
+ def from_platform_url(cls, platform_url: str) -> "KASAllowlist":
163
+ """Create an allowlist from a platform URL.
164
+
165
+ This is the default behavior: auto-allow the platform's KAS endpoint.
166
+
167
+ Args:
168
+ platform_url: The OpenTDF platform URL. The KAS endpoint is
169
+ assumed to be at {platform_url}/kas.
170
+
171
+ Returns:
172
+ KASAllowlist configured to allow the platform's KAS endpoint.
173
+
174
+ """
175
+ allowlist = cls()
176
+ # Add the platform URL itself (KAS might be at root or /kas)
177
+ allowlist.add(platform_url)
178
+ # Also construct the /kas endpoint explicitly
179
+ kas_url = platform_url.rstrip("/") + "/kas"
180
+ allowlist.add(kas_url)
181
+ logging.info(f"Created KAS allowlist from platform URL: {platform_url}")
182
+ return allowlist
otdf_python/kas_client.py CHANGED
@@ -38,13 +38,26 @@ class KASClient:
38
38
  cache=None,
39
39
  use_plaintext=False,
40
40
  verify_ssl=True,
41
+ kas_allowlist=None,
41
42
  ):
42
- """Initialize KAS client."""
43
+ """Initialize KAS client.
44
+
45
+ Args:
46
+ kas_url: Default KAS URL
47
+ token_source: Function that returns an authentication token
48
+ cache: Optional KASKeyCache for caching public keys
49
+ use_plaintext: Whether to use HTTP instead of HTTPS
50
+ verify_ssl: Whether to verify SSL certificates
51
+ kas_allowlist: Optional KASAllowlist for URL validation. If provided,
52
+ only URLs in the allowlist will be contacted.
53
+
54
+ """
43
55
  self.kas_url = kas_url
44
56
  self.token_source = token_source
45
57
  self.cache = cache or KASKeyCache()
46
58
  self.use_plaintext = use_plaintext
47
59
  self.verify_ssl = verify_ssl
60
+ self.kas_allowlist = kas_allowlist
48
61
  self.decryptor = None
49
62
  self.client_public_key = None
50
63
 
@@ -65,18 +78,47 @@ class KASClient:
65
78
  self._dpop_public_key
66
79
  )
67
80
 
81
+ def __enter__(self):
82
+ """Enter context manager."""
83
+ return self
84
+
85
+ def __exit__(self, exc_type, exc_val, exc_tb):
86
+ """Exit context manager and clean up resources."""
87
+ self.close()
88
+
89
+ def close(self):
90
+ """Close the KAS client and release resources.
91
+
92
+ This method should be called when the client is no longer needed
93
+ to properly clean up resources. It's also called automatically
94
+ when using the client as a context manager.
95
+ """
96
+ if self.connect_rpc_client:
97
+ self.connect_rpc_client.close()
98
+
68
99
  def _normalize_kas_url(self, url: str) -> str:
69
100
  """Normalize KAS URLs based on client security settings.
70
101
 
102
+ This method also validates the URL against the KAS allowlist if one
103
+ is configured. This prevents SSRF attacks where malicious TDF files
104
+ could contain attacker-controlled KAS URLs to steal OIDC credentials.
105
+
71
106
  Args:
72
107
  url: The KAS URL to normalize
73
108
 
74
109
  Returns:
75
110
  Normalized URL with appropriate protocol and port
76
111
 
112
+ Raises:
113
+ KASAllowlistException: If the URL is not in the allowlist
114
+
77
115
  """
78
116
  from urllib.parse import urlparse
79
117
 
118
+ # Validate against allowlist BEFORE making any requests
119
+ if self.kas_allowlist is not None:
120
+ self.kas_allowlist.validate(url)
121
+
80
122
  try:
81
123
  # Parse the URL
82
124
  parsed = urlparse(url)
@@ -136,7 +178,7 @@ class KASClient:
136
178
  # Reconstruct URL preserving the path (especially /kas prefix)
137
179
  try:
138
180
  # Create URL preserving the path component for proper endpoint routing
139
- path = parsed.path if parsed.path else ""
181
+ path = parsed.path or ""
140
182
  normalized_url = f"{scheme}://{parsed.hostname}:{port}{path}"
141
183
  logging.debug(f"normalized url [{parsed.geturl()}] to [{normalized_url}]")
142
184
  return normalized_url
@@ -4,9 +4,9 @@ This class encapsulates all interactions with otdf_python_proto.
4
4
 
5
5
  import logging
6
6
 
7
- import urllib3
7
+ import httpx
8
8
  from otdf_python_proto.kas import kas_pb2
9
- from otdf_python_proto.kas.kas_pb2_connect import AccessServiceClient
9
+ from otdf_python_proto.kas.kas_connect import AccessServiceClientSync
10
10
 
11
11
  from otdf_python.auth_headers import AuthHeaders
12
12
 
@@ -26,21 +26,56 @@ class KASConnectRPCClient:
26
26
  """
27
27
  self.use_plaintext = use_plaintext
28
28
  self.verify_ssl = verify_ssl
29
+ self._http_client = None
30
+
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.
46
+ """
47
+ if self._http_client is not None:
48
+ self._http_client.close()
49
+ self._http_client = None
29
50
 
30
51
  def _create_http_client(self):
31
52
  """Create HTTP client with SSL verification configuration.
32
53
 
33
54
  Returns:
34
- urllib3.PoolManager configured for SSL verification settings
55
+ httpx.Client configured for SSL verification settings
35
56
 
36
57
  """
37
58
  if self.verify_ssl:
38
59
  logging.info("Using SSL verification enabled HTTP client")
39
- return urllib3.PoolManager()
60
+ return httpx.Client()
40
61
  else:
41
62
  logging.info("Using SSL verification disabled HTTP client")
42
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
43
- 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
70
+
71
+ """
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
44
79
 
45
80
  def _prepare_connect_rpc_url(self, kas_url):
46
81
  """Prepare the base URL for Connect RPC client.
@@ -93,8 +128,6 @@ class KASConnectRPCClient:
93
128
  f"kas_url={kas_info.url}"
94
129
  )
95
130
 
96
- http_client = self._create_http_client()
97
-
98
131
  try:
99
132
  connect_rpc_base_url = self._prepare_connect_rpc_url(normalized_kas_url)
100
133
 
@@ -103,9 +136,13 @@ class KASConnectRPCClient:
103
136
  f"for public key retrieval"
104
137
  )
105
138
 
106
- # Create Connect RPC client with configured HTTP client using Connect protocol
107
- # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
108
- 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
+ )
109
146
 
110
147
  # Create public key request
111
148
  algorithm = getattr(kas_info, "algorithm", "") or ""
@@ -116,10 +153,10 @@ class KASConnectRPCClient:
116
153
  )
117
154
 
118
155
  # Prepare headers with authentication if available
119
- extra_headers = self._prepare_auth_headers(access_token)
156
+ headers = self._prepare_auth_headers(access_token)
120
157
 
121
158
  # Make the public key call with authentication headers
122
- response = client.public_key(request, extra_headers=extra_headers)
159
+ response = client.public_key(request, headers=headers)
123
160
 
124
161
  # Update kas_info with response
125
162
  kas_info.public_key = response.public_key
@@ -158,8 +195,6 @@ class KASConnectRPCClient:
158
195
  f"kas_url={key_access.url}"
159
196
  )
160
197
 
161
- http_client = self._create_http_client()
162
-
163
198
  try:
164
199
  kas_service_url = self._prepare_connect_rpc_url(normalized_kas_url)
165
200
 
@@ -167,8 +202,13 @@ class KASConnectRPCClient:
167
202
  f"Creating Connect RPC client for base URL: {kas_service_url}, for unwrap"
168
203
  )
169
204
 
170
- # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
171
- 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
+ )
172
212
 
173
213
  # Create rewrap request
174
214
  request = kas_pb2.RewrapRequest(
@@ -179,10 +219,10 @@ class KASConnectRPCClient:
179
219
  logging.info(f"Connect RPC signed token: {signed_token}")
180
220
 
181
221
  # Prepare headers with authentication if available
182
- extra_headers = self._prepare_auth_headers(access_token)
222
+ headers = self._prepare_auth_headers(access_token)
183
223
 
184
224
  # Make the rewrap call with authentication headers
185
- response = client.rewrap(request, extra_headers=extra_headers)
225
+ response = client.rewrap(request, headers=headers)
186
226
 
187
227
  # Extract the entity wrapped key from v2 response structure
188
228
  # The v2 response has responses[] array with results[] for each policy
otdf_python/nanotdf.py CHANGED
@@ -28,26 +28,18 @@ from .asym_crypto import AsymDecryption
28
28
  class NanoTDFException(SDKException):
29
29
  """Base exception for NanoTDF operations."""
30
30
 
31
- pass
32
-
33
31
 
34
32
  class NanoTDFMaxSizeLimit(NanoTDFException):
35
33
  """Exception for NanoTDF size limit exceeded."""
36
34
 
37
- pass
38
-
39
35
 
40
36
  class UnsupportedNanoTDFFeature(NanoTDFException):
41
37
  """Exception for unsupported NanoTDF features."""
42
38
 
43
- pass
44
-
45
39
 
46
40
  class InvalidNanoTDFConfig(NanoTDFException):
47
41
  """Exception for invalid NanoTDF configuration."""
48
42
 
49
- pass
50
-
51
43
 
52
44
  class NanoTDF:
53
45
  """NanoTDF reader and writer for compact TDF format."""
@@ -78,8 +70,8 @@ class NanoTDF:
78
70
  if isinstance(obj, PolicyBody):
79
71
  # Convert data_attributes to dataAttributes and use null instead of empty array
80
72
  result = {
81
- "dataAttributes": obj.data_attributes if obj.data_attributes else None,
82
- "dissem": obj.dissem if obj.dissem else None,
73
+ "dataAttributes": obj.data_attributes or None,
74
+ "dissem": obj.dissem or None,
83
75
  }
84
76
  return result
85
77
  elif isinstance(obj, AttributeObject):
@@ -123,14 +115,12 @@ class NanoTDF:
123
115
  tuple: (policy_body, policy_type)
124
116
 
125
117
  """
126
- attributes = config.attributes if config.attributes else []
118
+ attributes = config.attributes or []
127
119
  policy_object = self._create_policy_object(attributes)
128
120
  policy_json = json.dumps(
129
121
  policy_object, default=self._serialize_policy_object
130
122
  ).encode("utf-8")
131
- policy_type = (
132
- config.policy_type if config.policy_type else "EMBEDDED_POLICY_PLAIN_TEXT"
133
- )
123
+ policy_type = config.policy_type or "EMBEDDED_POLICY_PLAIN_TEXT"
134
124
 
135
125
  if policy_type == "EMBEDDED_POLICY_PLAIN_TEXT":
136
126
  policy_body = policy_json
@@ -6,8 +6,6 @@ from dataclasses import dataclass, field
6
6
  class IncorrectNanoTDFECDSASignatureSize(Exception):
7
7
  """Exception raised when the signature size is incorrect."""
8
8
 
9
- pass
10
-
11
9
 
12
10
  @dataclass
13
11
  class NanoTDFECDSAStruct:
@@ -8,7 +8,7 @@ class ECCurve(Enum):
8
8
 
9
9
  SECP256R1 = "secp256r1"
10
10
  SECP384R1 = "secp384r1"
11
- SECP521R1 = "secp384r1"
11
+ SECP521R1 = "secp521r1"
12
12
  SECP256K1 = "secp256k1"
13
13
 
14
14
  def __str__(self):