Files

123 lines
4.3 KiB
Python

"""End-to-end tests for raycast_api.signing.Signer.
Hand-computed canonical/HMAC values for synthetic specs prove the spec
fields actually drive behaviour (not just defaults).
"""
from __future__ import annotations
import hashlib
import hmac as _hmac
import pytest
from raycast_api.signing import Signer
from raycast_api.signing_spec import RotRange, SigningSpec
RAYCAST_SPEC = SigningSpec(
rot_fn_name="Sur",
signing_fn_name="Nkt",
rot_ranges=[
RotRange(0x41, 0x5A, 13),
RotRange(0x61, 0x7A, 13),
RotRange(0x30, 0x39, 5),
],
)
FAKE_SECRET = "DEAD" + "BEEF" * 15
TS = "1778858809"
DEVICE = "20eca913cada74f879e6535304f9d44da380c28eb855065c0d71017a3d7c3099"
class TestCanonicalString:
"""Canonical string composition matches rot(ts) + join + rot(device) + join + rot(sha256(body))."""
def test_canonical_string_shape(self) -> None:
body = b"hello"
signer = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
canonical = signer.canonical_string(TS, DEVICE, body)
body_hex = hashlib.sha256(body).hexdigest()
from raycast_api.signing.transforms import apply_rot
expected = ".".join(
apply_rot(p, RAYCAST_SPEC.rot_ranges) for p in (TS, DEVICE, body_hex)
)
assert canonical == expected
class TestSpecDriven:
"""Changing a spec field changes the signature — proves nothing is hardcoded."""
def test_join_char_change_changes_signature(self) -> None:
a = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
b_spec = SigningSpec(
rot_fn_name="Sur",
signing_fn_name="Nkt",
rot_ranges=RAYCAST_SPEC.rot_ranges,
join_char=":",
)
b = Signer(spec=b_spec, secret=FAKE_SECRET)
assert a.sign(timestamp=TS, device_id=DEVICE, body=b"x") != b.sign(
timestamp=TS, device_id=DEVICE, body=b"x"
)
def test_rot_ranges_change_changes_signature(self) -> None:
a = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
b_spec = SigningSpec(
rot_fn_name="Sur",
signing_fn_name="Nkt",
rot_ranges=[
RotRange(0x41, 0x5A, 1),
RotRange(0x61, 0x7A, 1),
RotRange(0x30, 0x39, 1),
],
)
b = Signer(spec=b_spec, secret=FAKE_SECRET)
assert a.sign(timestamp=TS, device_id=DEVICE, body=b"x") != b.sign(
timestamp=TS, device_id=DEVICE, body=b"x"
)
def test_secret_change_changes_signature(self) -> None:
a = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
b = Signer(spec=RAYCAST_SPEC, secret="CAFE" + "BABE" * 15)
assert a.sign(timestamp=TS, device_id=DEVICE, body=b"x") != b.sign(
timestamp=TS, device_id=DEVICE, body=b"x"
)
def test_body_bytes_change_changes_signature(self) -> None:
signer = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
sig_a = signer.sign(timestamp=TS, device_id=DEVICE, body=b'{"k":1}')
sig_b = signer.sign(timestamp=TS, device_id=DEVICE, body=b'{"k":2}')
assert sig_a != sig_b
def test_timestamp_string_used_verbatim(self) -> None:
signer = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
a = signer.sign(timestamp="1778858809", device_id=DEVICE, body=b"x")
b = signer.sign(timestamp="01778858809", device_id=DEVICE, body=b"x")
assert a != b
class TestConstruction:
def test_empty_rot_ranges_rejected(self) -> None:
bad = SigningSpec(rot_fn_name="x", signing_fn_name="y", rot_ranges=[])
with pytest.raises(ValueError):
Signer(spec=bad, secret=FAKE_SECRET)
def test_signer_is_reusable_for_concurrent_messages(self) -> None:
signer = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
outputs = {
signer.sign(timestamp=TS, device_id=DEVICE, body=f"{i}".encode())
for i in range(8)
}
assert len(outputs) == 8
def test_hmac_uses_utf8_encoded_secret_not_hex_decoded(self) -> None:
signer = Signer(spec=RAYCAST_SPEC, secret=FAKE_SECRET)
body = b'{"buffer_id":"x"}'
canonical = signer.canonical_string(TS, DEVICE, body)
expected = _hmac.new(
FAKE_SECRET.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256
).hexdigest()
assert signer.sign(timestamp=TS, device_id=DEVICE, body=body) == expected