otdf-python 0.1.9__py3-none-any.whl → 0.3.0__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 +25 -0
- otdf_python/__main__.py +12 -0
- otdf_python/address_normalizer.py +84 -0
- otdf_python/aesgcm.py +55 -0
- otdf_python/assertion_config.py +84 -0
- otdf_python/asym_crypto.py +85 -0
- otdf_python/asym_decryption.py +53 -0
- otdf_python/asym_encryption.py +75 -0
- otdf_python/auth_headers.py +21 -0
- otdf_python/autoconfigure_utils.py +113 -0
- otdf_python/cli.py +570 -0
- otdf_python/collection_store.py +41 -0
- otdf_python/collection_store_impl.py +22 -0
- otdf_python/config.py +69 -0
- otdf_python/connect_client.py +0 -0
- otdf_python/constants.py +1 -0
- otdf_python/crypto_utils.py +78 -0
- otdf_python/dpop.py +81 -0
- otdf_python/ecc_mode.py +32 -0
- otdf_python/eckeypair.py +75 -0
- otdf_python/header.py +143 -0
- otdf_python/invalid_zip_exception.py +8 -0
- otdf_python/kas_client.py +603 -0
- otdf_python/kas_connect_rpc_client.py +207 -0
- otdf_python/kas_info.py +25 -0
- otdf_python/kas_key_cache.py +52 -0
- otdf_python/key_type.py +31 -0
- otdf_python/key_type_constants.py +43 -0
- otdf_python/manifest.py +215 -0
- otdf_python/nanotdf.py +553 -0
- otdf_python/nanotdf_ecdsa_struct.py +132 -0
- otdf_python/nanotdf_type.py +43 -0
- otdf_python/policy_binding_serializer.py +39 -0
- otdf_python/policy_info.py +78 -0
- otdf_python/policy_object.py +22 -0
- otdf_python/policy_stub.py +2 -0
- otdf_python/resource_locator.py +44 -0
- otdf_python/sdk.py +528 -0
- otdf_python/sdk_builder.py +448 -0
- otdf_python/sdk_exceptions.py +16 -0
- otdf_python/symmetric_and_payload_config.py +30 -0
- otdf_python/tdf.py +479 -0
- otdf_python/tdf_reader.py +153 -0
- otdf_python/tdf_writer.py +23 -0
- otdf_python/token_source.py +34 -0
- otdf_python/version.py +57 -0
- otdf_python/zip_reader.py +47 -0
- otdf_python/zip_writer.py +70 -0
- otdf_python-0.3.0.dist-info/METADATA +231 -0
- otdf_python-0.3.0.dist-info/RECORD +137 -0
- {otdf_python-0.1.9.dist-info → otdf_python-0.3.0.dist-info}/WHEEL +1 -2
- {otdf_python-0.1.9.dist-info → otdf_python-0.3.0.dist-info/licenses}/LICENSE +1 -1
- otdf_python_proto/__init__.py +37 -0
- otdf_python_proto/authorization/__init__.py +1 -0
- otdf_python_proto/authorization/authorization_pb2.py +80 -0
- otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
- otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
- otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
- otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
- otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
- otdf_python_proto/common/__init__.py +1 -0
- otdf_python_proto/common/common_pb2.py +52 -0
- otdf_python_proto/common/common_pb2.pyi +61 -0
- otdf_python_proto/entity/__init__.py +1 -0
- otdf_python_proto/entity/entity_pb2.py +47 -0
- otdf_python_proto/entity/entity_pb2.pyi +50 -0
- otdf_python_proto/entityresolution/__init__.py +1 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
- otdf_python_proto/kas/__init__.py +9 -0
- otdf_python_proto/kas/kas_pb2.py +103 -0
- otdf_python_proto/kas/kas_pb2.pyi +170 -0
- otdf_python_proto/kas/kas_pb2_connect.py +192 -0
- otdf_python_proto/legacy_grpc/__init__.py +1 -0
- otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
- otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
- otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
- otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
- otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
- otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
- otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
- otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
- otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
- otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
- otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
- otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
- otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
- otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
- otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
- otdf_python_proto/logger/__init__.py +1 -0
- otdf_python_proto/logger/audit/test_pb2.py +43 -0
- otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
- otdf_python_proto/policy/__init__.py +1 -0
- otdf_python_proto/policy/actions/actions_pb2.py +75 -0
- otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
- otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
- otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
- otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
- otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
- otdf_python_proto/policy/objects_pb2.py +150 -0
- otdf_python_proto/policy/objects_pb2.pyi +464 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
- otdf_python_proto/policy/selectors_pb2.py +57 -0
- otdf_python_proto/policy/selectors_pb2.pyi +90 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
- otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
- otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
- otdf_python/build.py +0 -190
- otdf_python/go.py +0 -1478
- otdf_python/gotdf_python.py +0 -383
- otdf_python-0.1.9.dist-info/METADATA +0 -149
- otdf_python-0.1.9.dist-info/RECORD +0 -10
- otdf_python-0.1.9.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KASConnectRPCClient: Handles Connect RPC communication with the Key Access Service (KAS).
|
|
3
|
+
This class encapsulates all interactions with otdf_python_proto.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import urllib3
|
|
9
|
+
from otdf_python_proto.kas import kas_pb2
|
|
10
|
+
from otdf_python_proto.kas.kas_pb2_connect import AccessServiceClient
|
|
11
|
+
|
|
12
|
+
from .sdk_exceptions import SDKException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KASConnectRPCClient:
|
|
16
|
+
"""
|
|
17
|
+
Handles Connect RPC communication with KAS service using otdf_python_proto.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, use_plaintext=False, verify_ssl=True):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the Connect RPC client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
use_plaintext: Whether to use plaintext (HTTP) connections
|
|
26
|
+
verify_ssl: Whether to verify SSL certificates
|
|
27
|
+
"""
|
|
28
|
+
self.use_plaintext = use_plaintext
|
|
29
|
+
self.verify_ssl = verify_ssl
|
|
30
|
+
|
|
31
|
+
def _create_http_client(self):
|
|
32
|
+
"""
|
|
33
|
+
Create HTTP client with SSL verification configuration.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
urllib3.PoolManager configured for SSL verification settings
|
|
37
|
+
"""
|
|
38
|
+
if self.verify_ssl:
|
|
39
|
+
logging.info("Using SSL verification enabled HTTP client")
|
|
40
|
+
return urllib3.PoolManager()
|
|
41
|
+
else:
|
|
42
|
+
logging.info("Using SSL verification disabled HTTP client")
|
|
43
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
44
|
+
return urllib3.PoolManager(cert_reqs="CERT_NONE")
|
|
45
|
+
|
|
46
|
+
def _prepare_connect_rpc_url(self, kas_url):
|
|
47
|
+
"""
|
|
48
|
+
Prepare the base URL for Connect RPC client.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
kas_url: The normalized KAS URL
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Base URL for Connect RPC client (without /kas suffix)
|
|
55
|
+
"""
|
|
56
|
+
connect_rpc_base_url = kas_url
|
|
57
|
+
# Remove /kas suffix, if present
|
|
58
|
+
connect_rpc_base_url = connect_rpc_base_url.removesuffix("/kas")
|
|
59
|
+
return connect_rpc_base_url
|
|
60
|
+
|
|
61
|
+
def _prepare_auth_headers(self, access_token):
|
|
62
|
+
"""
|
|
63
|
+
Prepare authentication headers if access token is available.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
access_token: Bearer token for authentication
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary with authentication headers or None
|
|
70
|
+
"""
|
|
71
|
+
if access_token:
|
|
72
|
+
return {"Authorization": f"Bearer {access_token}"}
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def get_public_key(self, normalized_kas_url, kas_info, access_token=None):
|
|
76
|
+
"""
|
|
77
|
+
Get KAS public key using Connect RPC.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
normalized_kas_url: The normalized KAS URL
|
|
81
|
+
kas_info: KAS information object with algorithm
|
|
82
|
+
access_token: Optional access token for authentication
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Updated kas_info with public_key and kid
|
|
86
|
+
"""
|
|
87
|
+
logging.info(
|
|
88
|
+
f"KAS Connect RPC client settings for public key retrieval: "
|
|
89
|
+
f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
|
|
90
|
+
f"kas_url={kas_info.url}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
http_client = self._create_http_client()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
connect_rpc_base_url = self._prepare_connect_rpc_url(normalized_kas_url)
|
|
97
|
+
|
|
98
|
+
logging.info(
|
|
99
|
+
f"Creating Connect RPC client for base URL: {connect_rpc_base_url}, "
|
|
100
|
+
f"for public key retrieval"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Create Connect RPC client with configured HTTP client using Connect protocol
|
|
104
|
+
# Note: gRPC protocol is not supported with urllib3, use default Connect protocol
|
|
105
|
+
client = AccessServiceClient(connect_rpc_base_url, http_client=http_client)
|
|
106
|
+
|
|
107
|
+
# Create public key request
|
|
108
|
+
algorithm = getattr(kas_info, "algorithm", "") or ""
|
|
109
|
+
request = (
|
|
110
|
+
kas_pb2.PublicKeyRequest(algorithm=algorithm)
|
|
111
|
+
if algorithm
|
|
112
|
+
else kas_pb2.PublicKeyRequest()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Prepare headers with authentication if available
|
|
116
|
+
extra_headers = self._prepare_auth_headers(access_token)
|
|
117
|
+
|
|
118
|
+
# Make the public key call with authentication headers
|
|
119
|
+
response = client.public_key(request, extra_headers=extra_headers)
|
|
120
|
+
|
|
121
|
+
# Update kas_info with response
|
|
122
|
+
kas_info.public_key = response.public_key
|
|
123
|
+
kas_info.kid = response.kid
|
|
124
|
+
|
|
125
|
+
return kas_info
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
import traceback
|
|
129
|
+
|
|
130
|
+
error_details = traceback.format_exc()
|
|
131
|
+
logging.error(
|
|
132
|
+
f"Connect RPC public key request failed: {type(e).__name__}: {e}"
|
|
133
|
+
)
|
|
134
|
+
logging.error(f"Full traceback: {error_details}")
|
|
135
|
+
raise SDKException(f"Connect RPC public key request failed: {e}")
|
|
136
|
+
|
|
137
|
+
def unwrap_key(
|
|
138
|
+
self, normalized_kas_url, key_access, signed_token, access_token=None
|
|
139
|
+
):
|
|
140
|
+
"""
|
|
141
|
+
Unwrap a key using Connect RPC.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
normalized_kas_url: The normalized KAS URL
|
|
145
|
+
key_access: Key access information
|
|
146
|
+
signed_token: Signed JWT token for the request
|
|
147
|
+
access_token: Optional access token for authentication
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Unwrapped key bytes from the response
|
|
151
|
+
"""
|
|
152
|
+
logging.info(
|
|
153
|
+
f"KAS Connect RPC client settings for unwrap: "
|
|
154
|
+
f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
|
|
155
|
+
f"kas_url={key_access.url}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
http_client = self._create_http_client()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
kas_service_url = self._prepare_connect_rpc_url(normalized_kas_url)
|
|
162
|
+
|
|
163
|
+
logging.info(
|
|
164
|
+
f"Creating Connect RPC client for base URL: {kas_service_url}, for unwrap"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Note: gRPC protocol is not supported with urllib3, use default Connect protocol
|
|
168
|
+
client = AccessServiceClient(kas_service_url, http_client=http_client)
|
|
169
|
+
|
|
170
|
+
# Create rewrap request
|
|
171
|
+
request = kas_pb2.RewrapRequest(
|
|
172
|
+
signed_request_token=signed_token,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Debug: Log the signed token details
|
|
176
|
+
logging.info(f"Connect RPC signed token: {signed_token}")
|
|
177
|
+
|
|
178
|
+
# Prepare headers with authentication if available
|
|
179
|
+
extra_headers = self._prepare_auth_headers(access_token)
|
|
180
|
+
|
|
181
|
+
# Make the rewrap call with authentication headers
|
|
182
|
+
response = client.rewrap(request, extra_headers=extra_headers)
|
|
183
|
+
|
|
184
|
+
# Extract the entity wrapped key from v2 response structure
|
|
185
|
+
# The v2 response has responses[] array with results[] for each policy
|
|
186
|
+
if response.responses and len(response.responses) > 0:
|
|
187
|
+
policy_result = response.responses[0] # First policy
|
|
188
|
+
if policy_result.results and len(policy_result.results) > 0:
|
|
189
|
+
kao_result = policy_result.results[0] # First KAO result
|
|
190
|
+
if kao_result.kas_wrapped_key:
|
|
191
|
+
entity_wrapped_key = kao_result.kas_wrapped_key
|
|
192
|
+
else:
|
|
193
|
+
raise SDKException(f"KAO result error: {kao_result.error}")
|
|
194
|
+
else:
|
|
195
|
+
raise SDKException("No KAO results in policy response")
|
|
196
|
+
else:
|
|
197
|
+
# Fallback to legacy entity_wrapped_key field for backward compatibility
|
|
198
|
+
entity_wrapped_key = response.entity_wrapped_key
|
|
199
|
+
if not entity_wrapped_key:
|
|
200
|
+
raise SDKException("No entity_wrapped_key in Connect RPC response")
|
|
201
|
+
|
|
202
|
+
logging.info("Connect RPC rewrap succeeded")
|
|
203
|
+
return entity_wrapped_key
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logging.error(f"Connect RPC rewrap failed: {e}")
|
|
207
|
+
raise SDKException(f"Connect RPC rewrap failed: {e}")
|
otdf_python/kas_info.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class KASInfo:
|
|
6
|
+
"""
|
|
7
|
+
Configuration for Key Access Server (KAS) information.
|
|
8
|
+
This class stores details about a Key Access Server including its URL,
|
|
9
|
+
public key, key ID, default status, and cryptographic algorithm.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
url: str
|
|
13
|
+
public_key: str | None = None
|
|
14
|
+
kid: str | None = None
|
|
15
|
+
default: bool | None = None
|
|
16
|
+
algorithm: str | None = None
|
|
17
|
+
|
|
18
|
+
def clone(self):
|
|
19
|
+
"""Creates a copy of this KASInfo object."""
|
|
20
|
+
from copy import copy
|
|
21
|
+
|
|
22
|
+
return copy(self)
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return f"KASInfo(url={self.url}, kid={self.kid}, default={self.default}, algorithm={self.algorithm})"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KASKeyCache:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self._cache = {}
|
|
12
|
+
self._lock = threading.Lock()
|
|
13
|
+
|
|
14
|
+
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.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
url: The URL of the KAS
|
|
20
|
+
algorithm: Optional algorithm identifier
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The cached KASInfo object, or None if not found
|
|
24
|
+
"""
|
|
25
|
+
cache_key = self._make_key(url, algorithm)
|
|
26
|
+
with self._lock:
|
|
27
|
+
return self._cache.get(cache_key)
|
|
28
|
+
|
|
29
|
+
def store(self, kas_info) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Stores a KASInfo object in the cache.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
kas_info: The KASInfo object to store
|
|
35
|
+
"""
|
|
36
|
+
cache_key = self._make_key(kas_info.url, getattr(kas_info, "algorithm", None))
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._cache[cache_key] = kas_info
|
|
39
|
+
|
|
40
|
+
def set(self, key, value):
|
|
41
|
+
"""Store a key-value pair in the cache."""
|
|
42
|
+
with self._lock:
|
|
43
|
+
self._cache[key] = value
|
|
44
|
+
|
|
45
|
+
def clear(self):
|
|
46
|
+
"""Clears the cache"""
|
|
47
|
+
with self._lock:
|
|
48
|
+
self._cache.clear()
|
|
49
|
+
|
|
50
|
+
def _make_key(self, url: str, algorithm: str | None = None) -> str:
|
|
51
|
+
"""Creates a cache key from URL and algorithm"""
|
|
52
|
+
return f"{url}:{algorithm or ''}"
|
otdf_python/key_type.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KeyType(Enum):
|
|
5
|
+
RSA2048Key = "rsa:2048"
|
|
6
|
+
EC256Key = "ec:secp256r1"
|
|
7
|
+
EC384Key = "ec:secp384r1"
|
|
8
|
+
EC521Key = "ec:secp521r1"
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return self.value
|
|
12
|
+
|
|
13
|
+
def get_curve_name(self):
|
|
14
|
+
if self == KeyType.EC256Key:
|
|
15
|
+
return "secp256r1"
|
|
16
|
+
elif self == KeyType.EC384Key:
|
|
17
|
+
return "secp384r1"
|
|
18
|
+
elif self == KeyType.EC521Key:
|
|
19
|
+
return "secp521r1"
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Unsupported key type: {self}")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def from_string(key_type):
|
|
25
|
+
for t in KeyType:
|
|
26
|
+
if t.value.lower() == key_type.lower():
|
|
27
|
+
return t
|
|
28
|
+
raise ValueError(f"No enum constant for key type: {key_type}")
|
|
29
|
+
|
|
30
|
+
def is_ec(self):
|
|
31
|
+
return self != KeyType.RSA2048Key
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants for session key types used in the KAS client.
|
|
3
|
+
This matches the Java SDK's KeyType enum pattern.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KeyType(Enum):
|
|
10
|
+
"""
|
|
11
|
+
Enum for key types used in the KAS client.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
RSA2048 = auto()
|
|
15
|
+
EC_P256 = auto()
|
|
16
|
+
EC_P384 = auto()
|
|
17
|
+
EC_P521 = auto()
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def is_ec(self):
|
|
21
|
+
"""
|
|
22
|
+
Returns True if this key type is an EC key, False otherwise.
|
|
23
|
+
"""
|
|
24
|
+
return self in [KeyType.EC_P256, KeyType.EC_P384, KeyType.EC_P521]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def curve_name(self):
|
|
28
|
+
"""
|
|
29
|
+
Returns the curve name for EC keys.
|
|
30
|
+
"""
|
|
31
|
+
if self == KeyType.EC_P256:
|
|
32
|
+
return "P-256"
|
|
33
|
+
elif self == KeyType.EC_P384:
|
|
34
|
+
return "P-384"
|
|
35
|
+
elif self == KeyType.EC_P521:
|
|
36
|
+
return "P-521"
|
|
37
|
+
else:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Constants for backward compatibility with string literals
|
|
42
|
+
RSA_KEY_TYPE = KeyType.RSA2048
|
|
43
|
+
EC_KEY_TYPE = KeyType.EC_P256 # Default EC curve
|
otdf_python/manifest.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ManifestSegment:
|
|
8
|
+
hash: str
|
|
9
|
+
segmentSize: int
|
|
10
|
+
encryptedSegmentSize: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ManifestRootSignature:
|
|
15
|
+
alg: str
|
|
16
|
+
sig: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ManifestIntegrityInformation:
|
|
21
|
+
rootSignature: ManifestRootSignature
|
|
22
|
+
segmentHashAlg: str
|
|
23
|
+
segmentSizeDefault: int
|
|
24
|
+
encryptedSegmentSizeDefault: int
|
|
25
|
+
segments: list[ManifestSegment]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ManifestPolicyBinding:
|
|
30
|
+
alg: str
|
|
31
|
+
hash: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ManifestKeyAccess:
|
|
36
|
+
type: str
|
|
37
|
+
url: str
|
|
38
|
+
protocol: str
|
|
39
|
+
wrappedKey: str
|
|
40
|
+
policyBinding: Any = None
|
|
41
|
+
encryptedMetadata: str | None = None
|
|
42
|
+
kid: str | None = None
|
|
43
|
+
sid: str | None = None
|
|
44
|
+
schemaVersion: str | None = None
|
|
45
|
+
ephemeralPublicKey: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ManifestMethod:
|
|
50
|
+
algorithm: str
|
|
51
|
+
iv: str
|
|
52
|
+
isStreamable: bool | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ManifestEncryptionInformation:
|
|
57
|
+
type: str
|
|
58
|
+
policy: str
|
|
59
|
+
keyAccess: list[ManifestKeyAccess]
|
|
60
|
+
method: ManifestMethod
|
|
61
|
+
integrityInformation: ManifestIntegrityInformation
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ManifestPayload:
|
|
66
|
+
type: str
|
|
67
|
+
url: str
|
|
68
|
+
protocol: str
|
|
69
|
+
mimeType: str
|
|
70
|
+
isEncrypted: bool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ManifestBinding:
|
|
75
|
+
method: str
|
|
76
|
+
signature: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ManifestAssertion:
|
|
81
|
+
id: str
|
|
82
|
+
type: str
|
|
83
|
+
scope: str
|
|
84
|
+
appliesTo_state: str
|
|
85
|
+
statement: Any
|
|
86
|
+
binding: ManifestBinding | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Manifest:
|
|
91
|
+
schemaVersion: str | None = None
|
|
92
|
+
encryptionInformation: ManifestEncryptionInformation | None = None
|
|
93
|
+
payload: ManifestPayload | None = None
|
|
94
|
+
assertions: list[ManifestAssertion] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def _remove_none_values_and_empty_lists(self, obj):
|
|
97
|
+
"""Recursively remove None values and empty lists from dictionaries and lists."""
|
|
98
|
+
if isinstance(obj, dict):
|
|
99
|
+
cleaned = {}
|
|
100
|
+
for k, v in obj.items():
|
|
101
|
+
if v is not None:
|
|
102
|
+
# For 'assertions' field, exclude if it's an empty list
|
|
103
|
+
if k == "assertions" and isinstance(v, list) and len(v) == 0:
|
|
104
|
+
continue
|
|
105
|
+
cleaned[k] = self._remove_none_values_and_empty_lists(v)
|
|
106
|
+
return cleaned
|
|
107
|
+
elif isinstance(obj, list):
|
|
108
|
+
return [
|
|
109
|
+
self._remove_none_values_and_empty_lists(item)
|
|
110
|
+
for item in obj
|
|
111
|
+
if item is not None
|
|
112
|
+
]
|
|
113
|
+
else:
|
|
114
|
+
return obj
|
|
115
|
+
|
|
116
|
+
def to_json(self) -> str:
|
|
117
|
+
# Create manifest dict with fields ordered to match otdfctl expectations
|
|
118
|
+
# Order: encryptionInformation, payload, schemaVersion, assertions
|
|
119
|
+
manifest_dict = {}
|
|
120
|
+
|
|
121
|
+
# Add fields in the order expected by otdfctl
|
|
122
|
+
if self.encryptionInformation is not None:
|
|
123
|
+
manifest_dict["encryptionInformation"] = asdict(self.encryptionInformation)
|
|
124
|
+
|
|
125
|
+
if self.payload is not None:
|
|
126
|
+
manifest_dict["payload"] = asdict(self.payload)
|
|
127
|
+
|
|
128
|
+
if self.schemaVersion is not None:
|
|
129
|
+
manifest_dict["schemaVersion"] = self.schemaVersion
|
|
130
|
+
|
|
131
|
+
if self.assertions and len(self.assertions) > 0:
|
|
132
|
+
manifest_dict["assertions"] = [
|
|
133
|
+
asdict(assertion) for assertion in self.assertions
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
cleaned_dict = self._remove_none_values_and_empty_lists(manifest_dict)
|
|
137
|
+
return json.dumps(cleaned_dict, default=str)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def from_json(data: str) -> "Manifest":
|
|
141
|
+
d = json.loads(data)
|
|
142
|
+
|
|
143
|
+
# Recursively instantiate nested dataclasses
|
|
144
|
+
def _payload(p):
|
|
145
|
+
return ManifestPayload(**p) if p else None
|
|
146
|
+
|
|
147
|
+
def _segment(s):
|
|
148
|
+
return ManifestSegment(**s)
|
|
149
|
+
|
|
150
|
+
def _root_sig(rs):
|
|
151
|
+
return ManifestRootSignature(**rs)
|
|
152
|
+
|
|
153
|
+
def _integrity(i):
|
|
154
|
+
# Handle both snake_case and camelCase fields
|
|
155
|
+
# TODO: This can probably be simplified to only camelCase
|
|
156
|
+
return ManifestIntegrityInformation(
|
|
157
|
+
rootSignature=_root_sig(
|
|
158
|
+
i.get("rootSignature", i.get("root_signature"))
|
|
159
|
+
),
|
|
160
|
+
segmentHashAlg=i.get("segmentHashAlg", i.get("segment_hash_alg")),
|
|
161
|
+
segmentSizeDefault=i.get(
|
|
162
|
+
"segmentSizeDefault", i.get("segment_size_default")
|
|
163
|
+
),
|
|
164
|
+
encryptedSegmentSizeDefault=i.get(
|
|
165
|
+
"encryptedSegmentSizeDefault",
|
|
166
|
+
i.get("encrypted_segment_size_default"),
|
|
167
|
+
),
|
|
168
|
+
segments=[_segment(s) for s in i["segments"]],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _method(m):
|
|
172
|
+
return ManifestMethod(**m)
|
|
173
|
+
|
|
174
|
+
def _key_access(k):
|
|
175
|
+
return ManifestKeyAccess(**k)
|
|
176
|
+
|
|
177
|
+
def _enc_info(e):
|
|
178
|
+
# Handle both snake_case and camelCase fields
|
|
179
|
+
# TODO: This can probably be simplified to only camelCase
|
|
180
|
+
return ManifestEncryptionInformation(
|
|
181
|
+
type=e.get("type", e.get("key_access_type", "split")),
|
|
182
|
+
policy=e["policy"],
|
|
183
|
+
keyAccess=[
|
|
184
|
+
_key_access(k)
|
|
185
|
+
for k in e.get("keyAccess", e.get("key_access_obj", []))
|
|
186
|
+
],
|
|
187
|
+
method=_method(e["method"]),
|
|
188
|
+
integrityInformation=_integrity(
|
|
189
|
+
e.get("integrityInformation", e.get("integrity_information"))
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _binding(b):
|
|
194
|
+
return ManifestBinding(**b) if b else None
|
|
195
|
+
|
|
196
|
+
def _assertion(a):
|
|
197
|
+
return ManifestAssertion(
|
|
198
|
+
id=a["id"],
|
|
199
|
+
type=a["type"],
|
|
200
|
+
scope=a["scope"],
|
|
201
|
+
appliesTo_state=a.get("appliesTo_state", a.get("applies_to_state")),
|
|
202
|
+
statement=a["statement"],
|
|
203
|
+
binding=_binding(a.get("binding")),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return Manifest(
|
|
207
|
+
schemaVersion=d.get("schemaVersion", d.get("tdf_version")),
|
|
208
|
+
encryptionInformation=_enc_info(
|
|
209
|
+
d.get("encryptionInformation", d.get("encryption_information"))
|
|
210
|
+
)
|
|
211
|
+
if d.get("encryptionInformation") or d.get("encryption_information")
|
|
212
|
+
else None,
|
|
213
|
+
payload=_payload(d["payload"]) if d.get("payload") else None,
|
|
214
|
+
assertions=[_assertion(a) for a in d.get("assertions", [])],
|
|
215
|
+
)
|