diff --git a/pyproject.toml b/pyproject.toml index a18064f..7d5fc15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ authors = [ {name = "Manfred Dennerlein Rodelo", email = "manfred@dennerlein.name"}, ] dependencies = [ - "ecdsa>=0.19", + "cryptography>=42.0", "httpx>=0.25.1", "ms_cv>=0.1.1", "pydantic>=2.12", @@ -84,7 +84,7 @@ packages = ["src/pythonxbox"] python = "3.13" dependencies = [ "platformdirs==4.9.2", - "ecdsa==0.19.1", + "cryptography==46.0.5", "httpx==0.28.1", "ms_cv==0.1.1", "pydantic==2.12.5", diff --git a/src/pythonxbox/common/request_signer.py b/src/pythonxbox/common/request_signer.py index bbd8b17..3ca14da 100644 --- a/src/pythonxbox/common/request_signer.py +++ b/src/pythonxbox/common/request_signer.py @@ -1,5 +1,4 @@ -""" -Request Signer +"""Request Signer Employed for generating the "Signature" header in authentication requests. """ @@ -9,7 +8,8 @@ import hashlib import struct -from ecdsa import NIST256p, SigningKey, VerifyingKey +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, utils from pythonxbox.authentication.models import SignaturePolicy from pythonxbox.common import filetimes @@ -22,31 +22,39 @@ class RequestSigner: def __init__( self, - signing_key: SigningKey | None = None, + signing_key: ec.EllipticCurvePrivateKey | None = None, signing_policy: SignaturePolicy | None = None, ) -> None: - self.signing_key = signing_key or SigningKey.generate(curve=NIST256p) + self.signing_key = signing_key or ec.generate_private_key(ec.SECP256R1()) self.signing_policy = signing_policy or DEFAULT_SIGNING_POLICY - pk_point = self.signing_key.verifying_key.pubkey.point + pub_nums = self.signing_key.public_key().public_numbers() self.proof_field = { "use": "sig", "alg": self.signing_policy.supported_algorithms[0], "kty": "EC", "crv": "P-256", - "x": self.__encode_ec_coord(pk_point.x()), - "y": self.__encode_ec_coord(pk_point.y()), + "x": self.__encode_ec_coord(pub_nums.x), + "y": self.__encode_ec_coord(pub_nums.y), } def export_signing_key(self) -> str: - return self.signing_key.to_pem().decode() + return self.signing_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + ).decode() @staticmethod - def import_signing_key(signing_key: str) -> SigningKey: - return SigningKey.from_pem(signing_key) + def import_signing_key(signing_key: str) -> ec.EllipticCurvePrivateKey: + key = serialization.load_pem_private_key(signing_key.encode(), password=None) + if not isinstance(key, ec.EllipticCurvePrivateKey): + msg = "Expected an EC private key" + raise TypeError(msg) + return key @classmethod - def from_pem(cls, pem_string: str) -> SigningKey: + def from_pem(cls, pem_string: str) -> "RequestSigner": request_signer = RequestSigner.import_signing_key(pem_string) return cls(request_signer) @@ -79,7 +87,7 @@ def verify_digest( self, signature: bytes, digest: bytes, - verifying_key: VerifyingKey | None = None, + verifying_key: ec.EllipticCurvePublicKey | None = None, ) -> bool: """ Verify signature against digest @@ -91,8 +99,12 @@ def verify_digest( Returns: True on successful verification, False otherwise """ - verifier = verifying_key or self.signing_key.verifying_key - return verifier.verify_digest(signature, digest) + verifier = verifying_key or self.signing_key.public_key() + r = int.from_bytes(signature[:32], "big") + s = int.from_bytes(signature[32:], "big") + der_sig = utils.encode_dss_signature(r, s) + verifier.verify(der_sig, digest, ec.ECDSA(utils.Prehashed(hashes.SHA256()))) + return True def sign( self, @@ -139,7 +151,12 @@ def _sign_raw( digest = self._hash(data) # Sign the hash - signature = self.signing_key.sign_digest_deterministic(digest) + der_sig = self.signing_key.sign( + digest, + ec.ECDSA(utils.Prehashed(hashes.SHA256()), deterministic_signing=True), + ) + r, s = utils.decode_dss_signature(der_sig) + signature = r.to_bytes(32, "big") + s.to_bytes(32, "big") # Return signature version + timestamp encoded + signature return signature_version_bytes + ts_bytes + signature diff --git a/tests/conftest.py b/tests/conftest.py index 567f16c..1ce5c7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,8 @@ from datetime import UTC, datetime import uuid -from ecdsa.keys import SigningKey, VerifyingKey +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import load_pem_private_key import pytest import pytest_asyncio @@ -61,17 +62,21 @@ def ecdsa_signing_key_str() -> str: @pytest.fixture(scope="session") -def ecdsa_signing_key(ecdsa_signing_key_str: str) -> SigningKey: - return SigningKey.from_pem(ecdsa_signing_key_str) +def ecdsa_signing_key(ecdsa_signing_key_str: str) -> ec.EllipticCurvePrivateKey: + return load_pem_private_key(ecdsa_signing_key_str.encode(), password=None) @pytest.fixture(scope="session") -def ecdsa_verifying_key(ecdsa_signing_key: SigningKey) -> VerifyingKey: - return ecdsa_signing_key.get_verifying_key() +def ecdsa_verifying_key( + ecdsa_signing_key: ec.EllipticCurvePrivateKey, +) -> ec.EllipticCurvePublicKey: + return ecdsa_signing_key.public_key() @pytest.fixture(scope="session") -def synthetic_request_signer(ecdsa_signing_key: VerifyingKey) -> RequestSigner: +def synthetic_request_signer( + ecdsa_signing_key: ec.EllipticCurvePrivateKey, +) -> RequestSigner: return RequestSigner(ecdsa_signing_key) diff --git a/tests/data/test_signing_key.pem b/tests/data/test_signing_key.pem index ed1afee..af7e862 100644 --- a/tests/data/test_signing_key.pem +++ b/tests/data/test_signing_key.pem @@ -1,5 +1,5 @@ -----BEGIN EC PRIVATE KEY----- -MHcCAQEEIObr5IVtB+DQcn25+R9n4K/EyUUSbVvxIJY7WhVeELUuoAoGCCqGSM49AwEHoUQDQgAE -OKyCQ9qH5U4lZcS0c5/LxIyKvOpKe0l3x4Eg5OgDbzezKNLRgT28fd4Fq3rU/1OQKmx6jSq0vTB5 -Ao/48m0iGg== +MHcCAQEEIObr5IVtB+DQcn25+R9n4K/EyUUSbVvxIJY7WhVeELUuoAoGCCqGSM49 +AwEHoUQDQgAEOKyCQ9qH5U4lZcS0c5/LxIyKvOpKe0l3x4Eg5OgDbzezKNLRgT28 +fd4Fq3rU/1OQKmx6jSq0vTB5Ao/48m0iGg== -----END EC PRIVATE KEY----- diff --git a/tests/test_request_signer.py b/tests/test_request_signer.py index effac60..d4e4596 100644 --- a/tests/test_request_signer.py +++ b/tests/test_request_signer.py @@ -2,7 +2,8 @@ from binascii import unhexlify from datetime import datetime -from ecdsa.keys import BadSignatureError, VerifyingKey +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric import ec import pytest from pythonxbox.common.request_signer import RequestSigner @@ -77,25 +78,26 @@ def test_synthetic_signature( assert ( test_signature - == "AAAAAQHWE40Q98yAFe3R7GuZfvGA350cH7hWgg4HIHjaD9lGYiwxki6bNyGnB8dMEIfEmBiuNuGUfWjY5lL2h44X/VMGOkPIezVb7Q==" + == "AAAAAQHWE40Q98yA7m+8q9G9JwEg9aIyK7yadd3P1nUG10lTF/FcV+y87bUVOUJoihhLSTcJCg4UvOR/aDj46lb6Le82PrsSXHoMOw==" ) def test_synthetic_verify_digest( - synthetic_request_signer: RequestSigner, ecdsa_verifying_key: VerifyingKey + synthetic_request_signer: RequestSigner, + ecdsa_verifying_key: ec.EllipticCurvePublicKey, ) -> None: message = unhexlify( "f7d61b6f8d4dcd86da1aa8553f0ee7c15450811e7cd2759364e22f67d853ff50" ) signature = base64.b64decode( - "Fe3R7GuZfvGA350cH7hWgg4HIHjaD9lGYiwxki6bNyGnB8dMEIfEmBiuNuGUfWjY5lL2h44X/VMGOkPIezVb7Q==" + "7m+8q9G9JwEg9aIyK7yadd3P1nUG10lTF/FcV+y87bUVOUJoihhLSTcJCg4UvOR/aDj46lb6Le82PrsSXHoMOw==" ) invalid_signature = b"\xff" + bytes(signature)[1:] success = synthetic_request_signer.verify_digest(signature, message) success_via_vk = synthetic_request_signer.verify_digest( signature, message, ecdsa_verifying_key ) - with pytest.raises(BadSignatureError): + with pytest.raises(InvalidSignature): synthetic_request_signer.verify_digest(invalid_signature, message) assert success is True