otdf-python 0.1.10__py3-none-any.whl → 0.3.5__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 (144) hide show
  1. otdf_python/__init__.py +25 -0
  2. otdf_python/__main__.py +12 -0
  3. otdf_python/address_normalizer.py +84 -0
  4. otdf_python/aesgcm.py +55 -0
  5. otdf_python/assertion_config.py +84 -0
  6. otdf_python/asym_crypto.py +198 -0
  7. otdf_python/auth_headers.py +33 -0
  8. otdf_python/autoconfigure_utils.py +113 -0
  9. otdf_python/cli.py +569 -0
  10. otdf_python/collection_store.py +41 -0
  11. otdf_python/collection_store_impl.py +22 -0
  12. otdf_python/config.py +69 -0
  13. otdf_python/connect_client.py +0 -0
  14. otdf_python/constants.py +1 -0
  15. otdf_python/crypto_utils.py +78 -0
  16. otdf_python/dpop.py +81 -0
  17. otdf_python/ecc_constants.py +176 -0
  18. otdf_python/ecc_mode.py +83 -0
  19. otdf_python/ecdh.py +317 -0
  20. otdf_python/eckeypair.py +75 -0
  21. otdf_python/header.py +181 -0
  22. otdf_python/invalid_zip_exception.py +8 -0
  23. otdf_python/kas_client.py +709 -0
  24. otdf_python/kas_connect_rpc_client.py +213 -0
  25. otdf_python/kas_info.py +25 -0
  26. otdf_python/kas_key_cache.py +52 -0
  27. otdf_python/key_type.py +31 -0
  28. otdf_python/key_type_constants.py +43 -0
  29. otdf_python/manifest.py +215 -0
  30. otdf_python/nanotdf.py +863 -0
  31. otdf_python/nanotdf_ecdsa_struct.py +132 -0
  32. otdf_python/nanotdf_type.py +43 -0
  33. otdf_python/policy_binding_serializer.py +39 -0
  34. otdf_python/policy_info.py +55 -0
  35. otdf_python/policy_object.py +22 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +172 -0
  38. otdf_python/sdk.py +436 -0
  39. otdf_python/sdk_builder.py +416 -0
  40. otdf_python/sdk_exceptions.py +16 -0
  41. otdf_python/symmetric_and_payload_config.py +30 -0
  42. otdf_python/tdf.py +480 -0
  43. otdf_python/tdf_reader.py +153 -0
  44. otdf_python/tdf_writer.py +23 -0
  45. otdf_python/token_source.py +34 -0
  46. otdf_python/version.py +57 -0
  47. otdf_python/zip_reader.py +47 -0
  48. otdf_python/zip_writer.py +70 -0
  49. otdf_python-0.3.5.dist-info/METADATA +153 -0
  50. otdf_python-0.3.5.dist-info/RECORD +137 -0
  51. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info}/WHEEL +1 -2
  52. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info/licenses}/LICENSE +1 -1
  53. otdf_python_proto/__init__.py +37 -0
  54. otdf_python_proto/authorization/__init__.py +1 -0
  55. otdf_python_proto/authorization/authorization_pb2.py +80 -0
  56. otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
  57. otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
  58. otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
  59. otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
  60. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
  61. otdf_python_proto/common/__init__.py +1 -0
  62. otdf_python_proto/common/common_pb2.py +52 -0
  63. otdf_python_proto/common/common_pb2.pyi +61 -0
  64. otdf_python_proto/entity/__init__.py +1 -0
  65. otdf_python_proto/entity/entity_pb2.py +47 -0
  66. otdf_python_proto/entity/entity_pb2.pyi +50 -0
  67. otdf_python_proto/entityresolution/__init__.py +1 -0
  68. otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
  69. otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
  70. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
  71. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
  72. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
  73. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
  74. otdf_python_proto/kas/__init__.py +9 -0
  75. otdf_python_proto/kas/kas_pb2.py +103 -0
  76. otdf_python_proto/kas/kas_pb2.pyi +170 -0
  77. otdf_python_proto/kas/kas_pb2_connect.py +192 -0
  78. otdf_python_proto/legacy_grpc/__init__.py +1 -0
  79. otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
  80. otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
  81. otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
  82. otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
  83. otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
  84. otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
  85. otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
  86. otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
  87. otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
  88. otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
  89. otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
  90. otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
  91. otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
  92. otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
  93. otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
  94. otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
  95. otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
  96. otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
  97. otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
  98. otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
  99. otdf_python_proto/logger/__init__.py +1 -0
  100. otdf_python_proto/logger/audit/test_pb2.py +43 -0
  101. otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
  102. otdf_python_proto/policy/__init__.py +1 -0
  103. otdf_python_proto/policy/actions/actions_pb2.py +75 -0
  104. otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
  105. otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
  106. otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
  107. otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
  108. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
  109. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
  110. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
  111. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
  112. otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
  113. otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
  114. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
  115. otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
  116. otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
  117. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
  118. otdf_python_proto/policy/objects_pb2.py +150 -0
  119. otdf_python_proto/policy/objects_pb2.pyi +464 -0
  120. otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
  121. otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
  122. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
  123. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
  124. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
  125. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
  126. otdf_python_proto/policy/selectors_pb2.py +57 -0
  127. otdf_python_proto/policy/selectors_pb2.pyi +90 -0
  128. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
  129. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
  130. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
  131. otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
  132. otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
  133. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
  134. otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
  135. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
  136. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
  137. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
  138. otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
  139. otdf_python/build.py +0 -190
  140. otdf_python/go.py +0 -1478
  141. otdf_python/gotdf_python.py +0 -383
  142. otdf_python-0.1.10.dist-info/METADATA +0 -149
  143. otdf_python-0.1.10.dist-info/RECORD +0 -10
  144. otdf_python-0.1.10.dist-info/top_level.txt +0 -1
otdf_python/ecdh.py ADDED
@@ -0,0 +1,317 @@
1
+ """
2
+ ECDH (Elliptic Curve Diffie-Hellman) key exchange for NanoTDF.
3
+
4
+ This module implements the ECDH key exchange protocol with HKDF key derivation
5
+ as specified in the NanoTDF spec. It supports the following curves:
6
+ - secp256r1 (NIST P-256)
7
+ - secp384r1 (NIST P-384)
8
+ - secp521r1 (NIST P-521)
9
+ - secp256k1 (Bitcoin curve)
10
+
11
+ The protocol follows ECIES methodology similar to S/MIME and GPG:
12
+ 1. Generate ephemeral keypair
13
+ 2. Perform ECDH with recipient's public key to get shared secret
14
+ 3. Use HKDF to derive symmetric encryption key from shared secret
15
+ """
16
+
17
+ from cryptography.hazmat.backends import default_backend
18
+ from cryptography.hazmat.primitives import hashes, serialization
19
+ from cryptography.hazmat.primitives.asymmetric import ec
20
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
21
+ from cryptography.hazmat.primitives.serialization import (
22
+ Encoding,
23
+ PublicFormat,
24
+ )
25
+
26
+ from otdf_python.ecc_constants import ECCConstants
27
+
28
+ # HKDF salt for NanoTDF key derivation
29
+ # Per spec: "salt value derived from magic number/version"
30
+ # This is the SHA-256 hash of the NanoTDF magic number and version
31
+ NANOTDF_HKDF_SALT = bytes.fromhex(
32
+ "3de3ca1e50cf62d8b6aba603a96fca6761387a7ac86c3d3afe85ae2d1812edfc"
33
+ )
34
+
35
+
36
+ class ECDHError(Exception):
37
+ """Base exception for ECDH operations."""
38
+
39
+ pass
40
+
41
+
42
+ class UnsupportedCurveError(ECDHError):
43
+ """Raised when an unsupported curve is specified."""
44
+
45
+ pass
46
+
47
+
48
+ class InvalidKeyError(ECDHError):
49
+ """Raised when a key is invalid or malformed."""
50
+
51
+ pass
52
+
53
+
54
+ def get_curve(curve_name: str) -> ec.EllipticCurve:
55
+ """
56
+ Get the cryptography curve object for a given curve name.
57
+
58
+ Args:
59
+ curve_name: Name of the curve (e.g., "secp256r1")
60
+
61
+ Returns:
62
+ ec.EllipticCurve: The curve object
63
+
64
+ Raises:
65
+ UnsupportedCurveError: If the curve is not supported
66
+ """
67
+ try:
68
+ # Delegate to ECCConstants for the authoritative mapping
69
+ return ECCConstants.get_curve_object(curve_name)
70
+ except ValueError as e:
71
+ raise UnsupportedCurveError(str(e)) from e
72
+
73
+
74
+ def get_compressed_key_size(curve_name: str) -> int:
75
+ """
76
+ Get the size of a compressed public key for a given curve.
77
+
78
+ Args:
79
+ curve_name: Name of the curve (e.g., "secp256r1")
80
+
81
+ Returns:
82
+ int: Size in bytes of the compressed public key
83
+
84
+ Raises:
85
+ UnsupportedCurveError: If the curve is not supported
86
+ """
87
+ try:
88
+ # Delegate to ECCConstants for the authoritative mapping
89
+ return ECCConstants.get_compressed_key_size_by_name(curve_name)
90
+ except ValueError as e:
91
+ raise UnsupportedCurveError(str(e)) from e
92
+
93
+
94
+ def generate_ephemeral_keypair(
95
+ curve_name: str,
96
+ ) -> tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]:
97
+ """
98
+ Generate an ephemeral keypair for ECDH.
99
+
100
+ Args:
101
+ curve_name: Name of the curve (e.g., "secp256r1")
102
+
103
+ Returns:
104
+ tuple: (private_key, public_key)
105
+
106
+ Raises:
107
+ UnsupportedCurveError: If the curve is not supported
108
+ """
109
+ curve = get_curve(curve_name)
110
+ private_key = ec.generate_private_key(curve, default_backend())
111
+ public_key = private_key.public_key()
112
+ return private_key, public_key
113
+
114
+
115
+ def compress_public_key(public_key: ec.EllipticCurvePublicKey) -> bytes:
116
+ """
117
+ Compress an EC public key to compressed point format.
118
+
119
+ Args:
120
+ public_key: The EC public key to compress
121
+
122
+ Returns:
123
+ bytes: Compressed public key (33-67 bytes depending on curve)
124
+ """
125
+ return public_key.public_bytes(
126
+ encoding=Encoding.X962, format=PublicFormat.CompressedPoint
127
+ )
128
+
129
+
130
+ def decompress_public_key(
131
+ compressed_key: bytes, curve_name: str
132
+ ) -> ec.EllipticCurvePublicKey:
133
+ """
134
+ Decompress a public key from compressed point format.
135
+
136
+ Args:
137
+ compressed_key: The compressed public key bytes
138
+ curve_name: Name of the curve (e.g., "secp256r1")
139
+
140
+ Returns:
141
+ ec.EllipticCurvePublicKey: The decompressed public key
142
+
143
+ Raises:
144
+ InvalidKeyError: If the key cannot be decompressed
145
+ UnsupportedCurveError: If the curve is not supported
146
+ """
147
+ try:
148
+ curve = get_curve(curve_name)
149
+ # Verify the size matches expected compressed size
150
+ expected_size = get_compressed_key_size(curve_name)
151
+ if len(compressed_key) != expected_size:
152
+ raise InvalidKeyError(
153
+ f"Invalid compressed key size for {curve_name}: "
154
+ f"expected {expected_size} bytes, got {len(compressed_key)} bytes"
155
+ )
156
+
157
+ return ec.EllipticCurvePublicKey.from_encoded_point(curve, compressed_key)
158
+ except (ValueError, TypeError) as e:
159
+ raise InvalidKeyError(f"Failed to decompress public key: {e}")
160
+
161
+
162
+ def derive_shared_secret(
163
+ private_key: ec.EllipticCurvePrivateKey, public_key: ec.EllipticCurvePublicKey
164
+ ) -> bytes:
165
+ """
166
+ Derive a shared secret using ECDH.
167
+
168
+ Args:
169
+ private_key: The private key (can be ephemeral or recipient's key)
170
+ public_key: The public key (recipient's or ephemeral key)
171
+
172
+ Returns:
173
+ bytes: The raw shared secret (x-coordinate of the ECDH point)
174
+
175
+ Raises:
176
+ ECDHError: If ECDH fails
177
+ """
178
+ try:
179
+ shared_secret = private_key.exchange(ec.ECDH(), public_key)
180
+ return shared_secret
181
+ except Exception as e:
182
+ raise ECDHError(f"Failed to derive shared secret: {e}")
183
+
184
+
185
+ def derive_key_from_shared_secret(
186
+ shared_secret: bytes,
187
+ key_length: int = 32,
188
+ salt: bytes | None = None,
189
+ info: bytes = b"",
190
+ ) -> bytes:
191
+ """
192
+ Derive a symmetric encryption key from the ECDH shared secret using HKDF.
193
+
194
+ Args:
195
+ shared_secret: The raw ECDH shared secret
196
+ key_length: Length of the derived key in bytes (default: 32 for AES-256)
197
+ salt: Optional salt for HKDF (default: NANOTDF_HKDF_SALT)
198
+ info: Optional context/application-specific info (default: empty)
199
+
200
+ Returns:
201
+ bytes: Derived symmetric encryption key
202
+
203
+ Raises:
204
+ ECDHError: If key derivation fails
205
+ """
206
+ if salt is None:
207
+ salt = NANOTDF_HKDF_SALT
208
+
209
+ try:
210
+ hkdf = HKDF(
211
+ algorithm=hashes.SHA256(),
212
+ length=key_length,
213
+ salt=salt,
214
+ info=info,
215
+ backend=default_backend(),
216
+ )
217
+ return hkdf.derive(shared_secret)
218
+ except Exception as e:
219
+ raise ECDHError(f"Failed to derive key from shared secret: {e}")
220
+
221
+
222
+ def encrypt_key_with_ecdh(
223
+ recipient_public_key_pem: str, curve_name: str = "secp256r1"
224
+ ) -> tuple[bytes, bytes]:
225
+ """
226
+ High-level function: Generate ephemeral keypair and derive encryption key.
227
+
228
+ This is used during NanoTDF encryption to derive the key that will be used
229
+ to encrypt the payload. The ephemeral public key must be stored in the
230
+ NanoTDF header so the recipient can derive the same key.
231
+
232
+ Args:
233
+ recipient_public_key_pem: Recipient's public key in PEM format (e.g., KAS public key)
234
+ curve_name: Name of the curve to use (default: "secp256r1")
235
+
236
+ Returns:
237
+ tuple: (derived_key, compressed_ephemeral_public_key)
238
+ - derived_key: 32-byte AES-256 key for encrypting the payload
239
+ - compressed_ephemeral_public_key: Ephemeral public key to store in header
240
+
241
+ Raises:
242
+ ECDHError: If key derivation fails
243
+ InvalidKeyError: If recipient's public key is invalid
244
+ UnsupportedCurveError: If the curve is not supported
245
+ """
246
+ # Load recipient's public key
247
+ try:
248
+ recipient_public_key = serialization.load_pem_public_key(
249
+ recipient_public_key_pem.encode(), backend=default_backend()
250
+ )
251
+ if not isinstance(recipient_public_key, ec.EllipticCurvePublicKey):
252
+ raise InvalidKeyError("Recipient's public key is not an EC key")
253
+ except Exception as e:
254
+ raise InvalidKeyError(f"Failed to load recipient's public key: {e}")
255
+
256
+ # Generate ephemeral keypair
257
+ ephemeral_private_key, ephemeral_public_key = generate_ephemeral_keypair(curve_name)
258
+
259
+ # Derive shared secret
260
+ shared_secret = derive_shared_secret(ephemeral_private_key, recipient_public_key)
261
+
262
+ # Derive encryption key from shared secret
263
+ derived_key = derive_key_from_shared_secret(shared_secret, key_length=32)
264
+
265
+ # Compress ephemeral public key for storage in header
266
+ compressed_ephemeral_key = compress_public_key(ephemeral_public_key)
267
+
268
+ return derived_key, compressed_ephemeral_key
269
+
270
+
271
+ def decrypt_key_with_ecdh(
272
+ recipient_private_key_pem: str,
273
+ compressed_ephemeral_public_key: bytes,
274
+ curve_name: str = "secp256r1",
275
+ ) -> bytes:
276
+ """
277
+ High-level function: Derive decryption key from ephemeral public key and recipient's private key.
278
+
279
+ This is used during NanoTDF decryption to derive the same key that was used
280
+ to encrypt the payload. The ephemeral public key is extracted from the
281
+ NanoTDF header.
282
+
283
+ Args:
284
+ recipient_private_key_pem: Recipient's private key in PEM format (e.g., KAS private key)
285
+ compressed_ephemeral_public_key: Ephemeral public key from NanoTDF header
286
+ curve_name: Name of the curve (default: "secp256r1")
287
+
288
+ Returns:
289
+ bytes: 32-byte AES-256 key for decrypting the payload
290
+
291
+ Raises:
292
+ ECDHError: If key derivation fails
293
+ InvalidKeyError: If keys are invalid
294
+ UnsupportedCurveError: If the curve is not supported
295
+ """
296
+ # Load recipient's private key
297
+ try:
298
+ recipient_private_key = serialization.load_pem_private_key(
299
+ recipient_private_key_pem.encode(), password=None, backend=default_backend()
300
+ )
301
+ if not isinstance(recipient_private_key, ec.EllipticCurvePrivateKey):
302
+ raise InvalidKeyError("Recipient's private key is not an EC key")
303
+ except Exception as e:
304
+ raise InvalidKeyError(f"Failed to load recipient's private key: {e}")
305
+
306
+ # Decompress ephemeral public key
307
+ ephemeral_public_key = decompress_public_key(
308
+ compressed_ephemeral_public_key, curve_name
309
+ )
310
+
311
+ # Derive shared secret
312
+ shared_secret = derive_shared_secret(recipient_private_key, ephemeral_public_key)
313
+
314
+ # Derive decryption key from shared secret
315
+ derived_key = derive_key_from_shared_secret(shared_secret, key_length=32)
316
+
317
+ return derived_key
@@ -0,0 +1,75 @@
1
+ from cryptography.exceptions import InvalidSignature
2
+ from cryptography.hazmat.backends import default_backend
3
+ from cryptography.hazmat.primitives import hashes, serialization
4
+ from cryptography.hazmat.primitives.asymmetric import ec
5
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
6
+ from cryptography.hazmat.primitives.serialization import (
7
+ Encoding,
8
+ NoEncryption,
9
+ PrivateFormat,
10
+ PublicFormat,
11
+ )
12
+
13
+
14
+ class ECKeyPair:
15
+ def __init__(self, curve=None):
16
+ if curve is None:
17
+ curve = ec.SECP256R1()
18
+ self.private_key = ec.generate_private_key(curve, default_backend())
19
+ self.public_key = self.private_key.public_key()
20
+ self.curve = curve
21
+
22
+ def public_key_pem(self):
23
+ return self.public_key.public_bytes(
24
+ Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
25
+ ).decode()
26
+
27
+ def private_key_pem(self):
28
+ return self.private_key.private_bytes(
29
+ Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
30
+ ).decode()
31
+
32
+ def key_size(self):
33
+ return self.private_key.key_size
34
+
35
+ def compress_public_key(self):
36
+ return self.public_key.public_bytes(Encoding.X962, PublicFormat.CompressedPoint)
37
+
38
+ @staticmethod
39
+ def public_key_from_pem(pem):
40
+ return serialization.load_pem_public_key(
41
+ pem.encode(), backend=default_backend()
42
+ )
43
+
44
+ @staticmethod
45
+ def private_key_from_pem(pem):
46
+ return serialization.load_pem_private_key(
47
+ pem.encode(), password=None, backend=default_backend()
48
+ )
49
+
50
+ @staticmethod
51
+ def compute_ecdh_key(public_key, private_key):
52
+ return private_key.exchange(ec.ECDH(), public_key)
53
+
54
+ @staticmethod
55
+ def calculate_hkdf(salt, secret, length=32):
56
+ hkdf = HKDF(
57
+ algorithm=hashes.SHA256(),
58
+ length=length,
59
+ salt=salt,
60
+ info=None,
61
+ backend=default_backend(),
62
+ )
63
+ return hkdf.derive(secret)
64
+
65
+ @staticmethod
66
+ def sign_ecdsa(data, private_key):
67
+ return private_key.sign(data, ec.ECDSA(hashes.SHA256()))
68
+
69
+ @staticmethod
70
+ def verify_ecdsa(data, signature, public_key):
71
+ try:
72
+ public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))
73
+ return True
74
+ except InvalidSignature:
75
+ return False
otdf_python/header.py ADDED
@@ -0,0 +1,181 @@
1
+ from otdf_python.constants import MAGIC_NUMBER_AND_VERSION
2
+ from otdf_python.ecc_mode import ECCMode
3
+ from otdf_python.policy_info import PolicyInfo
4
+ from otdf_python.resource_locator import ResourceLocator
5
+ from otdf_python.symmetric_and_payload_config import SymmetricAndPayloadConfig
6
+
7
+
8
+ class Header:
9
+ # Size of GMAC (Galois Message Authentication Code) for policy binding
10
+ GMAC_SIZE = 8
11
+
12
+ def __init__(self):
13
+ self.kas_locator: ResourceLocator | None = None
14
+ self.ecc_mode: ECCMode | None = None
15
+ self.payload_config: SymmetricAndPayloadConfig | None = None
16
+ self.policy_info: PolicyInfo | None = None
17
+ self.policy_binding: bytes | None = None
18
+ self.ephemeral_key: bytes | None = None
19
+
20
+ @classmethod
21
+ def from_bytes(cls, buffer: bytes):
22
+ # Parse header from bytes, validate magic/version
23
+ offset = 0
24
+ magic = buffer[offset : offset + 3]
25
+ if magic != MAGIC_NUMBER_AND_VERSION:
26
+ raise ValueError("Invalid magic number and version in nano tdf.")
27
+ offset += 3
28
+ kas_locator, kas_size = ResourceLocator.from_bytes_with_size(buffer[offset:])
29
+ offset += kas_size
30
+ ecc_mode = ECCMode(buffer[offset])
31
+ offset += 1
32
+ payload_config = SymmetricAndPayloadConfig(buffer[offset])
33
+ offset += 1
34
+ policy_info, policy_size = PolicyInfo.from_bytes_with_size(
35
+ buffer[offset:], ecc_mode
36
+ )
37
+ offset += policy_size
38
+
39
+ # Read policy binding (GMAC - 8 bytes fixed size)
40
+ # Note: ECDSA binding not yet supported in this implementation
41
+ policy_binding = buffer[offset : offset + cls.GMAC_SIZE]
42
+ if len(policy_binding) != cls.GMAC_SIZE:
43
+ raise ValueError("Failed to read policy binding - invalid buffer size.")
44
+ offset += cls.GMAC_SIZE
45
+
46
+ compressed_pubkey_size = ECCMode.get_ec_compressed_pubkey_size(
47
+ ecc_mode.get_elliptic_curve_type()
48
+ )
49
+ ephemeral_key = buffer[offset : offset + compressed_pubkey_size]
50
+ if len(ephemeral_key) != compressed_pubkey_size:
51
+ raise ValueError("Failed to read ephemeral key - invalid buffer size.")
52
+ obj = cls()
53
+ obj.kas_locator = kas_locator
54
+ obj.ecc_mode = ecc_mode
55
+ obj.payload_config = payload_config
56
+ obj.policy_info = policy_info
57
+ obj.policy_binding = policy_binding
58
+ obj.ephemeral_key = ephemeral_key
59
+ return obj
60
+
61
+ @staticmethod
62
+ def peek_length(buffer: bytes) -> int:
63
+ offset = 0
64
+ # MAGIC_NUMBER_AND_VERSION (3 bytes)
65
+ offset += 3
66
+ # ResourceLocator
67
+ _kas_locator, kas_size = ResourceLocator.from_bytes_with_size(buffer[offset:])
68
+ offset += kas_size
69
+ # ECC mode (1 byte)
70
+ ecc_mode = ECCMode(buffer[offset])
71
+ offset += 1
72
+ # Payload config (1 byte)
73
+ offset += 1
74
+ # PolicyInfo
75
+ _policy_info, policy_size = PolicyInfo.from_bytes_with_size(
76
+ buffer[offset:], ecc_mode
77
+ )
78
+ offset += policy_size
79
+ # Policy binding (GMAC - 8 bytes)
80
+ offset += Header.GMAC_SIZE
81
+ # Ephemeral key (size depends on curve)
82
+ compressed_pubkey_size = ECCMode.get_ec_compressed_pubkey_size(
83
+ ecc_mode.get_elliptic_curve_type()
84
+ )
85
+ offset += compressed_pubkey_size
86
+ return offset
87
+
88
+ def set_kas_locator(self, kas_locator: ResourceLocator):
89
+ self.kas_locator = kas_locator
90
+
91
+ def get_kas_locator(self) -> ResourceLocator | None:
92
+ return self.kas_locator
93
+
94
+ def set_ecc_mode(self, ecc_mode: ECCMode):
95
+ self.ecc_mode = ecc_mode
96
+
97
+ def get_ecc_mode(self) -> ECCMode | None:
98
+ return self.ecc_mode
99
+
100
+ def set_payload_config(self, payload_config: SymmetricAndPayloadConfig):
101
+ self.payload_config = payload_config
102
+
103
+ def get_payload_config(self) -> SymmetricAndPayloadConfig | None:
104
+ return self.payload_config
105
+
106
+ def set_policy_info(self, policy_info: PolicyInfo):
107
+ self.policy_info = policy_info
108
+
109
+ def get_policy_info(self) -> PolicyInfo | None:
110
+ return self.policy_info
111
+
112
+ def set_policy_binding(self, policy_binding: bytes):
113
+ if len(policy_binding) != self.GMAC_SIZE:
114
+ raise ValueError(
115
+ f"Policy binding must be exactly {self.GMAC_SIZE} bytes (GMAC), got {len(policy_binding)}"
116
+ )
117
+ self.policy_binding = policy_binding
118
+
119
+ def get_policy_binding(self) -> bytes | None:
120
+ return self.policy_binding
121
+
122
+ def set_ephemeral_key(self, ephemeral_key: bytes):
123
+ if self.ecc_mode is not None:
124
+ expected_size = ECCMode.get_ec_compressed_pubkey_size(
125
+ self.ecc_mode.get_elliptic_curve_type()
126
+ )
127
+ if len(ephemeral_key) != expected_size:
128
+ raise ValueError("Failed to read ephemeral key - invalid buffer size.")
129
+ self.ephemeral_key = ephemeral_key
130
+
131
+ def get_ephemeral_key(self) -> bytes | None:
132
+ return self.ephemeral_key
133
+
134
+ def get_total_size(self) -> int:
135
+ total = 0
136
+ total += self.kas_locator.get_total_size() if self.kas_locator else 0
137
+ total += 1 # ECC mode
138
+ total += 1 # payload config
139
+ total += self.policy_info.get_total_size() if self.policy_info else 0
140
+ total += self.GMAC_SIZE # policy binding (GMAC)
141
+ total += len(self.ephemeral_key) if self.ephemeral_key else 0
142
+ return total
143
+
144
+ def write_into_buffer(self, buffer: bytearray) -> int:
145
+ total_size = self.get_total_size()
146
+ if len(buffer) < total_size:
147
+ raise ValueError("Failed to write header - invalid buffer size.")
148
+ offset = 0
149
+ # ResourceLocator
150
+ n = self.kas_locator.write_into_buffer(buffer, offset)
151
+ offset += n
152
+ # ECCMode (1 byte)
153
+ buffer[offset] = self.ecc_mode.get_ecc_mode_as_byte()
154
+ offset += 1
155
+ # SymmetricAndPayloadConfig (1 byte)
156
+ buffer[offset] = self.payload_config.get_symmetric_and_payload_config_as_byte()
157
+ offset += 1
158
+ # PolicyInfo
159
+ n = self.policy_info.write_into_buffer(buffer, offset)
160
+ offset += n
161
+ # Policy binding (GMAC - 8 bytes)
162
+ if self.policy_binding:
163
+ if len(self.policy_binding) != self.GMAC_SIZE:
164
+ raise ValueError(
165
+ f"Policy binding must be exactly {self.GMAC_SIZE} bytes (GMAC), got {len(self.policy_binding)}"
166
+ )
167
+ buffer[offset : offset + self.GMAC_SIZE] = self.policy_binding
168
+ offset += self.GMAC_SIZE
169
+ else:
170
+ # Write zeros if no binding provided
171
+ buffer[offset : offset + self.GMAC_SIZE] = b"\x00" * self.GMAC_SIZE
172
+ offset += self.GMAC_SIZE
173
+ # Ephemeral key
174
+ buffer[offset : offset + len(self.ephemeral_key)] = self.ephemeral_key
175
+ offset += len(self.ephemeral_key)
176
+ return offset
177
+
178
+ def to_bytes(self):
179
+ buf = bytearray(self.get_total_size())
180
+ self.write_into_buffer(buf)
181
+ return bytes(buf)
@@ -0,0 +1,8 @@
1
+ class InvalidZipException(Exception):
2
+ """
3
+ Raised when a ZIP file is invalid or corrupted.
4
+ Based on Java implementation.
5
+ """
6
+
7
+ def __init__(self, message: str):
8
+ super().__init__(message)