otdf-python 0.3.0__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.
@@ -2,6 +2,9 @@
2
2
  Asymmetric encryption and decryption utilities for RSA keys in PEM format.
3
3
  """
4
4
 
5
+ import base64
6
+ import re
7
+
5
8
  from cryptography.hazmat.backends import default_backend
6
9
  from cryptography.hazmat.primitives import hashes, serialization
7
10
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
@@ -13,18 +16,68 @@ from .sdk_exceptions import SDKException
13
16
  class AsymDecryption:
14
17
  """
15
18
  Provides functionality for asymmetric decryption using an RSA private key.
19
+
20
+ Supports both PEM string and key object initialization for flexibility.
16
21
  """
17
22
 
18
- def __init__(self, private_key_pem: str):
19
- try:
20
- self.private_key = serialization.load_pem_private_key(
21
- private_key_pem.encode(), password=None, backend=default_backend()
22
- )
23
- except Exception as e:
24
- raise SDKException(f"Failed to load private key: {e}")
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
25
66
 
26
67
  def decrypt(self, data: bytes) -> bytes:
27
- if not self.private_key:
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:
28
81
  raise SDKException("Failed to decrypt, private key is empty")
29
82
  try:
30
83
  return self.private_key.decrypt(
@@ -42,26 +95,77 @@ class AsymDecryption:
42
95
  class AsymEncryption:
43
96
  """
44
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.
45
101
  """
46
102
 
47
- def __init__(self, public_key_pem: str):
48
- try:
49
- if "BEGIN CERTIFICATE" in public_key_pem:
50
- cert = load_pem_x509_certificate(
51
- public_key_pem.encode(), default_backend()
52
- )
53
- self.public_key = cert.public_key()
54
- else:
55
- self.public_key = serialization.load_pem_public_key(
56
- public_key_pem.encode(), backend=default_backend()
57
- )
58
- except Exception as e:
59
- raise SDKException(f"Failed to load public key: {e}")
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
60
147
 
61
- if not isinstance(self.public_key, rsa.RSAPublicKey):
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
+ ):
62
152
  raise SDKException("Not an RSA PEM formatted public key")
63
153
 
64
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")
65
169
  try:
66
170
  return self.public_key.encrypt(
67
171
  data,
@@ -75,6 +179,15 @@ class AsymEncryption:
75
179
  raise SDKException(f"Error performing encryption: {e}")
76
180
 
77
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
+ """
78
191
  try:
