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}