otdf-python 0.11.0__py3-none-any.whl → 0.12.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/aesgcm.py +9 -2
- otdf_python/cli.py +5 -1
- otdf_python/collection_store.py +1 -1
- otdf_python/kas_client.py +1 -1
- otdf_python/manifest.py +18 -1
- otdf_python/nanotdf.py +2 -2
- otdf_python/policy_binding_serializer.py +2 -2
- otdf_python/policy_info.py +1 -1
- otdf_python/sdk.py +16 -7
- otdf_python/sdk_builder.py +3 -1
- otdf_python/tdf.py +10 -5
- otdf_python/tdf_reader.py +68 -11
- otdf_python/tdf_writer.py +3 -1
- {otdf_python-0.11.0.dist-info → otdf_python-0.12.0.dist-info}/METADATA +9 -3
- {otdf_python-0.11.0.dist-info → otdf_python-0.12.0.dist-info}/RECORD +16 -16
- {otdf_python-0.11.0.dist-info → otdf_python-0.12.0.dist-info}/WHEEL +1 -1
otdf_python/aesgcm.py
CHANGED
|
@@ -44,7 +44,9 @@ class AesGcm:
|
|
|
44
44
|
def encrypt_with_iv(
|
|
45
45
|
self,
|
|
46
46
|
iv: bytes,
|
|
47
|
-
|
|
47
|
+
# Kept for signature parity with the Java SDK; the cryptography lib
|
|
48
|
+
# derives the tag length itself.
|
|
49
|
+
auth_tag_len: int, # noqa: ARG002
|
|
48
50
|
plaintext: bytes,
|
|
49
51
|
offset: int = 0,
|
|
50
52
|
length: int | None = None,
|
|
@@ -58,6 +60,11 @@ class AesGcm:
|
|
|
58
60
|
return self.aesgcm.decrypt(encrypted.iv, encrypted.ciphertext, None)
|
|
59
61
|
|
|
60
62
|
def decrypt_with_iv(
|
|
61
|
-
self,
|
|
63
|
+
self,
|
|
64
|
+
iv: bytes,
|
|
65
|
+
# Kept for signature parity with the Java SDK; the cryptography lib
|
|
66
|
+
# derives the tag length itself.
|
|
67
|
+
auth_tag_len: int, # noqa: ARG002
|
|
68
|
+
cipher_data: bytes,
|
|
62
69
|
) -> bytes:
|
|
63
70
|
return self.aesgcm.decrypt(iv, cipher_data, None)
|
otdf_python/cli.py
CHANGED
|
@@ -205,12 +205,15 @@ def build_sdk(args) -> SDK:
|
|
|
205
205
|
|
|
206
206
|
# Features the Python SDK currently exercises honestly in community xtest Stage-1.
|
|
207
207
|
# Keep conservative: only advertise what encrypt/decrypt paths actually honor.
|
|
208
|
+
# "spec-container": writes manifest.json at the zip root and resolves the
|
|
209
|
+
# payload entry from manifest.payload.url (opentdf/spec container rules).
|
|
208
210
|
_SUPPORTED_FEATURES: frozenset[str] = frozenset(
|
|
209
211
|
{
|
|
210
212
|
"autoconfigure",
|
|
211
213
|
"connectrpc",
|
|
212
214
|
"hexless",
|
|
213
215
|
"kasallowlist",
|
|
216
|
+
"spec-container",
|
|
214
217
|
}
|
|
215
218
|
)
|
|
216
219
|
|
|
@@ -238,6 +241,7 @@ _KNOWN_FEATURES: frozenset[str] = frozenset(
|
|
|
238
241
|
"mechanism-mlkem",
|
|
239
242
|
"ns_grants",
|
|
240
243
|
"obligations",
|
|
244
|
+
"spec-container",
|
|
241
245
|
}
|
|
242
246
|
)
|
|
243
247
|
|
|
@@ -286,7 +290,7 @@ def create_tdf_config(sdk: SDK, args) -> TDFConfig:
|
|
|
286
290
|
return config
|
|
287
291
|
|
|
288
292
|
|
|
289
|
-
def create_nano_tdf_config(
|
|
293
|
+
def create_nano_tdf_config(_sdk: SDK, args) -> NanoTDFConfig:
|
|
290
294
|
"""Create NanoTDF configuration from CLI arguments."""
|
|
291
295
|
attributes = (
|
|
292
296
|
parse_attributes(args.attributes)
|
otdf_python/collection_store.py
CHANGED
|
@@ -29,7 +29,7 @@ class NoOpCollectionStore(CollectionStore):
|
|
|
29
29
|
def store(self, header, key: CollectionKey):
|
|
30
30
|
"""Discard key operation (no-op)."""
|
|
31
31
|
|
|
32
|
-
def get_key(self,
|
|
32
|
+
def get_key(self, _header) -> CollectionKey:
|
|
33
33
|
return self.NO_PRIVATE_KEY
|
|
34
34
|
|
|
35
35
|
|
otdf_python/kas_client.py
CHANGED
otdf_python/manifest.py
CHANGED
|
@@ -86,6 +86,8 @@ class ManifestPayload:
|
|
|
86
86
|
protocol: str
|
|
87
87
|
mimeType: str
|
|
88
88
|
isEncrypted: bool
|
|
89
|
+
# Read-only: some spec revisions place the version here. Never written.
|
|
90
|
+
tdf_spec_version: str | None = None
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
@dataclass
|
|
@@ -116,6 +118,18 @@ class Manifest:
|
|
|
116
118
|
encryptionInformation: ManifestEncryptionInformation | None = None
|
|
117
119
|
payload: ManifestPayload | None = None
|
|
118
120
|
assertions: list[ManifestAssertion] = field(default_factory=list)
|
|
121
|
+
# Read-only: spec prose places tdf_spec_version at the top level. Never written.
|
|
122
|
+
tdf_spec_version: str | None = None
|
|
123
|
+
|
|
124
|
+
def spec_version(self) -> str | None:
|
|
125
|
+
"""Resolve the spec version: schemaVersion, then tdf_spec_version, then payload.tdf_spec_version."""
|
|
126
|
+
if self.schemaVersion:
|
|
127
|
+
return self.schemaVersion
|
|
128
|
+
if self.tdf_spec_version:
|
|
129
|
+
return self.tdf_spec_version
|
|
130
|
+
if self.payload and self.payload.tdf_spec_version:
|
|
131
|
+
return self.payload.tdf_spec_version
|
|
132
|
+
return None
|
|
119
133
|
|
|
120
134
|
def _remove_none_values_and_empty_lists(self, obj):
|
|
121
135
|
"""Recursively remove None values and empty lists from dictionaries and lists."""
|
|
@@ -147,7 +161,9 @@ class Manifest:
|
|
|
147
161
|
manifest_dict["encryptionInformation"] = asdict(self.encryptionInformation)
|
|
148
162
|
|
|
149
163
|
if self.payload is not None:
|
|
150
|
-
|
|
164
|
+
payload_dict = asdict(self.payload)
|
|
165
|
+
payload_dict.pop("tdf_spec_version", None)
|
|
166
|
+
manifest_dict["payload"] = payload_dict
|
|
151
167
|
|
|
152
168
|
if self.schemaVersion is not None:
|
|
153
169
|
manifest_dict["schemaVersion"] = self.schemaVersion
|
|
@@ -229,6 +245,7 @@ class Manifest:
|
|
|
229
245
|
|
|
230
246
|
return Manifest(
|
|
231
247
|
schemaVersion=d.get("schemaVersion", d.get("tdf_version")),
|
|
248
|
+
tdf_spec_version=d.get("tdf_spec_version"),
|
|
232
249
|
encryptionInformation=_enc_info(
|
|
233
250
|
d.get("encryptionInformation", d.get("encryption_information"))
|
|
234
251
|
)
|
otdf_python/nanotdf.py
CHANGED
|
@@ -468,7 +468,7 @@ class NanoTDF:
|
|
|
468
468
|
return len(header_bytes) + 3 + payload_length
|
|
469
469
|
|
|
470
470
|
def _kas_unwrap(
|
|
471
|
-
self, nano_tdf_data: bytes, header_len: int,
|
|
471
|
+
self, nano_tdf_data: bytes, header_len: int, _wrapped_key: bytes
|
|
472
472
|
) -> bytes | None:
|
|
473
473
|
try:
|
|
474
474
|
# For NanoTDF, send the entire header to KAS
|
|
@@ -622,7 +622,7 @@ class NanoTDF:
|
|
|
622
622
|
# Try KAS unwrap first if services available
|
|
623
623
|
if self.services:
|
|
624
624
|
try:
|
|
625
|
-
key = self._kas_unwrap(nano_tdf_data, header_len,
|
|
625
|
+
key = self._kas_unwrap(nano_tdf_data, header_len, b"")
|
|
626
626
|
if key:
|
|
627
627
|
logging.info(
|
|
628
628
|
"Successfully unwrapped NanoTDF key via KAS (ECDH mode)"
|
|
@@ -25,7 +25,7 @@ class PolicyBindingSerializer:
|
|
|
25
25
|
|
|
26
26
|
@staticmethod
|
|
27
27
|
def deserialize(
|
|
28
|
-
json_data: Any,
|
|
28
|
+
json_data: Any, _typeof_t: type | None = None, _context: Any = None
|
|
29
29
|
) -> Any:
|
|
30
30
|
if isinstance(json_data, dict):
|
|
31
31
|
return PolicyBinding(**json_data)
|
|
@@ -35,7 +35,7 @@ class PolicyBindingSerializer:
|
|
|
35
35
|
|
|
36
36
|
@staticmethod
|
|
37
37
|
def serialize(
|
|
38
|
-
src: Any,
|
|
38
|
+
src: Any, _typeof_src: type | None = None, _context: Any = None
|
|
39
39
|
) -> dict | str:
|
|
40
40
|
if isinstance(src, PolicyBinding):
|
|
41
41
|
return vars(src)
|
otdf_python/policy_info.py
CHANGED
|
@@ -43,7 +43,7 @@ class PolicyInfo:
|
|
|
43
43
|
return offset - start
|
|
44
44
|
|
|
45
45
|
@staticmethod
|
|
46
|
-
def from_bytes_with_size(buffer: bytes,
|
|
46
|
+
def from_bytes_with_size(buffer: bytes, _ecc_mode):
|
|
47
47
|
# Parse policy_type (1 byte), body_len (2 bytes), body
|
|
48
48
|
# Note: binding is NOT part of PolicyInfo - it's read separately in Header
|
|
49
49
|
offset = 0
|
otdf_python/sdk.py
CHANGED
|
@@ -97,9 +97,9 @@ class KAS(AbstractContextManager):
|
|
|
97
97
|
|
|
98
98
|
def unwrap_nanotdf(
|
|
99
99
|
self,
|
|
100
|
-
curve: Any,
|
|
101
|
-
header: str,
|
|
102
|
-
kas_url: str,
|
|
100
|
+
curve: Any, # noqa: ARG002
|
|
101
|
+
header: str, # noqa: ARG002
|
|
102
|
+
kas_url: str, # noqa: ARG002
|
|
103
103
|
wrapped_key: bytes | None = None,
|
|
104
104
|
kas_private_key: str | None = None,
|
|
105
105
|
mock: bool = False,
|
|
@@ -368,13 +368,22 @@ class SDK(AbstractContextManager):
|
|
|
368
368
|
import zipfile
|
|
369
369
|
from io import BytesIO
|
|
370
370
|
|
|
371
|
+
from otdf_python.tdf_reader import (
|
|
372
|
+
payload_url_from_manifest_json,
|
|
373
|
+
resolve_manifest_name,
|
|
374
|
+
resolve_payload_name,
|
|
375
|
+
)
|
|
376
|
+
|
|
371
377
|
try:
|
|
372
378
|
file_like = BytesIO(data) if isinstance(data, bytes | bytearray) else data
|
|
373
379
|
with zipfile.ZipFile(file_like) as zf:
|
|
374
|
-
names =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
names = zf.namelist()
|
|
381
|
+
manifest_name = resolve_manifest_name(names)
|
|
382
|
+
url = payload_url_from_manifest_json(
|
|
383
|
+
zf.read(manifest_name).decode("utf-8")
|
|
384
|
+
)
|
|
385
|
+
resolve_payload_name(url, names)
|
|
386
|
+
return True
|
|
378
387
|
except Exception:
|
|
379
388
|
return False
|
|
380
389
|
|
otdf_python/sdk_builder.py
CHANGED
|
@@ -275,7 +275,9 @@ class SDKBuilder:
|
|
|
275
275
|
config_doc = response.json()
|
|
276
276
|
# Newer platform versions return the fields directly at the top
|
|
277
277
|
# level instead of nested under a "configuration" key.
|
|
278
|
-
configuration = config_doc.get("configuration")
|
|
278
|
+
configuration = config_doc.get("configuration")
|
|
279
|
+
if configuration is None:
|
|
280
|
+
configuration = config_doc
|
|
279
281
|
|
|
280
282
|
# Try to get token endpoint from IDP configuration
|
|
281
283
|
idp_config = configuration.get("idp", {})
|
otdf_python/tdf.py
CHANGED
|
@@ -28,6 +28,7 @@ from otdf_python.manifest import (
|
|
|
28
28
|
ManifestSegment,
|
|
29
29
|
)
|
|
30
30
|
from otdf_python.policy_stub import NULL_POLICY_UUID
|
|
31
|
+
from otdf_python.tdf_reader import resolve_manifest_name, resolve_payload_name
|
|
31
32
|
from otdf_python.tdf_writer import TDFWriter
|
|
32
33
|
|
|
33
34
|
|
|
@@ -399,7 +400,7 @@ class TDF:
|
|
|
399
400
|
)
|
|
400
401
|
payload_info = ManifestPayload(
|
|
401
402
|
type="reference", # Changed from "file" to "reference" to match Java SDK
|
|
402
|
-
url=
|
|
403
|
+
url=TDFWriter.TDF_PAYLOAD_FILE_NAME,
|
|
403
404
|
protocol="zip",
|
|
404
405
|
mimeType=config.mime_type, # Use MIME type from config
|
|
405
406
|
isEncrypted=True, # Changed from is_encrypted to isEncrypted
|
|
@@ -433,7 +434,8 @@ class TDF:
|
|
|
433
434
|
tdf_bytes_io = io.BytesIO(tdf_data) if isinstance(tdf_data, bytes) else tdf_data
|
|
434
435
|
|
|
435
436
|
with zipfile.ZipFile(tdf_bytes_io, "r") as z:
|
|
436
|
-
|
|
437
|
+
names = z.namelist()
|
|
438
|
+
manifest_json = z.read(resolve_manifest_name(names)).decode()
|
|
437
439
|
manifest = Manifest.from_json(manifest_json)
|
|
438
440
|
|
|
439
441
|
if not manifest.encryptionInformation:
|
|
@@ -464,7 +466,8 @@ class TDF:
|
|
|
464
466
|
segments = (
|
|
465
467
|
manifest.encryptionInformation.integrityInformation.segments
|
|
466
468
|
) # Changed field name
|
|
467
|
-
|
|
469
|
+
payload_url = manifest.payload.url if manifest.payload else None
|
|
470
|
+
encrypted_payload = z.read(resolve_payload_name(payload_url, names))
|
|
468
471
|
payload = self._decrypt_segments(aesgcm, segments, encrypted_payload)
|
|
469
472
|
return TDFReader(payload=payload, manifest=manifest)
|
|
470
473
|
|
|
@@ -487,7 +490,8 @@ class TDF:
|
|
|
487
490
|
from .asym_crypto import AsymDecryption
|
|
488
491
|
|
|
489
492
|
with zipfile.ZipFile(io.BytesIO(tdf_bytes), "r") as z:
|
|
490
|
-
|
|
493
|
+
names = z.namelist()
|
|
494
|
+
manifest_json = z.read(resolve_manifest_name(names)).decode()
|
|
491
495
|
manifest = Manifest.from_json(manifest_json)
|
|
492
496
|
|
|
493
497
|
if not manifest.encryptionInformation:
|
|
@@ -510,7 +514,8 @@ class TDF:
|
|
|
510
514
|
segments = (
|
|
511
515
|
manifest.encryptionInformation.integrityInformation.segments
|
|
512
516
|
) # Changed field names
|
|
513
|
-
|
|
517
|
+
payload_url = manifest.payload.url if manifest.payload else None
|
|
518
|
+
encrypted_payload = z.read(resolve_payload_name(payload_url, names))
|
|
514
519
|
offset = 0
|
|
515
520
|
for seg in segments:
|
|
516
521
|
enc_len = seg.encryptedSegmentSize # Changed field name
|
otdf_python/tdf_reader.py
CHANGED
|
@@ -1,15 +1,71 @@
|
|
|
1
1
|
"""TDFReader is responsible for reading and processing Trusted Data Format (TDF) files."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
3
6
|
from .manifest import Manifest
|
|
4
7
|
from .policy_object import PolicyObject
|
|
5
8
|
from .sdk_exceptions import SDKException
|
|
6
9
|
from .zip_reader import ZipReader
|
|
7
10
|
|
|
8
|
-
#
|
|
9
|
-
|
|
11
|
+
# Spec (opentdf/spec schema/OpenTDF/README.md): the manifest entry MUST be
|
|
12
|
+
# `manifest.json` at the archive root. `0.manifest.json` is what every SDK
|
|
13
|
+
# wrote before this change and is accepted forever on read.
|
|
14
|
+
TDF_MANIFEST_FILE_NAME = "manifest.json"
|
|
15
|
+
LEGACY_TDF_MANIFEST_FILE_NAME = "0.manifest.json"
|
|
16
|
+
# Payload entry name. Written by TDFWriter and into manifest.payload.url.
|
|
17
|
+
# On read this is only a fallback for manifests with an empty payload.url.
|
|
10
18
|
TDF_PAYLOAD_FILE_NAME = "0.payload"
|
|
11
19
|
|
|
12
20
|
|
|
21
|
+
def resolve_manifest_name(names: Iterable[str]) -> str:
|
|
22
|
+
"""Return the zip entry holding the manifest, spec name first."""
|
|
23
|
+
name_set = set(names)
|
|
24
|
+
for candidate in (TDF_MANIFEST_FILE_NAME, LEGACY_TDF_MANIFEST_FILE_NAME):
|
|
25
|
+
if candidate in name_set:
|
|
26
|
+
return candidate
|
|
27
|
+
raise ValueError("tdf doesn't contain a manifest")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_safe_entry_name(name: str) -> bool:
|
|
31
|
+
if not name or name.startswith("/") or "\\" in name:
|
|
32
|
+
return False
|
|
33
|
+
return ".." not in name.split("/")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_payload_name(payload_url: str | None, names: Iterable[str]) -> str:
|
|
37
|
+
"""Return the zip entry holding the payload.
|
|
38
|
+
|
|
39
|
+
Uses manifest.payload.url when present; falls back to `0.payload` only
|
|
40
|
+
when the url is empty or missing.
|
|
41
|
+
"""
|
|
42
|
+
name_set = set(names)
|
|
43
|
+
if payload_url:
|
|
44
|
+
if not _is_safe_entry_name(payload_url):
|
|
45
|
+
raise ValueError(f"unsafe payload url in manifest: {payload_url!r}")
|
|
46
|
+
if payload_url in name_set:
|
|
47
|
+
return payload_url
|
|
48
|
+
raise ValueError(f"tdf doesn't contain payload entry {payload_url!r}")
|
|
49
|
+
if TDF_PAYLOAD_FILE_NAME in name_set:
|
|
50
|
+
return TDF_PAYLOAD_FILE_NAME
|
|
51
|
+
raise ValueError("tdf doesn't contain a payload")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def payload_url_from_manifest_json(manifest_text: str) -> str | None:
|
|
55
|
+
"""Extract payload.url without requiring a fully valid manifest."""
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(manifest_text)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
return None
|
|
60
|
+
if not isinstance(data, dict):
|
|
61
|
+
return None
|
|
62
|
+
payload = data.get("payload")
|
|
63
|
+
if not isinstance(payload, dict):
|
|
64
|
+
return None
|
|
65
|
+
url = payload.get("url")
|
|
66
|
+
return url if isinstance(url, str) else None
|
|
67
|
+
|
|
68
|
+
|
|
13
69
|
class TDFReader:
|
|
14
70
|
"""TDFReader is responsible for reading and processing Trusted Data Format (TDF) files.
|
|
15
71
|
|
|
@@ -31,15 +87,16 @@ class TDFReader:
|
|
|
31
87
|
try:
|
|
32
88
|
self._zip_reader = ZipReader(tdf)
|
|
33
89
|
namelist = self._zip_reader.namelist()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
self._manifest_name = resolve_manifest_name(namelist)
|
|
91
|
+
manifest_text = self._zip_reader.read(self._manifest_name).decode("utf-8")
|
|
92
|
+
payload_url = payload_url_from_manifest_json(manifest_text)
|
|
93
|
+
self._payload_name = resolve_payload_name(payload_url, namelist)
|
|
94
|
+
except UnicodeDecodeError as e:
|
|
95
|
+
# UnicodeDecodeError is a ValueError subclass, but it means the
|
|
96
|
+
# manifest entry is corrupt, not that the tdf is missing an
|
|
97
|
+
# entry (the resolvers' own ValueErrors). Wrap it like any
|
|
98
|
+
# other unexpected failure instead of letting it pass through.
|
|
99
|
+
raise SDKException("Error initializing TDFReader") from e
|
|
43
100
|
except Exception as e:
|
|
44
101
|
if isinstance(e, ValueError):
|
|
45
102
|
raise
|
otdf_python/tdf_writer.py
CHANGED
|
@@ -8,8 +8,10 @@ from otdf_python.zip_writer import ZipWriter
|
|
|
8
8
|
class TDFWriter:
|
|
9
9
|
"""TDF file writer for creating encrypted TDF packages."""
|
|
10
10
|
|
|
11
|
+
# Spec: manifest entry MUST be `manifest.json` at the archive root.
|
|
12
|
+
TDF_MANIFEST_FILE_NAME = "manifest.json"
|
|
13
|
+
# Payload entry name; TDF.create_tdf writes this same value into manifest.payload.url.
|
|
11
14
|
TDF_PAYLOAD_FILE_NAME = "0.payload"
|
|
12
|
-
TDF_MANIFEST_FILE_NAME = "0.manifest.json"
|
|
13
15
|
|
|
14
16
|
def __init__(self, out_stream: io.BytesIO | None = None):
|
|
15
17
|
"""Initialize TDF writer."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: otdf-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Unofficial OpenTDF SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/b-long/opentdf-python-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/b-long/opentdf-python-sdk
|
|
@@ -25,9 +25,9 @@ Requires-Dist: connect-python>=0.9.0
|
|
|
25
25
|
Requires-Dist: cryptography>=50.0.0
|
|
26
26
|
Requires-Dist: grpcio-status>=1.74.0
|
|
27
27
|
Requires-Dist: grpcio>=1.74.0
|
|
28
|
-
Requires-Dist: httpx2>=2.
|
|
28
|
+
Requires-Dist: httpx2>=2.12.0
|
|
29
29
|
Requires-Dist: protobuf>=6.31.1
|
|
30
|
-
Requires-Dist: pyjwt>=2.
|
|
30
|
+
Requires-Dist: pyjwt>=2.15.0
|
|
31
31
|
Requires-Dist: typing-extensions>=4.14.1
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
@@ -130,6 +130,12 @@ with open("decrypted.txt", "wb") as f:
|
|
|
130
130
|
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
## TDF container format
|
|
134
|
+
|
|
135
|
+
TDF archives are written with the spec-mandated `manifest.json` zip entry (per opentdf/spec) instead of the legacy `0.manifest.json`. Readers accept both names, so archives written by older versions of this SDK remain readable forever. The payload zip entry is resolved from `manifest.payload.url` rather than assumed. `schemaVersion` is unchanged (still `4.3.0`); `tdf_spec_version` is now read at either its legacy or spec-conformant placement, but never written.
|
|
136
|
+
|
|
137
|
+
**Interoperability note:** archives written by this version cannot currently be opened by released `otdfctl`, or by the upstream `opentdf/platform` Go, Java, or JS SDKs, until they add a `manifest.json` read fallback (an upstream reader-fallback PR is planned). If you interoperate with those tools today, hold off upgrading until that fallback ships.
|
|
138
|
+
|
|
133
139
|
## Project Structure
|
|
134
140
|
|
|
135
141
|
```
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
otdf_python/__init__.py,sha256=q93tKt2r5-_244iKPeJf4ja1ftlmOmdRQnXWAZsxISI,542
|
|
2
2
|
otdf_python/__main__.py,sha256=VuImcvrzCHTYOwQ-yS9HQwKmF2O6lORf1psmOkdZIh0,275
|
|
3
3
|
otdf_python/address_normalizer.py,sha256=7gigfxFIFnDILi8BGQipuLl-Cr71pwi5vE72hbpLdmI,2850
|
|
4
|
-
otdf_python/aesgcm.py,sha256=
|
|
4
|
+
otdf_python/aesgcm.py,sha256=XUZpcmyegvPLa8oRgQJ2A7-c8lIUdW2tPE41Fa3neLw,2238
|
|
5
5
|
otdf_python/assertion_config.py,sha256=rw0SIB3xG-nAeb5r_liuxLphU4tcj-zlq8rVvXncX-Y,2317
|
|
6
6
|
otdf_python/asym_crypto.py,sha256=EYkMNhZJP5khH0IvICTOG2bMg_TMvd6wXDu5zW0jpj4,7234
|
|
7
7
|
otdf_python/auth_headers.py,sha256=onS-fXsIZN4l33yQS9VspPvoiAnfM2B0nNAVL_YIGZ0,987
|
|
8
8
|
otdf_python/autoconfigure_utils.py,sha256=W5aJ0tC7HWfGb_1Mva_oxgduUzSpPoyDeaq0rPwgPAs,3689
|
|
9
|
-
otdf_python/cli.py,sha256=
|
|
10
|
-
otdf_python/collection_store.py,sha256=
|
|
9
|
+
otdf_python/cli.py,sha256=4IHVQdNrAjMr3dWHVMDNEj_VgHCL5s2o6d-uH0yR54I,24468
|
|
10
|
+
otdf_python/collection_store.py,sha256=y-iMdeGDdKpte6zWUhrJOXPBku7wU_LfRzs4NmWdI_s,1430
|
|
11
11
|
otdf_python/collection_store_impl.py,sha256=3RqO3rvDCosajKpuls5DiO2_SWYsNQul9_9L7n-lQ68,758
|
|
12
12
|
otdf_python/config.py,sha256=l1Ykg1gFUrFZTnd6bwMI6oi_clR5uCZ_Y1qH7QKtW90,2523
|
|
13
13
|
otdf_python/connect_client.py,sha256=TpHpcU2t19pSqWn30cqzfM48nDG590BfNTlDPgUu054,45
|
|
@@ -21,28 +21,28 @@ otdf_python/eckeypair.py,sha256=qcPKv0OS1lYxRICj9dhAW_eMz32anFBtpI8EJfXxpX0,2470
|
|
|
21
21
|
otdf_python/header.py,sha256=peG14kE_KAUCW4fY82sqcWF5zTAVAnJfFXCHtC8Z0iQ,7189
|
|
22
22
|
otdf_python/invalid_zip_exception.py,sha256=M_bAiXEjJdxPfA178YH-uHGRwMrNBKzjQzlQ54aDP2w,292
|
|
23
23
|
otdf_python/kas_allowlist.py,sha256=0yX3J9J1od_ew0sp3pX6kfav39pTz7aBBpMnVkE5T7s,5884
|
|
24
|
-
otdf_python/kas_client.py,sha256=
|
|
24
|
+
otdf_python/kas_client.py,sha256=Xclv4s9KI65dyHSk8Rt8oeYadUbZ61CokVM9QZtfnMk,27717
|
|
25
25
|
otdf_python/kas_connect_rpc_client.py,sha256=Y2w0SPh4QZxLLhneKky8N20jd9UQmShmAnLo_c9kOXk,9113
|
|
26
26
|
otdf_python/kas_info.py,sha256=R_zfW8m2n2PT1-Enk6NG5K8YIjWvb1g-PQ6M0bZX-o8,739
|
|
27
27
|
otdf_python/kas_key_cache.py,sha256=6hfzRAg9o_IfRErWSe-_gGTG9kRyYENMizMY1Shkmfk,1548
|
|
28
28
|
otdf_python/key_type.py,sha256=2gQlXOj35J3ISCcWjU3sGYUxmlZR47BMq6Xr2yoKA8k,928
|
|
29
29
|
otdf_python/key_type_constants.py,sha256=0aqG1N1Ay0Z8o5fsq7fAiJXQeD6k-fxp06If__BGcR4,955
|
|
30
|
-
otdf_python/manifest.py,sha256=
|
|
31
|
-
otdf_python/nanotdf.py,sha256=
|
|
30
|
+
otdf_python/manifest.py,sha256=Wtgi5Jn9Fxuya8qlwydFUNbvoqE4QYeGRh-Q4d9gGNY,7863
|
|
31
|
+
otdf_python/nanotdf.py,sha256=V_D0No6z3P-ZNCKfVLLkSVV_wae606UTrh1v4DKPqpQ,33910
|
|
32
32
|
otdf_python/nanotdf_ecdsa_struct.py,sha256=C-qQnroJGO9yXrBb-p-rYMSxf2gS8kF6SCUvO-njAXU,4067
|
|
33
33
|
otdf_python/nanotdf_type.py,sha256=3MQzT6lJ3WJKMICFyyYZXX2_cFYcZ5G4m1uif-l9Nxo,1112
|
|
34
|
-
otdf_python/policy_binding_serializer.py,sha256=
|
|
35
|
-
otdf_python/policy_info.py,sha256=
|
|
34
|
+
otdf_python/policy_binding_serializer.py,sha256=hlzJWu34m8S9oy3McRxW7f03f3t9R3bYEn-DRQCr5dk,1278
|
|
35
|
+
otdf_python/policy_info.py,sha256=6kQc5eNMXzAAWkmnE2Kl9gpQ3WmuNbhszLu2Xrdz5Ow,2077
|
|
36
36
|
otdf_python/policy_object.py,sha256=LikIsahPkKr-iYA0lhgQitCbh8CsmxUBYyBs6VYfmxY,512
|
|
37
37
|
otdf_python/policy_stub.py,sha256=RfU_fICqsAOnTXOHpKhtKC0RJ3KoWhDxO0XecZWM548,159
|
|
38
38
|
otdf_python/resource_locator.py,sha256=bjK935XcfNq-PyqidHNq8eIiPeZEStYdQvmQ9B9GY20,6290
|
|
39
|
-
otdf_python/sdk.py,sha256=
|
|
40
|
-
otdf_python/sdk_builder.py,sha256=
|
|
39
|
+
otdf_python/sdk.py,sha256=pFXqmnKluBEYuLIwbFQlEYTZIK45y31XRc3YIz8Djdk,15411
|
|
40
|
+
otdf_python/sdk_builder.py,sha256=f0bxDQrTrAMkNFDkCGVVgOBkDZuXcg9LATB7z_uD-5g,19243
|
|
41
41
|
otdf_python/sdk_exceptions.py,sha256=2GElGyM5LKN8Uh_lAiT6Ho4oNRWYRQsMNOK5s2jiv28,687
|
|
42
42
|
otdf_python/symmetric_and_payload_config.py,sha256=iPHeZSeY9BjsQ-wkvdm6-FIR7773EgGiaIvSG-ICOHw,1158
|
|
43
|
-
otdf_python/tdf.py,sha256=
|
|
44
|
-
otdf_python/tdf_reader.py,sha256=
|
|
45
|
-
otdf_python/tdf_writer.py,sha256=
|
|
43
|
+
otdf_python/tdf.py,sha256=BPGGfPD6oWSxEN-f1bWPQl5Ee3SjBQlxqdXNXTxOREI,21842
|
|
44
|
+
otdf_python/tdf_reader.py,sha256=_bW1u0_HhUN4Xr9YidMUAwYmJutBH-Ew_ItQ5pqNJuY,7661
|
|
45
|
+
otdf_python/tdf_writer.py,sha256=LYA1dCCaxhBRtrzL7TXa2gaFLH4dup8zpnDiEoLfcBM,959
|
|
46
46
|
otdf_python/token_source.py,sha256=Ow4bZqPqVxbZx64M7pLxQarVlPfiz15ZvZr9H17_ZMY,1021
|
|
47
47
|
otdf_python/version.py,sha256=uDKJKsSQoaEH-JlqAwiXDxceLRX9hkG4I3NVLEfDCHA,2025
|
|
48
48
|
otdf_python/zip_reader.py,sha256=qrHv-ecs09tz99ZKdOMiWaciYf2XsQOUTiXy1JHjuEY,1705
|
|
@@ -170,6 +170,6 @@ otdf_python_proto/wellknownconfiguration/__init__.py,sha256=xK8XUrwCL9elWuMTx6vV
|
|
|
170
170
|
otdf_python_proto/wellknownconfiguration/wellknown_configuration_connect.py,sha256=HShWKFfd_7p1aZzqZKJiT1lDma0IINTB4kfv_DcrBA0,6585
|
|
171
171
|
otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py,sha256=KXjTE12hr8GV7apJLTzv2VDBv8jw-74jZUjQnIzzP8g,3633
|
|
172
172
|
otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi,sha256=H-Pjnl-fvckhUilvIHIuuSbHTDovejk-e7HFQ8f8eOc,1431
|
|
173
|
-
otdf_python-0.
|
|
174
|
-
otdf_python-0.
|
|
175
|
-
otdf_python-0.
|
|
173
|
+
otdf_python-0.12.0.dist-info/METADATA,sha256=7zQTqmj3hzn5DoZVreAKYbdz8DcsgUunMeaZrU25QiI,5884
|
|
174
|
+
otdf_python-0.12.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
175
|
+
otdf_python-0.12.0.dist-info/RECORD,,
|