lazyBoy/tests/file-retirement-proof.test.py

105 lines
6.3 KiB
Python
Raw Permalink Normal View History

2026-09-11 02:47:43 +00:00
"""Retirement claims are authenticated, bounded, current-scope capabilities."""
import hashlib
import hmac
import json
import sqlite3
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch
class FileRetirementProof(unittest.TestCase):
def setUp(self):
source = Path(__file__).resolve().parents[1] / 'crates/supervisor/src/runner_files.py'
self.module = {}
exec(compile(source.read_text(), str(source), 'exec'), self.module)
self.key = 'ab' * 32
self.current = {'computer_id': 'new', 'bot_id': 'a', 'generation': 2}
self.proof = {'version': 1, 'current': self.current,
'origin': {'computer_id': 'old', 'bot_id': 'a', 'generation': 1},
'home_key': 'home', 'operation_id': 'operation', 'proof_id': 'ack-id',
'proof_kind': 'provider_destroy_ack', 'origin_provider_ref': 'old',
'issued_at': 1000, 'expires_at': 1060}
def sign(self, proof):
raw = proof if isinstance(proof, str) else json.dumps(proof, separators=(',', ':'))
return {'payload': raw, 'signature': hmac.new(bytes.fromhex(self.key), raw.encode(), hashlib.sha256).hexdigest()}
def verify(self, envelope, **changes):
with patch.object(self.module['time'], 'time', return_value=1001):
return self.module['file_retirement_proof'](envelope, changes.get('identity', self.current),
changes.get('home', 'home'), changes.get('key', self.key))
def test_valid_proof_and_signature_scope(self):
signed = self.sign(self.proof)
self.assertEqual(self.verify(signed), self.proof)
for changes in [{'key': 'cd' * 32}, {'home': 'other'},
{'identity': {**self.current, 'bot_id': 'b'}},
{'identity': {**self.current, 'computer_id': 'replacement'}},
{'identity': {**self.current, 'generation': 3}}]:
with self.assertRaisesRegex(ValueError, '^INVALID_FILE_RETIREMENT_PROOF$'):
self.verify(signed, **changes)
with self.assertRaisesRegex(ValueError, '^INVALID_FILE_RETIREMENT_PROOF$'):
self.verify({**signed, 'payload': signed['payload'].replace('operation', 'tampered')})
def test_signed_invalid_claims_still_fail_closed(self):
for change in [{'version': True}, {'version': 2}, {'extra': True}, {'proof_kind': 'changed_namespace'},
{'origin': {**self.proof['origin'], 'bot_id': 'b'}},
{'origin': {**self.proof['origin'], 'generation': 2}},
{'origin': {**self.proof['origin'], 'generation': True}},
{'issued_at': True}, {'issued_at': 1007}, {'issued_at': -1},
{'expires_at': 1001}, {'expires_at': 1061}, {'expires_at': 1000},
{'operation_id': '../file'}, {'proof_id': ''}, {'origin_provider_ref': ''}]:
with self.subTest(change=change):
with self.assertRaisesRegex(ValueError, '^INVALID_FILE_RETIREMENT_PROOF$'):
self.verify(self.sign({**self.proof, **change}))
def test_duplicate_fields_and_oversized_or_untrusted_envelopes_fail(self):
raw = json.dumps(self.proof)
invalid = [self.sign(raw[:-1] + ',"version":1}'), self.sign(' ' * 4097),
self.sign('[]'), self.sign('{broken'), {'payload': raw},
{**self.sign(self.proof), 'key': self.key},
{**self.sign(self.proof), 'signature': '0' * 64}]
for envelope in invalid:
with self.assertRaisesRegex(ValueError, '^INVALID_FILE_RETIREMENT_PROOF$'):
self.verify(envelope)
def test_retirement_audit_failure_rolls_back_receipt_and_outbox(self):
# Real SQLite transaction, with the already-verified filesystem
# boundary replaced by a fixture to isolate late audit failure.
proof = self.verify(self.sign(self.proof))
origin = proof['origin']
encoded = self.module['file_identity'](origin)
key = hashlib.sha256((encoded + ':operation').encode()).hexdigest()
data = b'data'
digest = hashlib.sha256(data).hexdigest()
observation = {'source': {'path': 'file', 'sha256': digest, 'size': 4},
'helper_quiescent': True, 'controller_quiescent': True, 'desired_state_matches': True}
self.module['file_readback'] = lambda *args, **kwargs: observation
self.module['file_snapshot_store'] = lambda content, sha: sha
request = {'action': 'file_status', 'identity': origin,
'input': json.dumps({'operation_id': 'operation', 'readback': True, 'resolve': True})}
with sqlite3.connect(':memory:') as db:
self.module['file_journal_schema'](db)
db.execute('INSERT INTO file_operations VALUES (?,?,?,?,?,?,?)',
(key, encoded, 'digest', 'unknown', None, json.dumps({'action': 'write', 'path': 'file'}), 0))
self.module['file_event'](db, encoded, 'operation', 'accepted')
db.commit()
db.execute("CREATE TRIGGER reject_audit BEFORE INSERT ON file_retirement_receipts BEGIN SELECT RAISE(ABORT,'audit unavailable'); END")
with patch.object(self.module['subprocess'], 'run', return_value=SimpleNamespace(returncode=0, stdout=data)):
with self.assertRaisesRegex(sqlite3.IntegrityError, 'audit unavailable'):
self.module['file_journal_handle'](db, request, '', lambda *_: False, retirement=proof)
self.assertEqual(db.execute('SELECT status,result FROM file_operations').fetchone(), ('unknown', None))
self.assertEqual(db.execute('SELECT count(*) FROM file_retirement_receipts').fetchone()[0], 0)
self.assertEqual(db.execute('SELECT count(*) FROM file_outbox').fetchone()[0], 1)
db.execute('DROP TRIGGER reject_audit')
result = self.module['file_journal_handle'](db, request, '', lambda *_: False, retirement=proof)
self.assertEqual(result['status'], 'succeeded')
self.assertEqual(db.execute('SELECT count(*) FROM file_retirement_receipts').fetchone()[0], 1)
self.assertEqual(db.execute('SELECT count(*) FROM file_outbox').fetchone()[0], 2)
if __name__ == '__main__':
unittest.main()