swayfx-mosaic/tooling/probes/mosaic-first-session/repair.py

118 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Stage the one audited Conflicts decoding repair; never install or launch."""
import argparse
import difflib
import json
import os
from pathlib import Path
import stat
from prepare import digest, json_bytes, write_private
PREIMAGE_SHA = '556b490587af166730db686d973a63e586824b87b488e7364471228515848522'
OLD = """ conflicts = run(['/usr/bin/systemctl', '--user', 'show', SESSION_UNIT,
'-p', 'Conflicts', '--value']).split()
"""
NEW = """ # systemctl quotes unit names containing literal backslashes. Decode its
# quoted word list without shell execution; preserve the unit's \\x escapes.
conflicts = shlex.split(run([
'/usr/bin/systemctl', '--user', 'show', SESSION_UNIT,
'-p', 'Conflicts', '--value']))
"""
def corrected(data):
"""Refuse every source except the exact audited preimage."""
if digest(data) != PREIMAGE_SHA:
raise RuntimeError('Audited guard preimage changed')
source = data.decode()
if source.count(OLD) != 1 or source.count('import stat\n') != 1:
raise RuntimeError('Audited repair anchors changed')
return source.replace('import stat\n', 'import shlex\nimport stat\n').replace(
OLD, NEW).encode()
def stage(guard_path, lab):
"""Create an exclusive repair directory in an existing private LAB.
The installed guard authenticates all original inputs. Only the session.py
digest changes in the manifest; no installation function is provided.
"""
guard_path, lab = Path(guard_path).absolute(), Path(lab).absolute()
# Authenticate bytes before importing trusted Python; do not execute an
# arbitrary replacement supplied via the CLI.
source_fd = os.open(guard_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
try:
source_info = os.fstat(source_fd)
if (not stat.S_ISREG(source_info.st_mode) or source_info.st_nlink != 1
or source_info.st_uid != os.getuid()
or source_info.st_mode & 0o022 or source_info.st_size > 131072):
raise RuntimeError('Unsafe guard preimage')
with os.fdopen(source_fd, 'rb', closefd=False) as stream:
authenticated = stream.read(131073)
finally:
os.close(source_fd)
corrected(authenticated)
# Compile the authenticated bytes rather than reopening a mutable source.
import types
guard = types.ModuleType('authenticated_installed_guard')
exec(compile(authenticated, str(guard_path), 'exec'), guard.__dict__)
fd = guard.secure_directory(lab)
info = os.fstat(fd)
os.close(fd)
if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700:
raise RuntimeError('Existing LAB must be owned and mode0700')
original = guard.read_regular(guard_path, 131072)
candidate = corrected(original)
guard.verify_files()
manifest_path = guard_path.parent / 'manifest.json'
manifest_data = guard.read_regular(manifest_path)
manifest = json.loads(manifest_data)
key = str(guard_path)
if manifest['files'].get(key) != PREIMAGE_SHA:
raise RuntimeError('Manifest does not authenticate audited guard')
manifest['files'][key] = digest(candidate)
# Preserve the complete manifest formatting and all other bytes.
if manifest_data.count(PREIMAGE_SHA.encode()) != 1:
raise RuntimeError('Manifest hash anchor is ambiguous')
candidate_manifest = manifest_data.replace(PREIMAGE_SHA.encode(),
digest(candidate).encode())
if json.loads(candidate_manifest) != manifest:
raise RuntimeError('Unexpected manifest change')
destination = lab / 'guard-conflicts-repair'
destination.mkdir(mode=0o700) # Exclusive; never overwrite an earlier receipt.
records = {}
patch = ''
for path, old, new in ((guard_path, original, candidate),
(manifest_path, manifest_data, candidate_manifest)):
mode = stat.S_IMODE(path.stat().st_mode)
write_private(destination / (path.name + '.original'), old)
write_private(destination / path.name, new)
records[str(path)] = {'old_sha256': digest(old), 'new_sha256': digest(new),
'old_mode': mode, 'deployment_mode': mode,
'staged_mode': 0o400}
patch += ''.join(difflib.unified_diff(
old.decode().splitlines(True), new.decode().splitlines(True),
fromfile=str(path), tofile=str(path)))
write_private(destination / 'exact.patch', patch.encode())
write_private(destination / 'receipt.json', json_bytes({
'status': 'SOURCE_ONLY / NOT_INSTALLED / PHYSICAL_NOT_RUN',
'files': records, 'installation_authorized': False}))
guard.verify_files()
if (guard.read_regular(guard_path, 131072) != original
or guard.read_regular(manifest_path) != manifest_data):
raise RuntimeError('Installed inputs changed during staging')
return destination
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--guard', type=Path, required=True)
parser.add_argument('--lab', type=Path, required=True)
args = parser.parse_args()
print(stage(args.guard, args.lab))
if __name__ == '__main__':
main()