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/__init__.py CHANGED
@@ -0,0 +1,25 @@
1
+ """
2
+ OpenTDF Python SDK
3
+
4
+ A Python implementation of the OpenTDF SDK for working with Trusted Data Format (TDF) files.
5
+ Provides both programmatic APIs and command-line interface for encryption and decryption.
6
+ """
7
+
8
+ from .cli import main as cli_main
9
+ from .config import KASInfo, NanoTDFConfig, TDFConfig
10
+ from .sdk import SDK
11
+ from .sdk_builder import SDKBuilder
12
+
13
+ __all__ = [
14
+ "SDK",
15
+ "KASInfo",
16
+ "NanoTDFConfig",
17
+ "SDKBuilder",
18
+ "TDFConfig",
19
+ "cli_main",
20
+ ]
21
+
22
+
23
+ def main() -> None:
24
+ """Entry point for the CLI."""
25
+ cli_main()
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Main entry point for running otdf_python as a module.
4
+
5
+ This allows the package to be run with `python -m otdf_python` and properly
6
+ handles the CLI interface without import conflicts.
7
+ """
8
+
9
+ from .cli import main
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,84 @@
1
+ """
2
+ Address normalization utilities for OpenTDF.
3
+ """
4
+
5
+ import logging
6
+ import re
7
+ from urllib.parse import urlparse
8
+
9
+ from .sdk_exceptions import SDKException
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def normalize_address(url_string: str, use_plaintext: bool) -> str:
15
+ """
16
+ Normalize a URL address to ensure it has the correct scheme and port.
17
+
18
+ Args:
19
+ url_string: The URL string to normalize
20
+ use_plaintext: If True, use http scheme, otherwise use https
21
+
22
+ Returns:
23
+ The normalized URL string
24
+
25
+ Raises:
26
+ SDKException: If there's an error parsing or creating the URL
27
+ """
28
+ scheme = "http" if use_plaintext else "https"
29
+
30
+ # Check if we have a host:port format without scheme (with non-digit port)
31
+ host_port_pattern = re.match(r"^([^/:]+):([^/]+)$", url_string)
32
+ if host_port_pattern:
33
+ host = host_port_pattern.group(1)
34
+ port_str = host_port_pattern.group(2)
35
+ try:
36
+ port = int(port_str)
37
+ except ValueError:
38
+ raise SDKException(f"Invalid port in URL [{url_string}]")
39
+
40
+ normalized_url = f"{scheme}://{host}:{port}"
41
+ logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
42
+ return normalized_url
43
+
44
+ try:
45
+ # Check if we just have a hostname without scheme and port
46
+ if "://" not in url_string and "/" not in url_string and ":" not in url_string:
47
+ port = 80 if use_plaintext else 443
48
+ normalized_url = f"{scheme}://{url_string}:{port}"
49
+ logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
50
+ return normalized_url
51
+
52
+ # Parse the URL
53
+ parsed_url = urlparse(url_string)
54
+
55
+ # If no scheme, add one
56
+ if not parsed_url.scheme:
57
+ url_string = f"{scheme}://{url_string}"
58
+ parsed_url = urlparse(url_string)
59
+
60
+ # Extract host and port
61
+ host = parsed_url.netloc.split(":")[0] if parsed_url.netloc else parsed_url.path
62
+
63
+ # If there's a port in the URL, try to extract it
64
+ port = None
65
+ if ":" in parsed_url.netloc:
66
+ _, port_str = parsed_url.netloc.split(":", 1)
67
+ try:
68
+ port = int(port_str)
69
+ except ValueError:
70
+ raise SDKException(f"Invalid port in URL [{url_string}]")
71
+
72
+ # If no port was found or extracted, use the default
73
+ if port is None:
74
+ port = 80 if use_plaintext else 443
75
+
76
+ # Reconstruct the URL with the desired scheme
77
+ normalized_url = f"{scheme}://{host}:{port}"
78
+ logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
79
+ return normalized_url
80
+
81
+ except Exception as e:
82
+ if isinstance(e, SDKException):
83
+ raise e
84
+ raise SDKException(f"Error normalizing URL [{url_string}]", e)
otdf_python/aesgcm.py ADDED
@@ -0,0 +1,55 @@
1
+ import os
2
+
3
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
4
+
5
+
6
+ class AesGcm:
7
+ GCM_NONCE_LENGTH = 12
8
+ GCM_TAG_LENGTH = 16
9
+
10
+ def __init__(self, key: bytes):
11
+ if not key or len(key) not in (16, 24, 32):
12
+ raise ValueError("Invalid key size for GCM encryption")
13
+ self.key = key
14
+ self.aesgcm = AESGCM(key)
15
+
16
+ def get_key(self) -> bytes:
17
+ return self.key
18
+
19
+ class Encrypted:
20
+ def __init__(self, iv: bytes, ciphertext: bytes):
21
+ self.iv = iv
22
+ self.ciphertext = ciphertext
23
+
24
+ def as_bytes(self) -> bytes:
25
+ return self.iv + self.ciphertext
26
+
27
+ def encrypt(
28
+ self, plaintext: bytes, offset: int = 0, length: int | None = None
29
+ ) -> "AesGcm.Encrypted":
30
+ if length is None:
31
+ length = len(plaintext) - offset
32
+ iv = os.urandom(self.GCM_NONCE_LENGTH)
33
+ ct = self.aesgcm.encrypt(iv, plaintext[offset : offset + length], None)
34
+ return self.Encrypted(iv, ct)
35
+
36
+ def encrypt_with_iv(
37
+ self,
38
+ iv: bytes,
39
+ auth_tag_len: int,
40
+ plaintext: bytes,
41
+ offset: int = 0,
42
+ length: int | None = None,
43
+ ) -> bytes:
44
+ if length is None:
45
+ length = len(plaintext) - offset
46
+ ct = self.aesgcm.encrypt(iv, plaintext[offset : offset + length], None)
47
+ return iv + ct
48
+
49
+ def decrypt(self, encrypted: "AesGcm.Encrypted") -> bytes:
50
+ return self.aesgcm.decrypt(encrypted.iv, encrypted.ciphertext, None)
51
+
52
+ def decrypt_with_iv(
53
+ self, iv: bytes, auth_tag_len: int, cipher_data: bytes
54
+ ) -> bytes:
55
+ return self.aesgcm.decrypt(iv, cipher_data, None)
@@ -0,0 +1,84 @@
1
+ from enum import Enum, auto
2
+ from typing import Any
3
+
4
+
5
+ class Type(Enum):
6
+ HANDLING_ASSERTION = "handling"
7
+ BASE_ASSERTION = "base"
8
+
9
+ def __str__(self):
10
+ return self.value
11
+
12
+
13
+ class Scope(Enum):
14
+ TRUSTED_DATA_OBJ = "tdo"
15
+ PAYLOAD = "payload"
16
+
17
+ def __str__(self):
18
+ return self.value
19
+
20
+
21
+ class AssertionKeyAlg(Enum):
22
+ RS256 = auto()
23
+ HS256 = auto()
24
+ NOT_DEFINED = auto()
25
+
26
+
27
+ class AppliesToState(Enum):
28
+ ENCRYPTED = "encrypted"
29
+ UNENCRYPTED = "unencrypted"
30
+
31
+ def __str__(self):
32
+ return self.value
33
+
34
+
35
+ class BindingMethod(Enum):
36
+ JWS = "jws"
37
+
38
+ def __str__(self):
39
+ return self.value
40
+
41
+
42
+ class AssertionKey:
43
+ def __init__(self, alg: AssertionKeyAlg, key: Any):
44
+ self.alg = alg
45
+ self.key = key
46
+
47
+ def is_defined(self):
48
+ return self.alg != AssertionKeyAlg.NOT_DEFINED
49
+
50
+
51
+ class Statement:
52
+ def __init__(self, format: str, schema: str, value: str):
53
+ self.format = format
54
+ self.schema = schema
55
+ self.value = value
56
+
57
+ def __eq__(self, other):
58
+ return (
59
+ isinstance(other, Statement)
60
+ and self.format == other.format
61
+ and self.schema == other.schema
62
+ and self.value == other.value
63
+ )
64
+
65
+ def __hash__(self):
66
+ return hash((self.format, self.schema, self.value))
67
+
68
+
69
+ class AssertionConfig:
70
+ def __init__(
71
+ self,
72
+ id: str,
73
+ type: Type,
74
+ scope: Scope,
75
+ applies_to_state: AppliesToState,
76
+ statement: Statement,
77
+ signing_key: AssertionKey | None = None,
78
+ ):
79
+ self.id = id
80
+ self.type = type
81
+ self.scope = scope
82
+ self.applies_to_state = applies_to_state
83
+ self.statement = statement
84
+ self.signing_key = signing_key
@@ -0,0 +1,198 @@
1
+ """
2
+ Asymmetric encryption and decryption utilities for RSA keys in PEM format.
3
+ """
4
+
5
+ import base64
6
+ import re
7
+
8
+ from cryptography.hazmat.backends import default_backend
9
+ from cryptography.hazmat.primitives import hashes, serialization
10
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
11
+ from cryptography.x509 import load_pem_x509_certificate
12
+
13
+ from .sdk_exceptions import SDKException
14
+
15
+
16
+ class AsymDecryption:
17
+ """
18
+ Provides functionality for asymmetric decryption using an RSA private key.
19
+
20
+ Supports both PEM string and key object initialization for flexibility.
21
+ """
22
+
23
+ CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
24
+ PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"
25
+ PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----"
26
+
27
+ def __init__(self, private_key_pem: str | None = None, private_key_obj=None):
28
+ """
29
+ Initialize with either a PEM string or a key object.
30
+
31
+ Args:
32
+ private_key_pem: Private key in PEM format (with or without headers)
33
+ private_key_obj: Pre-loaded private key object from cryptography library
34
+
35
+ Raises:
36
+ SDKException: If key loading fails
37
+ """
38
+ if private_key_obj is not None:
39
+ self.private_key = private_key_obj
40
+ elif private_key_pem is not None:
41
+ try:
42
+ # Try direct PEM loading first (most common case)
43
+ try:
44
+ self.private_key = serialization.load_pem_private_key(
45
+ private_key_pem.encode(),
46
+ password=None,
47
+ backend=default_backend(),
48
+ )
49
+ except Exception:
50
+ # Fallback: strip headers and load as DER (for base64-only keys)
51
+ private_key_pem = (
52
+ private_key_pem.replace(self.PRIVATE_KEY_HEADER, "")
53
+ .replace(self.PRIVATE_KEY_FOOTER, "")
54
+ .replace("\n", "")
55
+ .replace("\r", "")
56
+ .replace(" ", "")
57
+ )
58
+ decoded = base64.b64decode(private_key_pem)
59
+ self.private_key = serialization.load_der_private_key(
60
+ decoded, password=None, backend=default_backend()
61
+ )
62
+ except Exception as e:
63
+ raise SDKException(f"Failed to load private key: {e}")
64
+ else:
65
+ self.private_key = None
66
+
67
+ def decrypt(self, data: bytes) -> bytes:
68
+ """
69
+ Decrypt data using RSA OAEP with SHA-1.
70
+
71
+ Args:
72
+ data: Encrypted bytes to decrypt
73
+
74
+ Returns:
75
+ Decrypted bytes
76
+
77
+ Raises:
78
+ SDKException: If decryption fails or key is not set
79
+ """
80
+ if self.private_key is None:
81
+ raise SDKException("Failed to decrypt, private key is empty")
82
+ try:
83
+ return self.private_key.decrypt(
84
+ data,
85
+ padding.OAEP(
86
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
87
+ algorithm=hashes.SHA1(),
88
+ label=None,
89
+ ),
90
+ )
91
+ except Exception as e:
92
+ raise SDKException(f"Error performing decryption: {e}")
93
+
94
+
95
+ class AsymEncryption:
96
+ """
97
+ Provides functionality for asymmetric encryption using an RSA public key or certificate in PEM format.
98
+
99
+ Supports PEM public keys, X.509 certificates, and pre-loaded key objects.
100
+ Also handles base64-encoded keys without PEM headers.
101
+ """
102
+
103
+ PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----"
104
+ PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----"
105
+ CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
106
+
107
+ def __init__(self, public_key_pem: str | None = None, public_key_obj=None):
108
+ """
109
+ Initialize with either a PEM string or a key object.
110
+
111
+ Args:
112
+ public_key_pem: Public key in PEM format, X.509 certificate, or base64 string
113
+ public_key_obj: Pre-loaded public key object from cryptography library
114
+
115
+ Raises:
116
+ SDKException: If key loading fails or key is not RSA
117
+ """
118
+ if public_key_obj is not None:
119
+ self.public_key = public_key_obj
120
+ elif public_key_pem is not None:
121
+ try:
122
+ if "BEGIN CERTIFICATE" in public_key_pem:
123
+ # Load from X.509 certificate
124
+ cert = load_pem_x509_certificate(
125
+ public_key_pem.encode(), default_backend()
126
+ )
127
+ self.public_key = cert.public_key()
128
+ else:
129
+ # Try direct PEM loading first (most common case)
130
+ try:
131
+ self.public_key = serialization.load_pem_public_key(
132
+ public_key_pem.encode(), backend=default_backend()
133
+ )
134
+ except Exception:
135
+ # Fallback: strip headers and load as DER (for base64-only keys)
136
+ pem_body = re.sub(r"-----BEGIN (.*)-----", "", public_key_pem)
137
+ pem_body = re.sub(r"-----END (.*)-----", "", pem_body)
138
+ pem_body = re.sub(r"\s", "", pem_body)
139
+ decoded = base64.b64decode(pem_body)
140
+ self.public_key = serialization.load_der_public_key(
141
+ decoded, backend=default_backend()
142
+ )
143
+ except Exception as e:
144
+ raise SDKException(f"Failed to load public key: {e}")
145
+ else:
146
+ self.public_key = None
147
+
148
+ # Validate that it's an RSA key
149
+ if self.public_key is not None and not isinstance(
150
+ self.public_key, rsa.RSAPublicKey
151
+ ):
152
+ raise SDKException("Not an RSA PEM formatted public key")
153
+
154
+ def encrypt(self, data: bytes) -> bytes:
155
+ """
156
+ Encrypt data using RSA OAEP with SHA-1.
157
+
158
+ Args:
159
+ data: Plaintext bytes to encrypt
160
+
161
+ Returns:
162
+ Encrypted bytes
163
+
164
+ Raises:
165
+ SDKException: If encryption fails or key is not set
166
+ """
167
+ if self.public_key is None:
168
+ raise SDKException("Failed to encrypt, public key is empty")
169
+ try:
170
+ return self.public_key.encrypt(
171
+ data,
172
+ padding.OAEP(
173
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
174
+ algorithm=hashes.SHA1(),
175
+ label=None,
176
+ ),
177
+ )
178
+ except Exception as e:
179
+ raise SDKException(f"Error performing encryption: {e}")
180
+
181
+ def public_key_in_pem_format(self) -> str:
182
+ """
183
+ Export the public key to PEM format.
184
+
185
+ Returns:
186
+ Public key as PEM-encoded string
187
+
188
+ Raises:
189
+ SDKException: If export fails
190
+ """
191
+ try:
192
+ pem = self.public_key.public_bytes(
193
+ encoding=serialization.Encoding.PEM,
194
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
195
+ )
196
+ return pem.decode()
197
+ except Exception as e:
198
+ raise SDKException(f"Error exporting public key to PEM: {e}")
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class AuthHeaders:
6
+ """
7
+ Represents authentication headers used in token-based authorization.
8
+ This class holds authorization and DPoP (Demonstrating Proof of Possession) headers
9
+ that are used in token-based API requests.
10
+ """
11
+
12
+ auth_header: str
13
+ dpop_header: str = ""
14
+
15
+ def get_auth_header(self) -> str:
16
+ """Returns the authorization header."""
17
+ return self.auth_header
18
+
19
+ def get_dpop_header(self) -> str:
20
+ """Returns the DPoP header."""
21
+ return self.dpop_header
22
+
23
+ def to_dict(self) -> dict[str, str]:
24
+ """
25
+ Convert authentication headers to a dictionary for use with HTTP clients.
26
+
27
+ Returns:
28
+ Dictionary with 'Authorization' header and optionally 'DPoP' header
29
+ """
30
+ headers = {"Authorization": self.auth_header}
31
+ if self.dpop_header:
32
+ headers["DPoP"] = self.dpop_header
33
+ return headers
@@ -0,0 +1,113 @@
1
+ import re
2
+ import urllib.parse
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ # RuleType constants
8
+ class RuleType:
9
+ HIERARCHY = "hierarchy"
10
+ ALL_OF = "allOf"
11
+ ANY_OF = "anyOf"
12
+ UNSPECIFIED = "unspecified"
13
+ EMPTY_TERM = "DEFAULT"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class KeySplitStep:
18
+ kas: str
19
+ splitID: str
20
+
21
+ def __str__(self):
22
+ return f"KeySplitStep{{kas={self.kas}, splitID={self.splitID}}}"
23
+
24
+ def __eq__(self, other: Any) -> bool:
25
+ if not isinstance(other, KeySplitStep):
26
+ return False
27
+ return self.kas == other.kas and self.splitID == other.splitID
28
+
29
+ def __hash__(self):
30
+ return hash((self.kas, self.splitID))
31
+
32
+
33
+ class AutoConfigureException(Exception):
34
+ pass
35
+
36
+
37
+ class AttributeNameFQN:
38
+ def __init__(self, url: str):
39
+ pattern = re.compile(r"^(https?://[\w./-]+)/attr/([^/\s]*)$")
40
+ matcher = pattern.match(url)
41
+ if not matcher or not matcher.group(1) or not matcher.group(2):
42
+ raise AutoConfigureException("invalid type: attribute regex fail")
43
+ try:
44
+ urllib.parse.unquote(matcher.group(2))
45
+ except Exception:
46
+ raise AutoConfigureException(
47
+ f"invalid type: error in attribute name [{matcher.group(2)}]"
48
+ )
49
+ self.url = url
50
+ self.key = url.lower()
51
+
52
+ def __str__(self):
53
+ return self.url
54
+
55
+ def select(self, value: str):
56
+ new_url = f"{self.url}/value/{urllib.parse.quote(value)}"
57
+ return AttributeValueFQN(new_url)
58
+
59
+ def prefix(self):
60
+ return self.url
61
+
62
+ def get_key(self):
63
+ return self.key
64
+
65
+ def authority(self):
66
+ pattern = re.compile(r"^(https?://[\w./-]+)/attr/[^/\s]*$")
67
+ matcher = pattern.match(self.url)
68
+ if not matcher:
69
+ raise AutoConfigureException("invalid type")
70
+ return matcher.group(1)
71
+
72
+ def name(self):
73
+ pattern = re.compile(r"^https?://[\w./-]+/attr/([^/\s]*)$")
74
+ matcher = pattern.match(self.url)
75
+ if not matcher:
76
+ raise AutoConfigureException("invalid attribute")
77
+ try:
78
+ return urllib.parse.unquote(matcher.group(1))
79
+ except Exception:
80
+ raise AutoConfigureException("invalid type")
81
+
82
+
83
+ class AttributeValueFQN:
84
+ def __init__(self, url: str):
85
+ pattern = re.compile(r"^(https?://[\w./-]+)/attr/(\S*)/value/(\S*)$")
86
+ matcher = pattern.match(url)
87
+ if (
88
+ not matcher
89
+ or not matcher.group(1)
90
+ or not matcher.group(2)
91
+ or not matcher.group(3)
92
+ ):
93
+ raise AutoConfigureException(
94
+ f"invalid type: attribute regex fail for [{url}]"
95
+ )
96
+ try:
97
+ urllib.parse.unquote(matcher.group(2))
98
+ urllib.parse.unquote(matcher.group(3))
99
+ except Exception:
100
+ raise AutoConfigureException("invalid type: error in attribute or value")
101
+ self.url = url
102
+ self.key = url.lower()
103
+
104
+ def __str__(self):
105
+ return self.url
106
+
107
+ def __eq__(self, other: Any) -> bool:
108
+ if not isinstance(other, AttributeValueFQN):
109
+ return False
110
+ return self.key == other.key
111
+
112
+ def __hash__(self):
113
+ return hash(self.key)