railiance-clock/tests/test_protocol.py
tegwick 3e537d117f Cover local acceptance gaps and reject nonfinite JSON exponents
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e6f0-a6f6-7b93-9124-55ef584930e2
2026-09-28 10:40:02 +02:00

83 lines
3.3 KiB
Python

import json
import jwt
import pytest
from railiance_clock.core import ClockError, MAX_NS
from railiance_clock.protocol import TYPE, decode64, encode64, sign, strict_json, verify
def payload(setup):
_, key, _, transport = setup
from railiance_clock.protocol import PROFILE
request = {'profile': PROFILE, 'authority_id': 'railiance01', 'environment': 'test',
'nonce': encode64(bytes(range(32)))}
return verify(transport(request), key.public_key(), 'test-key')
def signed_unchecked(value, key, header=None):
return jwt.encode(value, key, algorithm='ES256',
headers=header or {'kid': 'test-key', 'typ': TYPE}).encode()
@pytest.mark.parametrize('field,value', [
('profile', 'railiance-clock-v9'), ('quality', 'unknown'), ('unexpected', 1),
*[(field, bad) for field in ('receive_unix_ns', 'send_unix_ns', 'error_ns', 'max_age_ns')
for bad in (str(MAX_NS + 1), '-1', '01', '1.0', '1e3', '1ms', '1\n', 1, True, None)],
('max_age_ns', '0'), ('nonce', encode64(b'short')),
])
def test_signed_invalid_sample_refused(setup, field, value):
_, key, _, _ = setup
value_dict = payload(setup); value_dict[field] = value
# Bypass our sign() validator: this is an authentic but invalid authority statement.
raw = signed_unchecked(value_dict, key)
with pytest.raises(ClockError):
verify(raw, key.public_key(), 'test-key')
@pytest.mark.parametrize('header', [
{'kid': 'test-key', 'typ': 'wrong'}, {'kid': 'other-key', 'typ': TYPE},
{'kid': 'test-key', 'typ': TYPE, 'jku': 'https://untrusted.invalid/key'},
{'kid': 'test-key', 'typ': TYPE, 'crit': ['unknown']},
])
def test_authentic_unadmitted_header_refused(setup, header):
_, key, _, _ = setup
with pytest.raises(ClockError):
verify(signed_unchecked(payload(setup), key, header), key.public_key(), 'test-key')
@pytest.mark.parametrize('header', [b'null', b'[]', b'{}', b'{',
b'{"alg":"ES256","alg":"ES256","kid":"test-key","typ":"railiance-clock-sample-v0.1"}',
b'{"alg":"none","kid":"test-key","typ":"railiance-clock-sample-v0.1"}'])
def test_malformed_header_refused(setup, header):
_, key, _, _ = setup
raw = sign(payload(setup), key, 'test-key').split(b'.')
raw[0] = encode64(header).encode()
with pytest.raises(ClockError):
verify(b'.'.join(raw), key.public_key(), 'test-key')
def test_modified_uncertainty_invalidates_signature(setup):
_, key, _, _ = setup
parts = sign(payload(setup), key, 'test-key').split(b'.')
changed = json.loads(decode64(parts[1].decode())); changed['error_ns'] = '0'
parts[1] = encode64(json.dumps(changed).encode()).encode()
with pytest.raises(ClockError):
verify(b'.'.join(parts), key.public_key(), 'test-key')
@pytest.mark.parametrize('raw', [b'{"a":1e999}', b'{"a":[-1e999]}', b'{"a":Infinity}'])
def test_nonfinite_exponent_refused(raw):
with pytest.raises(ClockError):
strict_json(raw)
@pytest.mark.parametrize('raw', [b'x' * 8193, b'\xff.abc.def', b'a.b', b'a.b.c.d'])
def test_invalid_envelope_refused(setup, raw):
_, key, _, _ = setup
with pytest.raises(ClockError):
verify(raw, key.public_key(), 'test-key')
def test_finite_json_numbers_remain_supported():
assert strict_json(b'{"a":1.25,"b":1e100}') == {'a': 1.25, 'b': 1e100}