270 lines
14 KiB
Python
270 lines
14 KiB
Python
"""Synthetic temporary Git repositories only; no real publication or science."""
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import verify_git_payload as verifier
|
|
|
|
|
|
def encoded(value):
|
|
return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode('utf-8')
|
|
|
|
|
|
def sealed(relative, payload):
|
|
rows = [dict(path=path, bytes=len(data), sha256=verifier.sha(data)) for path, data in sorted(payload.items())]
|
|
manifest = encoded(dict(root=verifier.PROJECT + '/' + relative, files=rows))
|
|
checksums = ''.join(row['sha256'] + ' ' + row['path'] + '\n' for row in rows)
|
|
checksums += verifier.sha(manifest) + ' manifest.json\n'
|
|
controls = {'manifest.json': manifest, 'checksums.sha256': checksums.encode('utf-8')}
|
|
proof = dict(pass_all=True, root=verifier.PROJECT + '/' + relative,
|
|
payload_file_count=len(rows), payload_bytes=sum(row['bytes'] for row in rows),
|
|
manifest_sha256=verifier.sha(manifest), checksums_sha256=verifier.sha(controls['checksums.sha256']),
|
|
excluded_pycache_files=0, verification_scope='synthetic test only')
|
|
return rows, controls, proof
|
|
|
|
|
|
@unittest.skipUnless(shutil.which('git'), 'Git executable is required')
|
|
class GitPayloadTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory(prefix='phase4ak-git-payload-synthetic-')
|
|
self.addCleanup(temporary.cleanup)
|
|
self.repo = Path(temporary.name).resolve()
|
|
self.git('init', '--quiet')
|
|
self.git('config', 'user.name', 'Synthetic Test')
|
|
self.git('config', 'user.email', 'synthetic@example.invalid')
|
|
self.git('config', 'core.autocrlf', 'false')
|
|
self.git('config', 'commit.gpgsign', 'false')
|
|
self.model = {'module.py': b'# synthetic, not a brain\nx = 1\n', 'config.json': b'{"synthetic": true}\n'}
|
|
self.exp = {'audit/final-status.json': encoded(dict(status='FAIL-SEED-VALIDATION-REPRESENTATION')),
|
|
'case/summary.json': b'{"synthetic": true}\n', 'bulk.npy': b'not-a-real-array\x00'}
|
|
self.build()
|
|
|
|
def git(self, *args, data=None):
|
|
env = os.environ.copy()
|
|
for key in ('GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR',
|
|
'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES'):
|
|
env.pop(key, None)
|
|
result = subprocess.run(['git', '-C', str(self.repo), *args], input=data,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, check=False)
|
|
self.assertEqual(result.returncode, 0, result.stderr.decode('utf-8', 'replace'))
|
|
return result.stdout
|
|
|
|
def write(self, path, data):
|
|
target = self.repo / path
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(data)
|
|
|
|
def build(self):
|
|
_, model_controls, model_proof = sealed(verifier.MODEL, self.model)
|
|
exp_rows, exp_controls, exp_proof = sealed(verifier.EXPERIMENT, self.exp)
|
|
for path, data in {**self.model, **model_controls}.items():
|
|
self.write(verifier.MODEL + '/' + path, data)
|
|
registry = {}
|
|
index = []
|
|
omitted = []
|
|
for path, data in {**self.exp, **exp_controls}.items():
|
|
row = dict(path=path, bytes=len(data), sha256=verifier.sha(data))
|
|
if Path(path).suffix in verifier.SUFFIXES and len(data) <= verifier.MAX_EVIDENCE:
|
|
registry['evidence/' + path] = data
|
|
index.append(dict(source=verifier.EXPERIMENT + '/' + path, copy='evidence/' + path,
|
|
bytes=len(data), sha256=verifier.sha(data)))
|
|
else:
|
|
omitted.append(dict(row, reason='bulk_or_non_evidence_extension'))
|
|
for path, data in model_controls.items():
|
|
registry['model/' + path] = data
|
|
index.append(dict(source=verifier.MODEL + '/' + path, copy='model/' + path,
|
|
bytes=len(data), sha256=verifier.sha(data)))
|
|
registry['evidence-index.json'] = encoded(index)
|
|
registry['seal-verification.json'] = encoded(dict(model=model_proof, experiment=exp_proof))
|
|
self.receipt = dict(publication_complete=True, registry=verifier.REGISTRY, project_root=verifier.PROJECT,
|
|
model_payload_remains_at=verifier.MODEL, bulk_remains_at=verifier.EXPERIMENT,
|
|
model_manifest_sha256=model_proof['manifest_sha256'],
|
|
experiment_manifest_sha256=exp_proof['manifest_sha256'],
|
|
copied_evidence_files=len(index), copied_evidence_bytes=sum(row['bytes'] for row in index),
|
|
files=[dict(path=path, bytes=len(data), sha256=verifier.sha(data)) for path, data in sorted(registry.items())],
|
|
experiment_payload_not_copied=omitted, declared_audit_status='FAIL-SEED-VALIDATION-REPRESENTATION')
|
|
for path, data in registry.items():
|
|
self.write(verifier.REGISTRY + '/' + path, data)
|
|
self.receipt_write()
|
|
self.git('add', '--', verifier.MODEL, verifier.REGISTRY)
|
|
|
|
def receipt_write(self):
|
|
data = encoded(self.receipt)
|
|
self.write(verifier.REGISTRY + '/publication-receipt.json', data)
|
|
self.anchor = verifier.sha(data)
|
|
|
|
def anchored_registry_change(self, relative, data):
|
|
self.write(verifier.REGISTRY + '/' + relative, data)
|
|
row = next(row for row in self.receipt['files'] if row['path'] == relative)
|
|
row.update(bytes=len(data), sha256=verifier.sha(data))
|
|
self.receipt_write()
|
|
self.git('add', '--', verifier.REGISTRY)
|
|
|
|
def verify(self, **kwargs):
|
|
return verifier.verify_git_payload(self.repo, self.anchor, **({'staged': True} if not kwargs else kwargs))
|
|
|
|
def test_staged_verifies_blob_bytes_and_leaves_index_unchanged(self):
|
|
before = (self.repo / '.git/index').read_bytes()
|
|
result = self.verify()
|
|
self.assertTrue(result['pass_all'])
|
|
self.assertEqual(result['model_payload_files'], 2)
|
|
self.assertEqual(result['declared_audit_status'], 'FAIL-SEED-VALIDATION-REPRESENTATION')
|
|
self.assertFalse(result['experiment_bulk_verified_by_this_tool'])
|
|
self.assertFalse(result['scientific_gate_executed'])
|
|
self.assertTrue(result['publication_integrity_is_not_scientific_pass'])
|
|
self.assertEqual(before, (self.repo / '.git/index').read_bytes())
|
|
self.assertFalse((self.repo / verifier.EXPERIMENT).exists())
|
|
|
|
def test_dirty_or_missing_worktree_payload_does_not_replace_good_index(self):
|
|
(self.repo / verifier.MODEL / 'module.py').write_bytes(b'working copy differs')
|
|
(self.repo / verifier.REGISTRY / 'publication-receipt.json').unlink()
|
|
self.assertTrue(self.verify()['pass_all'])
|
|
|
|
def test_correct_worktree_cannot_hide_corrupt_staged_blob(self):
|
|
relative = verifier.MODEL + '/module.py'
|
|
self.write(relative, self.model['module.py'].replace(b'x = 1', b'x = 2'))
|
|
self.git('add', '--', relative)
|
|
self.write(relative, self.model['module.py'])
|
|
with self.assertRaisesRegex(ValueError, 'Git blob SHA-256 mismatch'):
|
|
self.verify()
|
|
|
|
def test_commit_verifies_immutable_commit_not_current_index(self):
|
|
self.git('commit', '--quiet', '-m', 'synthetic only')
|
|
commit = self.git('rev-parse', 'HEAD').decode().strip()
|
|
self.write(verifier.MODEL + '/module.py', b'bad newer index')
|
|
self.git('add', '--', verifier.MODEL)
|
|
result = self.verify(commit='HEAD')
|
|
self.assertEqual(result['commit'], commit)
|
|
self.assertEqual(result['source'], 'commit')
|
|
self.assertTrue(result['pass_all'])
|
|
|
|
def test_external_receipt_anchor_is_mandatory_and_checked(self):
|
|
for anchor in (None, '', '0' * 64, self.anchor.upper()):
|
|
with self.subTest(anchor=anchor), self.assertRaises(ValueError):
|
|
verifier.verify_git_payload(self.repo, anchor, staged=True)
|
|
|
|
def test_extra_model_file_is_rejected_but_outside_scope_is_ignored(self):
|
|
self.write('unrelated/old-file.txt', b'outside audit scope')
|
|
self.git('add', '--', 'unrelated')
|
|
self.assertTrue(self.verify()['pass_all'])
|
|
self.write(verifier.MODEL + '/extra.py', b'extra')
|
|
self.git('add', '--', verifier.MODEL)
|
|
with self.assertRaisesRegex(ValueError, 'Model Git inventory'):
|
|
self.verify()
|
|
|
|
def test_extra_registry_file_is_rejected(self):
|
|
self.write(verifier.REGISTRY + '/unlisted.json', b'{}')
|
|
self.git('add', '--', verifier.REGISTRY)
|
|
with self.assertRaisesRegex(ValueError, 'Registry Git inventory'):
|
|
self.verify()
|
|
|
|
def test_missing_staged_model_file_is_rejected(self):
|
|
self.git('update-index', '--force-remove', verifier.MODEL + '/module.py')
|
|
with self.assertRaisesRegex(ValueError, 'Model Git inventory'):
|
|
self.verify()
|
|
|
|
def test_symlink_blob_mode_is_rejected_without_following_it(self):
|
|
path = verifier.MODEL + '/module.py'
|
|
oid = self.git('rev-parse', ':' + path).decode().strip()
|
|
self.git('update-index', '--cacheinfo', '120000,' + oid + ',' + path)
|
|
with self.assertRaisesRegex(ValueError, 'Symlink/gitlink/nonregular'):
|
|
self.verify()
|
|
|
|
def test_gitlink_is_rejected(self):
|
|
self.git('commit', '--quiet', '-m', 'synthetic only')
|
|
oid = self.git('rev-parse', 'HEAD').decode().strip()
|
|
self.git('update-index', '--add', '--cacheinfo', '160000,' + oid + ',' + verifier.MODEL + '/submodule')
|
|
with self.assertRaisesRegex(ValueError, 'Symlink/gitlink/nonregular'):
|
|
self.verify()
|
|
|
|
def test_unmerged_index_is_rejected(self):
|
|
path = verifier.MODEL + '/module.py'
|
|
oid = self.git('rev-parse', ':' + path).decode().strip()
|
|
self.git('update-index', '--force-remove', path)
|
|
self.git('update-index', '--index-info', data=('100644 ' + oid + ' 1\t' + path + '\n').encode())
|
|
with self.assertRaisesRegex(ValueError, 'Unmerged index'):
|
|
self.verify()
|
|
|
|
def test_anchored_index_source_escape_is_rejected(self):
|
|
data = json.loads((self.repo / verifier.REGISTRY / 'evidence-index.json').read_bytes())
|
|
data[0]['source'] = 'experiments/another-phase/audit/final-status.json'
|
|
self.anchored_registry_change('evidence-index.json', encoded(data))
|
|
with self.assertRaisesRegex(ValueError, 'Evidence index differs'):
|
|
self.verify()
|
|
|
|
def test_anchored_registry_copy_cannot_disagree_with_model(self):
|
|
data = (self.repo / verifier.REGISTRY / 'model/checksums.sha256').read_bytes()
|
|
self.anchored_registry_change('model/checksums.sha256', b'0' * 64 + data[64:])
|
|
with self.assertRaisesRegex(ValueError, 'Checksum inventory'):
|
|
self.verify()
|
|
|
|
def test_anchored_receipt_cannot_change_model_manifest_anchor(self):
|
|
self.receipt['model_manifest_sha256'] = '0' * 64
|
|
self.receipt_write(); self.git('add', '--', verifier.REGISTRY)
|
|
with self.assertRaisesRegex(ValueError, 'Receipt-bound manifest'):
|
|
self.verify()
|
|
|
|
def test_registry_payload_corruption_is_rejected(self):
|
|
path = verifier.REGISTRY + '/evidence/case/summary.json'
|
|
self.write(path, b'{"synthetic":false}\n')
|
|
self.git('add', '--', path)
|
|
with self.assertRaisesRegex(ValueError, 'Git blob (byte count|SHA-256) mismatch'):
|
|
self.verify()
|
|
|
|
def test_anchored_receipt_path_escape_and_duplicate_fail(self):
|
|
self.receipt['files'][0]['path'] = '../outside.json'
|
|
self.receipt_write(); self.git('add', '--', verifier.REGISTRY)
|
|
with self.assertRaisesRegex(ValueError, 'Noncanonical or escaping path'):
|
|
self.verify()
|
|
|
|
def test_duplicate_json_key_fails_even_with_external_anchor(self):
|
|
data = encoded(self.receipt).replace(b'{\n', b'{\n "publication_complete": true,\n', 1)
|
|
self.write(verifier.REGISTRY + '/publication-receipt.json', data)
|
|
self.anchor = verifier.sha(data)
|
|
self.git('add', '--', verifier.REGISTRY)
|
|
with self.assertRaisesRegex(ValueError, 'Duplicate JSON key'):
|
|
self.verify()
|
|
|
|
def test_incomplete_status_is_preserved_not_regraded(self):
|
|
self.receipt['declared_audit_status'] = 'INCOMPLETE-VALIDATION-EXECUTION'
|
|
self.receipt_write(); self.git('add', '--', verifier.REGISTRY)
|
|
result = self.verify()
|
|
self.assertEqual(result['declared_audit_status'], 'INCOMPLETE-VALIDATION-EXECUTION')
|
|
self.assertTrue(result['publication_integrity_is_not_scientific_pass'])
|
|
|
|
def test_anchored_bulk_omission_inventory_mismatch_fails(self):
|
|
self.receipt['experiment_payload_not_copied'] = []
|
|
self.receipt_write(); self.git('add', '--', verifier.REGISTRY)
|
|
with self.assertRaisesRegex(ValueError, 'omitted-bulk inventory mismatch'):
|
|
self.verify()
|
|
|
|
def test_anchored_seal_proof_contradiction_fails(self):
|
|
proof = json.loads((self.repo / verifier.REGISTRY / 'seal-verification.json').read_bytes())
|
|
proof['model']['payload_file_count'] += 1
|
|
self.anchored_registry_change('seal-verification.json', encoded(proof))
|
|
with self.assertRaisesRegex(ValueError, 'Seal-verification evidence contradicts'):
|
|
self.verify()
|
|
|
|
def test_both_or_neither_git_sources_fail(self):
|
|
for kwargs in ({}, {'staged': True, 'commit': 'HEAD'}):
|
|
with self.subTest(kwargs=kwargs), self.assertRaisesRegex(ValueError, 'Select exactly one'):
|
|
verifier.verify_git_payload(self.repo, self.anchor, **kwargs)
|
|
|
|
def test_cli_success_and_failure_are_json_with_exit_status(self):
|
|
for anchor, expected in ((self.anchor, 0), ('0' * 64, 1)):
|
|
with self.subTest(expected=expected), contextlib.redirect_stdout(io.StringIO()) as output:
|
|
code = verifier.main(['--repo', str(self.repo), '--staged', '--receipt-sha256', anchor])
|
|
result = json.loads(output.getvalue())
|
|
self.assertEqual(code, expected)
|
|
self.assertEqual(result['pass_all'], expected == 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|