#!/usr/bin/env python3 """Verify release signatures with independent OpenSSL and native/static ARM tools.""" import argparse import copy import hashlib import json import os from pathlib import Path import shutil import stat import subprocess import tempfile project = Path(__file__).resolve().parents[2] parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--host-only', action='store_true') args = parser.parse_args() work = Path(tempfile.mkdtemp(prefix='m12-signing.', dir=project / 'out')) host = project / 'target/x86_64-unknown-linux-gnu/release/fds-release' arm = project / 'target/aarch64-unknown-linux-musl/release/fds-release' commands = [('host', [str(host)])] if not args.host_only: commands.append(('arm', [str(project / 'tools/in-void'), '--isolated-network', 'qemu-aarch64', str(arm)])) log = (work / 'commands.log').open('w') def invoke(prefix, arguments, ok=True): result = subprocess.run([*prefix, *map(str, arguments)], capture_output=True, text=True, timeout=60) log.write('COMMAND ' + repr([*prefix, *map(str, arguments)]) + '\n' + result.stdout + result.stderr) log.flush() assert (result.returncode == 0) == ok, (arguments, result.returncode, result.stdout, result.stderr) return result.stdout + result.stderr def check_all(directory, key, ok=True): for label, command in commands: result = invoke(command, ['verify', directory, '--key', key], ok) if ok: assert 'VERIFIED FDS/OS 0.1.0' in result key_prefix = work / 'test-key' invoke([str(host)], ['keygen', key_prefix]) private, public = work / 'test-key.key', work / 'test-key.pub' assert private.stat().st_size == 32 and stat.S_IMODE(private.stat().st_mode) == 0o600 invoke([str(host)], ['keygen', key_prefix], False) invoke([str(host)], ['public-key', private, work / 'exported.pub']) assert (work / 'exported.pub').read_bytes() == public.read_bytes() release = work / 'release' release.mkdir() payload = b'FDS signed artifact acceptance.\n' * 8192 (release / 'image.img').write_bytes(payload) manifest = {'format': 1, 'version': '0.1.0', 'source_epoch': 1, 'source_sha256': hashlib.sha256(b'fixture source').hexdigest(), 'void_commit': (project / 'VOID_PACKAGES_COMMIT').read_text().strip(), 'hardware_validation': 'deferred', 'files': [{'name': 'image.img', 'bytes': len(payload), 'sha256': hashlib.sha256(payload).hexdigest()}]} (release / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') invoke([str(host)], ['sign', release, '--key', private]) check_all(release, public) invoke([str(host)], ['sign', release, '--key', private], False) # Independent standard Ed25519 verification/signing over the exact domain prefix # and raw manifest bytes. Test-only DER files are private and never printed. message = work / 'message' message.write_bytes(b'FDS/OS release manifest v1\0' + (release / 'manifest.json').read_bytes()) private_der = work / 'private.der' fd = os.open(private_der, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, 'wb') as stream: stream.write(bytes.fromhex('302e020100300506032b657004220420') + private.read_bytes()) public_der = work / 'public.der' public_der.write_bytes(bytes.fromhex('302a300506032b6570032100') + bytes.fromhex(public.read_text().strip())) signature = work / 'signature.bin' signature.write_bytes(bytes.fromhex((release / 'manifest.sig').read_text().strip())) invoke(['openssl'], ['pkeyutl', '-verify', '-rawin', '-pubin', '-inkey', public_der, '-keyform', 'DER', '-in', message, '-sigfile', signature]) invoke(['openssl'], ['pkeyutl', '-sign', '-rawin', '-inkey', private_der, '-keyform', 'DER', '-in', message, '-out', work / 'openssl.sig']) assert (work / 'openssl.sig').read_bytes() == signature.read_bytes() (release / 'manifest.sig').write_text((work / 'openssl.sig').read_bytes().hex() + '\n') check_all(release, public) print('PASS: actual key generation, no-overwrite policy, deterministic signatures and independent OpenSSL interoperability', flush=True) other = work / 'other-key' invoke([str(host)], ['keygen', other]) check_all(release, work / 'other-key.pub', False) weak = work / 'weak.pub' weak.write_text('01' + '00' * 31 + '\n') check_all(release, weak, False) private.chmod(0o644) invoke([str(host)], ['public-key', private, work / 'insecure.pub'], False) private.chmod(0o600) for name in ['payload', 'size', 'missing', 'signature', 'manifest', 'symlink', 'fifo']: damaged = work / ('bad-' + name) shutil.copytree(release, damaged) target = damaged / 'image.img' if name == 'payload': data = bytearray(target.read_bytes()); data[4096] ^= 1; target.write_bytes(data) elif name == 'size': with target.open('ab') as stream: stream.write(b'X') elif name == 'missing': target.unlink() elif name == 'signature': (damaged / 'manifest.sig').write_text('00' * 64 + '\n') elif name == 'manifest': # A semantically identical JSON document still changes the signed bytes. with (damaged / 'manifest.json').open('a') as stream: stream.write('\n') elif name == 'symlink': target.unlink(); target.symlink_to(release / 'image.img') elif name == 'fifo': target.unlink(); os.mkfifo(target, 0o600) check_all(damaged, public, False) for name in ['traversal', 'duplicate', 'unknown', 'wrong-version', 'unbounded']: bad = work / ('manifest-' + name) bad.mkdir() value = copy.deepcopy(manifest) if name == 'traversal': value['files'][0]['name'] = '../image.img' elif name == 'duplicate': value['files'].append(copy.deepcopy(value['files'][0])) elif name == 'unknown': value['execute'] = 'sh' elif name == 'wrong-version': value['version'] = '999' (bad / 'manifest.json').write_text(json.dumps(value) + (' ' * (1024 * 1024) if name == 'unbounded' else '')) (bad / 'manifest.sig').write_bytes((release / 'manifest.sig').read_bytes()) check_all(bad, public, False) # The ARM signer must produce the same bytes for the same manifest and key. if not args.host_only: counterpart = work / 'arm-signed' shutil.copytree(release, counterpart) (counterpart / 'manifest.sig').unlink() invoke(commands[1][1], ['sign', counterpart, '--key', private]) assert (counterpart / 'manifest.sig').read_bytes() == (release / 'manifest.sig').read_bytes() check_all(counterpart, public) log.close() link = project / ('out/m12-signing-host-latest.next' if args.host_only else 'out/m12-signing-latest.next') link.symlink_to(work.name) link.replace(link.with_suffix('')) print(f'PASS: {"host" if args.host_only else "host and actual static ARM"} signature/artifact verification and rejection matrix: {work}', flush=True) print('NOTE: this is a tool acceptance fixture, not a signed FDS/OS release or a secure-boot claim')