Live-USB durch eigenständige Anwendung ersetzen
Some checks failed
Tests / test (push) Has been cancelled

This commit is contained in:
2026-10-15 10:26:33 +02:00
parent 692bbbb815
commit 1f91db25b5
23 changed files with 205 additions and 1228 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Source fingerprint and artifact verification; never touches USB devices."""
"""Create and verify the reproducible standalone application manifest."""
import argparse
import hashlib
import json
@@ -8,7 +8,7 @@ import subprocess
import tomllib
ROOT = Path(__file__).resolve().parents[1]
COMPONENTS = {'gui': 'intake-gui', 'scanner': 'pc-intake', 'controller': 'intake-controller'}
COMPONENTS = {'gui': 'intake-gui', 'scanner': 'pc-intake'}
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
@@ -19,8 +19,7 @@ def source_id():
base = ROOT / component
paths.extend((base / 'src').rglob('*.rs'))
paths.extend(p for p in [base / 'Cargo.toml', base / 'Cargo.lock', base / 'build.rs'] if p.exists())
for folder in ['scripts', 'live']:
paths.extend(p for p in (ROOT / folder).iterdir() if p.is_file())
paths.extend(p for p in (ROOT / 'scripts').iterdir() if p.is_file())
h = hashlib.sha256()
for path in sorted(paths):
h.update(str(path.relative_to(ROOT)).encode() + b'\0')
@@ -41,38 +40,33 @@ def create(path):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(manifest, indent=2) + '\n')
def verify(profile):
root = profile / 'airootfs'
manifest = json.loads((root / 'etc/csr-intake/release.json').read_text())
def verify(release_dir):
manifest = json.loads((release_dir / 'release.json').read_text())
expected = set(COMPONENTS.values())
if manifest.get('schema_version') != 1:
raise ValueError('Unsupported release manifest schema')
if set(manifest.get('binaries', {})) != expected:
raise ValueError('Release manifest has missing or unexpected binaries')
for name, spec in manifest['binaries'].items():
binary = root / 'usr/local/bin' / name
binary = release_dir / name
if digest(binary) != spec['sha256']:
raise ValueError(f'Binary hash mismatch: {name}')
if not binary.stat().st_mode & 0o111:
raise ValueError(f'Binary is not executable: {name}')
version = subprocess.check_output([root / 'usr/local/bin/intake-gui', '--version'], text=True)
version = subprocess.check_output([release_dir / 'intake-gui', '--version'], text=True)
if manifest['build_id'] not in version:
raise ValueError('GUI build ID differs from release manifest')
required = ['networkmanager', 'curl', 'ca-certificates', 'python', 'usbutils', 'smartmontools', 'alsa-utils', 'ffmpeg', 'stress-ng', 'xdg-utils', 'firefox']
packages = (profile / 'packages.x86_64').read_text().splitlines()
for package in required:
if package not in packages:
raise ValueError(f'Missing live package: {package}')
for file in ['usr/local/bin/intake-login', 'usr/local/bin/intake-prepare', 'etc/systemd/system/getty@tty1.service.d/autologin.conf', 'etc/systemd/system/multi-user.target.wants/NetworkManager.service']:
p = root / file
if not p.exists() and not p.is_symlink():
raise ValueError(f'Missing live startup file: {file}')
print(f"Profile verified: {manifest['build_id']}")
print(f"Release verified: {manifest['build_id']}")
def main():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--source-id', action='store_true')
group.add_argument('--create', type=Path)
group.add_argument('--verify-profile', type=Path)
group.add_argument('--verify-release', type=Path)
args = parser.parse_args()
if args.source_id: print(source_id())
elif args.create: create(args.create)
else: verify(args.verify_profile)
else: verify(args.verify_release)
if __name__ == '__main__': main()