118 lines
5.2 KiB
Python
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()
|