79
192
  pem = self.public_key.public_bytes(
80
193
  encoding=serialization.Encoding.PEM,
@@ -10,7 +10,7 @@ class AuthHeaders:
10
10
  """
11
11
 
12
12
  auth_header: str
13
- dpop_header: str
13
+ dpop_header: str = ""
14
14
 
15
15
  def get_auth_header(self) -> str:
16
16
  """Returns the authorization header."""
@@ -19,3 +19,15 @@ class AuthHeaders:
19
19
  def get_dpop_header(self) -> str:
20
20
  """Returns the DPoP header."""
21
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
otdf_python/cli.py CHANGED
@@ -20,7 +20,6 @@ from otdf_python.config import KASInfo, NanoTDFConfig, TDFConfig
20
20
  from otdf_python.sdk import SDK
21
21
  from otdf_python.sdk_builder import SDKBuilder
22
22
  from otdf_python.sdk_exceptions import SDKException
23
- from otdf_python.tdf import TDFReaderConfig
24
23
 
25
24
  try:
26
25
  __version__ = metadata.version("otdf-python")
@@ -89,7 +88,7 @@ def load_client_credentials(creds_file_path: str) -> tuple[str, str]:
89
88
  "CRITICAL", f"Credentials file does not exist: {creds_file_path}"
90
89
  )
91
90
 
92
- with open(creds_path) as f:
91
+ with creds_path.open() as f:
93
92
  creds = json.load(f)
94
93
 
95
94
  client_id = creds.get("clientId")
@@ -202,6 +201,13 @@ def create_nano_tdf_config(sdk: SDK, args) -> NanoTDFConfig:
202
201
  kas_endpoints = parse_kas_endpoints(args.kas_endpoint)
203
202
  kas_info_list = [KASInfo(url=kas_url) for kas_url in kas_endpoints]
204
203
  config.kas_info_list.extend(kas_info_list)
204
+ elif args.platform_url:
205
+ # If no explicit KAS endpoint provided, derive from platform URL
206
+ # This matches the default KAS path convention
207
+ kas_url = args.platform_url.rstrip("/") + "/kas"
208
+ logger.debug(f"Deriving KAS endpoint from platform URL: {kas_url}")
209
+ kas_info = KASInfo(url=kas_url)
210
+ config.kas_info_list.append(kas_info)
205
211
 
206
212
  if hasattr(args, "policy_binding") and args.policy_binding:
207
213
  if args.policy_binding.lower() == "ecdsa":
@@ -224,13 +230,13 @@ def cmd_encrypt(args):
224
230
 
225
231
  try:
226
232
  # Read input file
227
- with open(input_path, "rb") as input_file:
233
+ with input_path.open("rb") as input_file:
228
234
  payload = input_file.read()
229
235
 
230
236
  # Determine output
231
237
  if args.output:
232
238
  output_path = Path(args.output)
233
- with open(output_path, "wb") as output_file:
239
+ with output_path.open("wb") as output_file:
234
240
  try:
235
241
  # Create appropriate config based on container type
236
242
  container_type = getattr(args, "container_type", "tdf")
@@ -248,7 +254,7 @@ def cmd_encrypt(args):
248
254
  logger.debug("Creating TDF")
249
255
  config = create_tdf_config(sdk, args)
250
256
  output_stream = BytesIO()
251
- manifest, size, _ = sdk.create_tdf(
257
+ _manifest, size, _ = sdk.create_tdf(
252
258
  BytesIO(payload), config, output_stream
253
259
  )
254
260
  output_file.write(output_stream.getvalue())
@@ -275,7 +281,7 @@ def cmd_encrypt(args):
275
281
  logger.debug("Creating TDF")
276
282
  config = create_tdf_config(sdk, args)
277
283
  output_stream = BytesIO()
278
- manifest, size, _ = sdk.create_tdf(
284
+ _manifest, size, _ = sdk.create_tdf(
279
285
  BytesIO(payload), config, output_stream
280
286
  )
281
287
  output_file.write(output_stream.getvalue())
@@ -297,23 +303,20 @@ def cmd_decrypt(args):
297
303
 
298
304
  try:
299
305
  # Read encrypted file
300
- with open(input_path, "rb") as input_file:
306
+ with input_path.open("rb") as input_file:
301
307
  encrypted_data = input_file.read()
302
308
 
303
309
  # Determine output
304
310
  if args.output:
305
311
  output_path = Path(args.output)
306
- with open(output_path, "wb") as output_file:
312
+ with output_path.open("wb") as output_file:
307
313
  try:
308
314
  # Try to determine if it's a NanoTDF or regular TDF
309
315
  # NanoTDFs have a specific header format, regular TDFs are ZIP files
310
316
  if encrypted_data.startswith(b"PK"):
311
317
  # Regular TDF (ZIP format)
312
318
  logger.debug("Decrypting TDF")
313
- reader_config = TDFReaderConfig()
314
- tdf_reader = sdk.load_tdf_with_config(
315
- encrypted_data, reader_config
316
- )
319
+ tdf_reader = sdk.load_tdf(encrypted_data)
317
320
  # Access payload directly from TDFReader
318
321
  payload_bytes = tdf_reader.payload
319
322
  output_file.write(payload_bytes)
@@ -336,8 +339,7 @@ def cmd_decrypt(args):
336
339
  if encrypted_data.startswith(b"PK"):
337
340
  # Regular TDF (ZIP format)
338
341
  logger.debug("Decrypting TDF")
339
- reader_config = TDFReaderConfig()
340
- tdf_reader = sdk.load_tdf_with_config(encrypted_data, reader_config)
342
+ tdf_reader = sdk.load_tdf(encrypted_data)
341
343
  payload_bytes = tdf_reader.payload
342
344
  output_file.write(payload_bytes)
343
345
  logger.info("Successfully decrypted TDF")
@@ -364,16 +366,13 @@ def cmd_inspect(args):
364
366
 
365
367
  try:
366
368
  # Read encrypted file
367
- with open(input_path, "rb") as input_file:
369
+ with input_path.open("rb") as input_file:
368
370
  encrypted_data = input_file.read()
369
371
 
370
372
  if encrypted_data.startswith(b"PK"):
371
373
  # Regular TDF
372
374
  logger.debug("Inspecting TDF")
373
- reader_config = TDFReaderConfig()
374
- tdf_reader = sdk.load_tdf_with_config(
375
- BytesIO(encrypted_data), reader_config
376
- )
375
+ tdf_reader = sdk.load_tdf(BytesIO(encrypted_data))
377
376
  manifest = tdf_reader.manifest
378
377
 
379
378
  # Try to get data attributes
@@ -408,7 +407,7 @@ def cmd_inspect(args):
408
407
  except Exception as e:
409
408
  # If we can't inspect due to auth issues, show what we can
410
409
  logger.warning(f"Limited inspection due to: {e}")
411
- with open(input_path, "rb") as input_file:
410
+ with input_path.open("rb") as input_file:
412
411
  encrypted_data = input_file.read()
413
412
 
414
413
  file_type = "TDF" if encrypted_data.startswith(b"PK") else "NanoTDF"
@@ -562,7 +561,7 @@ def main():
562
561
  sys.exit(1)
563
562
  except Exception as e:
564
563
  logger.error(f"Unexpected error: {e}")
565
- logger.debug("", exc_info=True)
564
+ logger.error("", exc_info=True) # Always print traceback for unexpected errors
566
565
  sys.exit(1)
567
566
 
568
567
 
@@ -0,0 +1,176 @@
1
+ """
2
+ Elliptic Curve Constants for NanoTDF.
3
+
4
+ This module defines shared constants for elliptic curve operations used across
5
+ the SDK, particularly for NanoTDF encryption/decryption.
6
+
7
+ All supported curves follow the NanoTDF specification which uses compressed
8
+ public key encoding (X9.62 format) to minimize header size.
9
+ """
10
+
11
+ from typing import ClassVar
12
+
13
+ from cryptography.hazmat.primitives.asymmetric import ec
14
+
15
+
16
+ class ECCConstants:
17
+ """
18
+ Centralized constants for elliptic curve cryptography operations.
19
+
20
+ This class provides mappings between curve names, curve type integers,
21
+ cryptography curve objects, and compressed public key sizes.
22
+ """
23
+
24
+ # Mapping from curve names (strings) to curve type integers (per NanoTDF spec)
25
+ # These integer values are encoded in the NanoTDF header's ECC mode byte
26
+ CURVE_NAME_TO_TYPE: ClassVar[dict[str, int]] = {
27
+ "secp256r1": 0, # NIST P-256 (most common)
28
+ "secp384r1": 1, # NIST P-384
29
+ "secp521r1": 2, # NIST P-521
30
+ "secp256k1": 3, # Bitcoin curve (secp256k1)
31
+ }
32
+
33
+ # Mapping from curve type integers to curve names
34
+ # Inverse of CURVE_NAME_TO_TYPE for reverse lookups
35
+ CURVE_TYPE_TO_NAME: ClassVar[dict[int, str]] = {
36
+ 0: "secp256r1",
37
+ 1: "secp384r1",
38
+ 2: "secp521r1",
39
+ 3: "secp256k1",
40
+ }
41
+
42
+ # Compressed public key sizes (in bytes) for each curve
43
+ # Format: 1 byte prefix (0x02 or 0x03) + x-coordinate bytes
44
+ # Used by both ecc_mode.py (indexed by int) and ecdh.py (indexed by string)
45
+ COMPRESSED_KEY_SIZE_BY_TYPE: ClassVar[dict[int, int]] = {
46
+ 0: 33, # secp256r1: 1 byte prefix + 32 bytes x-coordinate
47
+ 1: 49, # secp384r1: 1 byte prefix + 48 bytes x-coordinate
48
+ 2: 67, # secp521r1: 1 byte prefix + 66 bytes x-coordinate
49
+ 3: 33, # secp256k1: 1 byte prefix + 32 bytes x-coordinate (same as secp256r1)
50
+ }
51
+
52
+ COMPRESSED_KEY_SIZE_BY_NAME: ClassVar[dict[str, int]] = {
53
+ "secp256r1": 33, # 1 byte prefix + 32 bytes
54
+ "secp384r1": 49, # 1 byte prefix + 48 bytes
55
+ "secp521r1": 67, # 1 byte prefix + 66 bytes
56
+ "secp256k1": 33, # 1 byte prefix + 32 bytes
57
+ }
58
+
59
+ # Mapping from curve names to cryptography library curve objects
60
+ # Used by ecdh.py for key generation and ECDH operations
61
+ CURVE_OBJECTS: ClassVar[dict[str, ec.EllipticCurve]] = {
62
+ "secp256r1": ec.SECP256R1(),
63
+ "secp384r1": ec.SECP384R1(),
64
+ "secp521r1": ec.SECP521R1(),
65
+ "secp256k1": ec.SECP256K1(),
66
+ }
67
+
68
+ @classmethod
69
+ def get_curve_name(cls, curve_type: int) -> str:
70
+ """
71
+ Get curve name from curve type integer.
72
+
73
+ Args:
74
+ curve_type: Curve type (0=secp256r1, 1=secp384r1, 2=secp521r1, 3=secp256k1)
75
+
76
+ Returns:
77
+ Curve name as string (e.g., "secp256r1")
78
+
79
+ Raises:
80
+ ValueError: If curve_type is not supported
81
+ """
82
+ name = cls.CURVE_TYPE_TO_NAME.get(curve_type)
83
+ if name is None:
84
+ raise ValueError(
85
+ f"Unsupported curve type: {curve_type}. "
86
+ f"Supported types: {list(cls.CURVE_TYPE_TO_NAME.keys())}"
87
+ )
88
+ return name
89
+
90
+ @classmethod
91
+ def get_curve_type(cls, curve_name: str) -> int:
92
+ """
93
+ Get curve type integer from curve name.
94
+
95
+ Args:
96
+ curve_name: Curve name (e.g., "secp256r1")
97
+
98
+ Returns:
99
+ Curve type as integer (0-3)
100
+
101
+ Raises:
102
+ ValueError: If curve_name is not supported
103
+ """
104
+ curve_type = cls.CURVE_NAME_TO_TYPE.get(curve_name.lower())
105
+ if curve_type is None:
106
+ raise ValueError(
107
+ f"Unsupported curve name: '{curve_name}'. "
108
+ f"Supported curves: {list(cls.CURVE_NAME_TO_TYPE.keys())}"
109
+ )
110
+ return curve_type
111
+
112
+ @classmethod
113
+ def get_compressed_key_size_by_type(cls, curve_type: int) -> int:
114
+ """
115
+ Get compressed public key size from curve type integer.
116
+
117
+ Args:
118
+ curve_type: Curve type (0=secp256r1, 1=secp384r1, 2=secp521r1, 3=secp256k1)
119
+
120
+ Returns:
121
+ Size in bytes of the compressed public key
122
+
123
+ Raises:
124
+ ValueError: If curve_type is not supported
125
+ """
126
+ size = cls.COMPRESSED_KEY_SIZE_BY_TYPE.get(curve_type)
127
+ if size is None:
128
+ raise ValueError(
129
+ f"Unsupported curve type: {curve_type}. "
130
+ f"Supported types: {list(cls.COMPRESSED_KEY_SIZE_BY_TYPE.keys())}"
131
+ )
132
+ return size
133
+
134
+ @classmethod
135
+ def get_compressed_key_size_by_name(cls, curve_name: str) -> int:
136
+ """
137
+ Get compressed public key size from curve name.
138
+
139
+ Args:
140
+ curve_name: Curve name (e.g., "secp256r1")
141
+
142
+ Returns:
143
+ Size in bytes of the compressed public key
144
+
145
+ Raises:
146
+ ValueError: If curve_name is not supported
147
+ """
148
+ size = cls.COMPRESSED_KEY_SIZE_BY_NAME.get(curve_name.lower())
149
+ if size is None:
150
+ raise ValueError(
151
+ f"Unsupported curve name: '{curve_name}'. "
152
+ f"Supported curves: {list(cls.COMPRESSED_KEY_SIZE_BY_NAME.keys())}"
153
+ )
154
+ return size
155
+
156
+ @classmethod
157
+ def get_curve_object(cls, curve_name: str) -> ec.EllipticCurve:
158
+ """
159
+ Get cryptography library curve object from curve name.
160
+
161
+ Args:
162
+ curve_name: Curve name (e.g., "secp256r1")
163
+
164
+ Returns:
165
+ Cryptography library EllipticCurve object
166
+
167
+ Raises:
168
+ ValueError: If curve_name is not supported
169
+ """
170
+ curve = cls.CURVE_OBJECTS.get(curve_name.lower())
171
+ if curve is None:
172
+ raise ValueError(
173
+ f"Unsupported curve name: '{curve_name}'. "
174
+ f"Supported curves: {list(cls.CURVE_OBJECTS.keys())}"
175
+ )
176
+ return curve
otdf_python/ecc_mode.py CHANGED
@@ -1,4 +1,15 @@
1
+ from otdf_python.ecc_constants import ECCConstants
2
+
3
+
1
4
  class ECCMode:
5
+ """
6
+ ECC (Elliptic Curve Cryptography) mode configuration for NanoTDF.
7
+
8
+ This class encapsulates the curve type and policy binding mode (GMAC vs ECDSA)
9
+ that are encoded in the NanoTDF header. It delegates to ECCConstants for
10
+ curve-related lookups to maintain a single source of truth.
11
+ """
12
+
2
13
  def __init__(self, curve_mode: int = 0, use_ecdsa_binding: bool = False):
3
14
  self.curve_mode = curve_mode
4
15
  self.use_ecdsa_binding = use_ecdsa_binding
@@ -15,18 +26,58 @@ class ECCMode:
15
26
  def get_elliptic_curve_type(self) -> int:
16
27
  return self.curve_mode
17
28
 
29
+ def get_curve_name(self) -> str:
30
+ """Get the curve name as a string (e.g., 'secp256r1').
31
+
32
+ Returns:
33
+ Curve name corresponding to the current curve_mode
34
+
35
+ Raises:
36
+ ValueError: If curve_mode is not supported
37
+ """
38
+ # Delegate to ECCConstants for the authoritative mapping
39
+ return ECCConstants.get_curve_name(self.curve_mode)
40
+
18
41
  @staticmethod
19
42
  def get_ec_compressed_pubkey_size(curve_type: int) -> int:
20
- # 0: secp256r1, 1: secp384r1, 2: secp521r1
21
- if curve_type == 0:
22
- return 33
23
- elif curve_type == 1:
24
- return 49
25
- elif curve_type == 2:
26
- return 67
27
- else:
28
- raise ValueError("Unsupported ECC algorithm.")
43
+ """Get the compressed public key size for a given curve type.
44
+
45
+ Args:
46
+ curve_type: Curve type identifier (0=secp256r1, 1=secp384r1, 2=secp521r1, 3=secp256k1)
47
+
48
+ Returns:
49
+ Size in bytes of the compressed public key
50
+
51
+ Raises:
52
+ ValueError: If curve_type is not supported
53
+ """
54
+ # Delegate to ECCConstants for the authoritative mapping
55
+ return ECCConstants.get_compressed_key_size_by_type(curve_type)
29
56
 
30
57
  def get_ecc_mode_as_byte(self) -> int:
31
58
  # Most significant bit: use_ecdsa_binding, lower 3 bits: curve_mode
32
59
  return ((1 if self.use_ecdsa_binding else 0) << 7) | (self.curve_mode & 0x07)
60
+
61
+ @staticmethod
62
+ def from_string(curve_str: str) -> "ECCMode":
63
+ """Create ECCMode from curve string or policy binding type.
64
+
65
+ Args:
66
+ curve_str: Either a curve name ('secp256r1', 'secp384r1', 'secp521r1', 'secp256k1')
67
+ or a policy binding type ('gmac', 'ecdsa')
68
+
69
+ Returns:
70
+ ECCMode instance configured with the appropriate curve and binding mode
71
+
72
+ Raises:
73
+ ValueError: If curve_str is not a supported curve or binding type
74
+ """
75
+ # Handle policy binding types (always use secp256r1 as default curve)
76
+ if curve_str.lower() == "gmac":
77
+ return ECCMode(0, False) # GMAC binding with default secp256r1 curve
78
+ elif curve_str.lower() == "ecdsa":
79
+ return ECCMode(0, True) # ECDSA binding with default secp256r1 curve
80
+
81
+ # Handle curve names - delegate to ECCConstants for the authoritative mapping
82
+ curve_mode = ECCConstants.get_curve_type(curve_str)
83
+ return ECCMode(curve_mode, False)