From e2725c32bf9488aa50d3f0ca00e835125aeabb2e Mon Sep 17 00:00:00 2001 From: Di Mei Date: Sat, 22 Feb 2025 16:59:44 -0500 Subject: [PATCH 1/6] support Edwards key --- cdp/cdp.py | 2 +- cdp/cdp_api_client.py | 43 ++++++++++++++++++++++++++++++++----------- cdp/wallet.py | 40 ++++++++++++++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/cdp/cdp.py b/cdp/cdp.py index 84c2416..f7a62d9 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -118,7 +118,7 @@ def configure_from_json( """ with open(os.path.expanduser(file_path)) as file: data = json.load(file) - api_key_name = data.get("name") + api_key_name = data.get("name") or data.get("id") private_key = data.get("privateKey") if not api_key_name: raise InvalidConfigurationError("Invalid JSON format: Missing 'api_key_name'") diff --git a/cdp/cdp_api_client.py b/cdp/cdp_api_client.py index 15a7d2f..fd4aa05 100644 --- a/cdp/cdp_api_client.py +++ b/cdp/cdp_api_client.py @@ -1,10 +1,11 @@ +import base64 import random import time from urllib.parse import urlparse import jwt from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 from urllib3.util import Retry from cdp import __version__ @@ -172,17 +173,37 @@ def _build_jwt(self, url: str, method: str = "GET") -> str: str: The JWT for the given API endpoint URL. """ + private_key_obj = None + key_data = self.private_key.encode() + # Business change: Support both ECDSA and Ed25519 keys. try: - private_key = serialization.load_pem_private_key( - self.private_key.encode(), password=None - ) - if not isinstance(private_key, ec.EllipticCurvePrivateKey): - raise InvalidAPIKeyFormatError("Invalid key type") - except Exception as e: - raise InvalidAPIKeyFormatError("Could not parse the private key") from e + # Try loading as a PEM-encoded key (typically for ECDSA keys). + private_key_obj = serialization.load_pem_private_key(key_data, password=None) + except Exception: + # If PEM loading fails, assume the key is provided as base64-encoded raw bytes (Ed25519). + try: + decoded_key = base64.b64decode(self.private_key) + if len(decoded_key) == 32: + private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key) + elif len(decoded_key) == 64: + private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key[:32]) + else: + raise InvalidAPIKeyFormatError( + "Ed25519 private key must be 32 or 64 bytes after base64 decoding" + ) + except Exception as e2: + raise InvalidAPIKeyFormatError("Could not parse the private key") from e2 + + # Determine signing algorithm based on the key type. + if isinstance(private_key_obj, ec.EllipticCurvePrivateKey): + alg = "ES256" + elif isinstance(private_key_obj, ed25519.Ed25519PrivateKey): + alg = "EdDSA" + else: + raise InvalidAPIKeyFormatError("Unsupported key type") header = { - "alg": "ES256", + "alg": alg, "kid": self.api_key, "typ": "JWT", "nonce": self._nonce(), @@ -195,12 +216,12 @@ def _build_jwt(self, url: str, method: str = "GET") -> str: "iss": "cdp", "aud": ["cdp_service"], "nbf": int(time.time()), - "exp": int(time.time()) + 60, # +1 minute + "exp": int(time.time()) + 60, # Token valid for 1 minute "uris": [uri], } try: - return jwt.encode(claims, private_key, algorithm="ES256", headers=header) + return jwt.encode(claims, private_key_obj, algorithm=alg, headers=header) except Exception as e: print(f"Error during JWT signing: {e!s}") raise InvalidAPIKeyFormatError("Could not sign the JWT") from e diff --git a/cdp/wallet.py b/cdp/wallet.py index 5707197..107d10e 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -1,3 +1,4 @@ +import base64 import builtins import hashlib import json @@ -12,7 +13,7 @@ from bip_utils import Bip32Slip10Secp256k1, Bip39MnemonicValidator, Bip39SeedGenerator from Crypto.Cipher import AES from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 from eth_account import Account from cdp.address import Address @@ -692,13 +693,36 @@ def _encryption_key(self) -> bytes: bytes: The generated encryption key. """ - private_key = serialization.load_pem_private_key(Cdp.private_key.encode(), password=None) - - public_key = private_key.public_key() - - shared_secret = private_key.exchange(ec.ECDH(), public_key) - - return hashlib.sha256(shared_secret).digest() + try: + key_obj = serialization.load_pem_private_key(Cdp.private_key.encode(), password=None) + except Exception: + # If PEM loading fails, assume the key is provided as a base64-encoded Ed25519 key. + try: + decoded = base64.b64decode(Cdp.private_key) + if len(decoded) == 32: + key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded) + elif len(decoded) == 64: + key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded[:32]) + else: + raise ValueError("Invalid Ed25519 key length") + except Exception as e2: + raise ValueError("Could not parse the private key") from e2 + + # For ECDSA keys, perform an ECDH exchange with its own public key. + if isinstance(key_obj, ec.EllipticCurvePrivateKey): + public_key = key_obj.public_key() + shared_secret = key_obj.exchange(ec.ECDH(), public_key) + return hashlib.sha256(shared_secret).digest() + # For Ed25519 keys, derive the encryption key by hashing the raw private key bytes. + elif isinstance(key_obj, ed25519.Ed25519PrivateKey): + raw_bytes = key_obj.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + return hashlib.sha256(raw_bytes).digest() + else: + raise ValueError("Unsupported key type for encryption key derivation") def _existing_seeds(self, file_path: str) -> dict[str, Any]: """Load existing seeds from a file. From 99c063edcd59043f4dee7216c9517c04f5db9135 Mon Sep 17 00:00:00 2001 From: Di Mei Date: Mon, 24 Feb 2025 13:52:58 -0500 Subject: [PATCH 2/6] create an api key helper --- cdp/api_key_helpers.py | 21 +++++++++++++++++++ cdp/cdp_api_client.py | 36 ++++---------------------------- cdp/wallet.py | 27 ++++-------------------- tests/test_api_key_helpers.py | 39 +++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 cdp/api_key_helpers.py create mode 100644 tests/test_api_key_helpers.py diff --git a/cdp/api_key_helpers.py b/cdp/api_key_helpers.py new file mode 100644 index 0000000..c7e3d97 --- /dev/null +++ b/cdp/api_key_helpers.py @@ -0,0 +1,21 @@ +import base64 + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 + + +def _parse_private_key(key_str: str): + key_data = key_str.encode() + try: + return serialization.load_pem_private_key(key_data, password=None) + except Exception: + try: + decoded_key = base64.b64decode(key_str) + if len(decoded_key) == 32: + return ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key) + elif len(decoded_key) == 64: + return ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key[:32]) + else: + raise ValueError("Ed25519 private key must be 32 or 64 bytes after base64 decoding") + except Exception as e: + raise ValueError("Could not parse the private key") from e diff --git a/cdp/cdp_api_client.py b/cdp/cdp_api_client.py index fd4aa05..8d63904 100644 --- a/cdp/cdp_api_client.py +++ b/cdp/cdp_api_client.py @@ -1,14 +1,13 @@ -import base64 import random import time from urllib.parse import urlparse import jwt -from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, ed25519 from urllib3.util import Retry from cdp import __version__ +from cdp.api_key_helpers import _parse_private_key from cdp.client import rest from cdp.client.api_client import ApiClient from cdp.client.api_response import ApiResponse @@ -163,36 +162,9 @@ def _apply_headers(self, url: str, method: str, header_params: dict[str, str]) - header_params["Correlation-Context"] = self._get_correlation_data() def _build_jwt(self, url: str, method: str = "GET") -> str: - """Build the JWT for the given API endpoint URL. - - Args: - url (str): The URL to authenticate. - method (str): The HTTP method to use. - - Returns: - str: The JWT for the given API endpoint URL. - - """ - private_key_obj = None - key_data = self.private_key.encode() - # Business change: Support both ECDSA and Ed25519 keys. - try: - # Try loading as a PEM-encoded key (typically for ECDSA keys). - private_key_obj = serialization.load_pem_private_key(key_data, password=None) - except Exception: - # If PEM loading fails, assume the key is provided as base64-encoded raw bytes (Ed25519). - try: - decoded_key = base64.b64decode(self.private_key) - if len(decoded_key) == 32: - private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key) - elif len(decoded_key) == 64: - private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key[:32]) - else: - raise InvalidAPIKeyFormatError( - "Ed25519 private key must be 32 or 64 bytes after base64 decoding" - ) - except Exception as e2: - raise InvalidAPIKeyFormatError("Could not parse the private key") from e2 + """Build the JWT for the given API endpoint URL.""" + # Parse the private key using our helper function. + private_key_obj = _parse_private_key(self.private_key) # Determine signing algorithm based on the key type. if isinstance(private_key_obj, ec.EllipticCurvePrivateKey): diff --git a/cdp/wallet.py b/cdp/wallet.py index 107d10e..61a5899 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -1,4 +1,3 @@ -import base64 import builtins import hashlib import json @@ -17,6 +16,7 @@ from eth_account import Account from cdp.address import Address +from cdp.api_key_helpers import _parse_private_key from cdp.balance_map import BalanceMap from cdp.cdp import Cdp from cdp.client.models.address import Address as AddressModel @@ -687,33 +687,14 @@ def load_seed_from_file(self, file_path: str) -> None: self._master = self._set_master_node() def _encryption_key(self) -> bytes: - """Generate an encryption key based on the private key. + """Generate an encryption key based on the private key.""" + # Use Cdp.private_key instead of self.private_key + key_obj = _parse_private_key(Cdp.private_key) - Returns: - bytes: The generated encryption key. - - """ - try: - key_obj = serialization.load_pem_private_key(Cdp.private_key.encode(), password=None) - except Exception: - # If PEM loading fails, assume the key is provided as a base64-encoded Ed25519 key. - try: - decoded = base64.b64decode(Cdp.private_key) - if len(decoded) == 32: - key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded) - elif len(decoded) == 64: - key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded[:32]) - else: - raise ValueError("Invalid Ed25519 key length") - except Exception as e2: - raise ValueError("Could not parse the private key") from e2 - - # For ECDSA keys, perform an ECDH exchange with its own public key. if isinstance(key_obj, ec.EllipticCurvePrivateKey): public_key = key_obj.public_key() shared_secret = key_obj.exchange(ec.ECDH(), public_key) return hashlib.sha256(shared_secret).digest() - # For Ed25519 keys, derive the encryption key by hashing the raw private key bytes. elif isinstance(key_obj, ed25519.Ed25519PrivateKey): raw_bytes = key_obj.private_bytes( encoding=serialization.Encoding.Raw, diff --git a/tests/test_api_key_helpers.py b/tests/test_api_key_helpers.py new file mode 100644 index 0000000..5f3cbfc --- /dev/null +++ b/tests/test_api_key_helpers.py @@ -0,0 +1,39 @@ +import base64 +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 +from cryptography.hazmat.primitives import serialization + +from cdp.api_key_helpers import _parse_private_key + + +DUMMY_ECDSA_PEM = "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMM75bm9WZCYPkfjXSUWNU5eHx47fWM2IpG8ki90BhRDoAoGCCqGSM49\nAwEHoUQDQgAEicwlaAqy7Z4SS7lvrEYoy6qR9Kf0n0jFzg+XExcXKU1JMr18z47W\n5mrftEqWIqPCLQ16ByoKW2Bsup5V3q9P4g==\n-----END EC PRIVATE KEY-----\n" +DUMMY_ED25519_BASE64 = "BXyKC+eFINc/6ztE/3neSaPGgeiU9aDRpaDnAbaA/vyTrUNgtuh/1oX6Vp+OEObV3SLWF+OkF2EQNPtpl0pbfA==" + +def test_parse_private_key_pem_ec(): + """Test that a PEM-encoded ECDSA key is parsed correctly using a hardcoded dummy key.""" + parsed_key = _parse_private_key(DUMMY_ECDSA_PEM) + assert isinstance(parsed_key, ec.EllipticCurvePrivateKey) + + +def test_parse_private_key_ed25519_32(): + """Test that a base64-encoded 32-byte Ed25519 key is parsed correctly using a hardcoded dummy key.""" + parsed_key = _parse_private_key(DUMMY_ED25519_BASE64) + assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + + +def test_parse_private_key_ed25519_64(): + """Test that a base64-encoded 64-byte input is parsed correctly by taking the first 32 bytes.""" + # Create a 64-byte dummy by concatenating the 32-byte dummy with itself. + dummy_64 = b'\x01' * 32 + b'\x01' * 32 + dummy_64_base64 = base64.b64encode(dummy_64).decode("utf-8") + parsed_key = _parse_private_key(dummy_64_base64) + assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + + +def test_parse_private_key_invalid(): + """Test that an invalid key string raises a ValueError.""" + try: + _parse_private_key("invalid_key") + except ValueError as e: + assert "Could not parse the private key" in str(e) + else: + assert False, "Expected ValueError was not raised" From c7478106ed67bb32652c957219cc3f017339ace6 Mon Sep 17 00:00:00 2001 From: Di Mei Date: Mon, 24 Feb 2025 13:54:39 -0500 Subject: [PATCH 3/6] lint error fix --- tests/test_api_key_helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_api_key_helpers.py b/tests/test_api_key_helpers.py index 5f3cbfc..4ed1ddc 100644 --- a/tests/test_api_key_helpers.py +++ b/tests/test_api_key_helpers.py @@ -1,10 +1,9 @@ import base64 + from cryptography.hazmat.primitives.asymmetric import ec, ed25519 -from cryptography.hazmat.primitives import serialization from cdp.api_key_helpers import _parse_private_key - DUMMY_ECDSA_PEM = "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMM75bm9WZCYPkfjXSUWNU5eHx47fWM2IpG8ki90BhRDoAoGCCqGSM49\nAwEHoUQDQgAEicwlaAqy7Z4SS7lvrEYoy6qR9Kf0n0jFzg+XExcXKU1JMr18z47W\n5mrftEqWIqPCLQ16ByoKW2Bsup5V3q9P4g==\n-----END EC PRIVATE KEY-----\n" DUMMY_ED25519_BASE64 = "BXyKC+eFINc/6ztE/3neSaPGgeiU9aDRpaDnAbaA/vyTrUNgtuh/1oX6Vp+OEObV3SLWF+OkF2EQNPtpl0pbfA==" @@ -36,4 +35,4 @@ def test_parse_private_key_invalid(): except ValueError as e: assert "Could not parse the private key" in str(e) else: - assert False, "Expected ValueError was not raised" + raise AssertionError("Expected ValueError was not raised") From 11298ae87293778fb4914293bf4b93affdbf9df0 Mon Sep 17 00:00:00 2001 From: Di Mei Date: Mon, 24 Feb 2025 18:10:13 -0500 Subject: [PATCH 4/6] addressing comments --- cdp/{api_key_helpers.py => api_key_utils.py} | 18 ++++++++++ cdp/cdp_api_client.py | 2 +- cdp/wallet.py | 13 +++++-- tests/factories/api_key_factory.py | 33 +++++++++++++++++ tests/test_api_key_helpers.py | 38 -------------------- tests/test_api_key_utils.py | 28 +++++++++++++++ 6 files changed, 90 insertions(+), 42 deletions(-) rename cdp/{api_key_helpers.py => api_key_utils.py} (63%) create mode 100644 tests/factories/api_key_factory.py delete mode 100644 tests/test_api_key_helpers.py create mode 100644 tests/test_api_key_utils.py diff --git a/cdp/api_key_helpers.py b/cdp/api_key_utils.py similarity index 63% rename from cdp/api_key_helpers.py rename to cdp/api_key_utils.py index c7e3d97..e9a8f7c 100644 --- a/cdp/api_key_helpers.py +++ b/cdp/api_key_utils.py @@ -5,6 +5,24 @@ def _parse_private_key(key_str: str): + """Parse a private key from a given string representation. + + Parameters + ---------- + key_str : str + A string representing the private key. This should be either a PEM-encoded + key (for ECDSA keys) or a base64-encoded string (for Ed25519 keys). + + Returns + ------- + An instance of a private key + + Raises + ------ + ValueError + If the key cannot be parsed as a valid PEM-encoded key or a base64-encoded Ed25519 private key. + + """ key_data = key_str.encode() try: return serialization.load_pem_private_key(key_data, password=None) diff --git a/cdp/cdp_api_client.py b/cdp/cdp_api_client.py index 8d63904..02a6a82 100644 --- a/cdp/cdp_api_client.py +++ b/cdp/cdp_api_client.py @@ -7,7 +7,7 @@ from urllib3.util import Retry from cdp import __version__ -from cdp.api_key_helpers import _parse_private_key +from cdp.api_key_utils import _parse_private_key from cdp.client import rest from cdp.client.api_client import ApiClient from cdp.client.api_response import ApiResponse diff --git a/cdp/wallet.py b/cdp/wallet.py index 61a5899..1ceacbd 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -16,7 +16,7 @@ from eth_account import Account from cdp.address import Address -from cdp.api_key_helpers import _parse_private_key +from cdp.api_key_utils import _parse_private_key from cdp.balance_map import BalanceMap from cdp.cdp import Cdp from cdp.client.models.address import Address as AddressModel @@ -687,8 +687,15 @@ def load_seed_from_file(self, file_path: str) -> None: self._master = self._set_master_node() def _encryption_key(self) -> bytes: - """Generate an encryption key based on the private key.""" - # Use Cdp.private_key instead of self.private_key + """Generate an encryption key derived from the configured private key. + + Returns: + bytes: A 32-byte encryption key derived via SHA-256 hashing. + + Raises: + ValueError: If the private key type is not supported for encryption key derivation. + + """ key_obj = _parse_private_key(Cdp.private_key) if isinstance(key_obj, ec.EllipticCurvePrivateKey): diff --git a/tests/factories/api_key_factory.py b/tests/factories/api_key_factory.py new file mode 100644 index 0000000..1d54679 --- /dev/null +++ b/tests/factories/api_key_factory.py @@ -0,0 +1,33 @@ +import base64 + +import pytest + + +@pytest.fixture +def dummy_key_factory(): + """Create and return a factory function for generating dummy keys for testing. + + The factory accepts a `key_type` parameter with the following options: + - "ecdsa": Returns a PEM-encoded ECDSA private key. + - "ed25519-32": Returns a base64-encoded 32-byte Ed25519 private key. + - "ed25519-64": Returns a base64-encoded 64-byte dummy Ed25519 key (the first 32 bytes will be used). + """ + def _create_dummy(key_type: str = "ecdsa") -> str: + if key_type == "ecdsa": + return ( + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIMM75bm9WZCYPkfjXSUWNU5eHx47fWM2IpG8ki90BhRDoAoGCCqGSM49\n" + "AwEHoUQDQgAEicwlaAqy7Z4SS7lvrEYoy6qR9Kf0n0jFzg+XExcXKU1JMr18z47W\n" + "5mrftEqWIqPCLQ16ByoKW2Bsup5V3q9P4g==\n" + "-----END EC PRIVATE KEY-----\n" + ) + elif key_type == "ed25519-32": + return "BXyKC+eFINc/6ztE/3neSaPGgeiU9aDRpaDnAbaA/vyTrUNgtuh/1oX6Vp+OEObV3SLWF+OkF2EQNPtpl0pbfA==" + elif key_type == "ed25519-64": + # Create a 64-byte dummy by concatenating a 32-byte sequence with itself. + dummy_32 = b'\x01' * 32 + dummy_64 = dummy_32 + dummy_32 + return base64.b64encode(dummy_64).decode("utf-8") + else: + raise ValueError("Unsupported key type for dummy key creation") + return _create_dummy diff --git a/tests/test_api_key_helpers.py b/tests/test_api_key_helpers.py deleted file mode 100644 index 4ed1ddc..0000000 --- a/tests/test_api_key_helpers.py +++ /dev/null @@ -1,38 +0,0 @@ -import base64 - -from cryptography.hazmat.primitives.asymmetric import ec, ed25519 - -from cdp.api_key_helpers import _parse_private_key - -DUMMY_ECDSA_PEM = "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMM75bm9WZCYPkfjXSUWNU5eHx47fWM2IpG8ki90BhRDoAoGCCqGSM49\nAwEHoUQDQgAEicwlaAqy7Z4SS7lvrEYoy6qR9Kf0n0jFzg+XExcXKU1JMr18z47W\n5mrftEqWIqPCLQ16ByoKW2Bsup5V3q9P4g==\n-----END EC PRIVATE KEY-----\n" -DUMMY_ED25519_BASE64 = "BXyKC+eFINc/6ztE/3neSaPGgeiU9aDRpaDnAbaA/vyTrUNgtuh/1oX6Vp+OEObV3SLWF+OkF2EQNPtpl0pbfA==" - -def test_parse_private_key_pem_ec(): - """Test that a PEM-encoded ECDSA key is parsed correctly using a hardcoded dummy key.""" - parsed_key = _parse_private_key(DUMMY_ECDSA_PEM) - assert isinstance(parsed_key, ec.EllipticCurvePrivateKey) - - -def test_parse_private_key_ed25519_32(): - """Test that a base64-encoded 32-byte Ed25519 key is parsed correctly using a hardcoded dummy key.""" - parsed_key = _parse_private_key(DUMMY_ED25519_BASE64) - assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) - - -def test_parse_private_key_ed25519_64(): - """Test that a base64-encoded 64-byte input is parsed correctly by taking the first 32 bytes.""" - # Create a 64-byte dummy by concatenating the 32-byte dummy with itself. - dummy_64 = b'\x01' * 32 + b'\x01' * 32 - dummy_64_base64 = base64.b64encode(dummy_64).decode("utf-8") - parsed_key = _parse_private_key(dummy_64_base64) - assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) - - -def test_parse_private_key_invalid(): - """Test that an invalid key string raises a ValueError.""" - try: - _parse_private_key("invalid_key") - except ValueError as e: - assert "Could not parse the private key" in str(e) - else: - raise AssertionError("Expected ValueError was not raised") diff --git a/tests/test_api_key_utils.py b/tests/test_api_key_utils.py new file mode 100644 index 0000000..30f347a --- /dev/null +++ b/tests/test_api_key_utils.py @@ -0,0 +1,28 @@ +import pytest +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 + +from cdp.api_key_utils import _parse_private_key + + +def test_parse_private_key_pem_ec(dummy_key_factory): + """Test that a PEM-encoded ECDSA key is parsed correctly using a dummy key from the factory.""" + dummy_key = dummy_key_factory("ecdsa") + parsed_key = _parse_private_key(dummy_key) + assert isinstance(parsed_key, ec.EllipticCurvePrivateKey) + +def test_parse_private_key_ed25519_32(dummy_key_factory): + """Test that a base64-encoded 32-byte Ed25519 key is parsed correctly using a dummy key from the factory.""" + dummy_key = dummy_key_factory("ed25519-32") + parsed_key = _parse_private_key(dummy_key) + assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + +def test_parse_private_key_ed25519_64(dummy_key_factory): + """Test that a base64-encoded 64-byte input is parsed correctly by taking the first 32 bytes using a dummy key from the factory.""" + dummy_key = dummy_key_factory("ed25519-64") + parsed_key = _parse_private_key(dummy_key) + assert isinstance(parsed_key, ed25519.Ed25519PrivateKey) + +def test_parse_private_key_invalid(): + """Test that an invalid key string raises a ValueError.""" + with pytest.raises(ValueError, match="Could not parse the private key"): + _parse_private_key("invalid_key") From a60b4e197b332b21e70e42dde05c2b08ac36836a Mon Sep 17 00:00:00 2001 From: Di Mei Date: Mon, 24 Feb 2025 18:50:37 -0500 Subject: [PATCH 5/6] addressing comments --- cdp/api_key_utils.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/cdp/api_key_utils.py b/cdp/api_key_utils.py index e9a8f7c..b3c9ff7 100644 --- a/cdp/api_key_utils.py +++ b/cdp/api_key_utils.py @@ -7,20 +7,16 @@ def _parse_private_key(key_str: str): """Parse a private key from a given string representation. - Parameters - ---------- - key_str : str - A string representing the private key. This should be either a PEM-encoded - key (for ECDSA keys) or a base64-encoded string (for Ed25519 keys). + Args: + key_str (str): A string representing the private key. It should be either a PEM-encoded + key (for ECDSA keys) or a base64-encoded string (for Ed25519 keys). - Returns - ------- - An instance of a private key + Returns: + An instance of a private key. Specifically: - Raises - ------ - ValueError - If the key cannot be parsed as a valid PEM-encoded key or a base64-encoded Ed25519 private key. + Raises: + ValueError: If the key cannot be parsed as a valid PEM-encoded key or a base64-encoded + Ed25519 private key. """ key_data = key_str.encode() From b201134e004de44fc858b1cde3adcfe76d2dc16f Mon Sep 17 00:00:00 2001 From: Di Mei Date: Tue, 25 Feb 2025 12:34:31 -0500 Subject: [PATCH 6/6] add an entry to CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf66b68..350d843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## [0.20.0] - 2025-02-25 + +### Added +- Support for Ed25519 API keys. + ## [0.19.0] - 2025-02-21 ### Added