Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
49 changes: 33 additions & 16 deletions src/pythonxbox/common/request_signer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Request Signer
"""Request Signer

Employed for generating the "Signature" header in authentication requests.
"""
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
17 changes: 11 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)


Expand Down
6 changes: 3 additions & 3 deletions tests/data/test_signing_key.pem
Original file line number Diff line number Diff line change
@@ -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-----
12 changes: 7 additions & 5 deletions tests/test_request_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading