"""Bind two fully tested editions to one immutable, selectable USB image."""
import argparse
import importlib.util
import json
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[2]
def load_media():
spec = importlib.util.spec_from_file_location('xfe_media', ROOT / 'scripts/physical-media-verify.py')
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def resolve_bound(folder, entry, media):
path = Path(entry['Path'])
if not path.is_absolute():
path = folder / path
path = path.resolve()
media.require(media.digest(path) == entry['Sha256'], 'Dual delivery artifact changed: ' + str(path))
return path
def evidence_files(proof_paths, media):
artifacts = {}
for proof_path in proof_paths:
for artifact in proof_path.parent.iterdir():
if artifact.is_file() and artifact.suffix.lower() in ('.json', '.png', '.log', '.txt'):
artifacts[str(artifact.resolve())] = {'Path': str(artifact.resolve()), 'Sha256': media.digest(artifact)}
return [artifacts[name] for name in sorted(artifacts)]
def verify_dual(image, composite_path, media=None):
media = media or load_media()
require = media.require
image, composite_path = Path(image).resolve(), Path(composite_path).resolve()
composite, composite_hash = media.read_bound_json(composite_path)
require(composite.get('Kind') == 'XfeIndependentDualBootProof' and composite.get('Passed') is True,
'A dual edition acceptance receipt is required')
require(composite.get('ImageSha256') == media.digest(image), 'The dual image differs from its acceptance')
manifest, manifest_hash = media.read_bound_json(str(image) + '.json')
require(manifest.get('Edition') == 'Dual' and manifest.get('BootEntries') == ['Desktop', 'Console'] and
manifest.get('DefaultBootEntry') == 'Desktop' and manifest.get('BootMenuTimeoutSeconds') == 10,
'Expected the explicit Desktop/Console boot menu')
desktop_path = resolve_bound(composite_path.parent, composite['DesktopBoot'], media)
console_path = resolve_bound(composite_path.parent, composite['ConsoleBoot'], media)
desktop = media.read_json(desktop_path)
console = media.read_json(console_path)
for proof, index, label in ((desktop, 0, 'XFESystem Desktop'), (console, 1, 'XFESystem Console')):
menu = proof.get('BootMenuSelection', {})
require(type(proof.get('BootEntry')) is int and proof['BootEntry'] == index and
proof.get('SourcesUnchanged') is True and
menu.get('Input') == 'Actual QMP PS2 keyboard packets only' and
type(menu.get('Index')) is int and menu['Index'] == index and menu.get('Entry') == label and
type(menu.get('MenuSerialOffset')) is int and menu['MenuSerialOffset'] > 0,
'Actual source-bound QMP menu selection is required for ' + label)
require(desktop.get('Edition') == 'Desktop' and desktop.get('KernelImagePath') == 'System/Kernel.elf' and
desktop.get('BootEntry') == 0, 'Actual default desktop menu boot is required')
# Reuse every existing geometry, source, application, HTTPS, documents and
# real terminal guard for the desktop half. No Boolean-only shortcut.
verified = media.verify(image, desktop_path, 'IndependentCSharp128', _dual_image=True)
require(console.get('Kind') == 'XfeIndependentKernelBootProof' and console.get('Passed') is True and
console.get('Edition') == 'Console' and console.get('KernelImagePath') == 'System/Console.elf' and
console.get('BootEntry') == 1 and console.get('ConsoleAcceptanceRequired') is True and
console.get('ConsoleAcceptancePassed') is True and console.get('ManagedRuntimeAcceptancePassed') is True and
console.get('ContainsCosmos') is False and console.get('ImageUnchanged') is True and
type(console.get('QemuExitCode')) is int and console['QemuExitCode'] == 0 and not console.get('ForcedCleanup'),
'Actual cleaned-up standalone console menu boot is required')
require(console.get('ImageSha256') == verified['ImageSha256'] and
console.get('KernelSha256') == manifest.get('ConsoleKernelSha256') and
console.get('KernelSha256') == verified['IndependentImageInspection']['Files'].get('System/Console.elf') and
console.get('ImageVerification', {}).get('Files') == verified['IndependentImageInspection']['Files'],
'Console proof does not bind the exact dual image inventory')
console_snapshot_path = Path(console['SourceSnapshotPath'])
if not console_snapshot_path.is_absolute():
console_snapshot_path = console_path.parent / console_snapshot_path
console_snapshot, console_snapshot_hash = media.read_bound_json(console_snapshot_path)
require(console_snapshot_hash == console['SourceSnapshotSha256'], 'Console source snapshot changed')
for relative, expected in console_snapshot.items():
source = (ROOT / relative).resolve()
require(source.is_relative_to(ROOT) and source.is_file() and media.digest(source) == expected,
'Console source changed: ' + relative)
console_build = manifest.get('IndependentConsoleKernelBuild', {})
require(console_build.get('Kind') == 'XfeIndependentKernelBuild' and console_build.get('Passed') is True and
console_build.get('Edition') == 'Console' and console_build.get('ContainsCosmos') is False and
console_build.get('BootstrapOnly') is False and
console_build.get('Configuration') == manifest['IndependentKernelBuild']['Configuration'],
'Both editions must use the same RAM-only self-developed kernel configuration')
gate = media.verify_independent_delivery(image, console_path, console_snapshot, verified['ImageSha256'],
media.digest(console_path), 'Console')
merged_path = resolve_bound(composite_path.parent, composite['MergedSourceSnapshot'], media)
merged = media.read_json(merged_path)
expected_merged = media.read_json(verified['SourceSnapshotPath'])
for name, expected in console_snapshot.items():
require(name not in expected_merged or expected_merged[name] == expected, 'Edition source snapshots conflict: ' + name)
expected_merged[name] = expected
require(merged == expected_merged, 'Dual source snapshot is not the exact union of both tested editions')
for relative in ('Tools/IndependentKernel/TestBoot.py', 'Tools/IndependentKernel/VerifyDualDelivery.py',
'scripts/physical-media-verify.py', 'Tools/CSharpBoot/Program.cs', 'Tools/CSharpBoot/ReadImage.py'):
require(merged.get(relative) == media.digest(ROOT / relative), 'Missing or stale dual delivery harness: ' + relative)
usb_paths=[]
if manifest['IndependentKernelBuild']['Configuration'].get('UsbHidEnabled') is True:
from IndependentConsoleEvidence import terminal_command_output
entries=composite.get('UsbInputEvidence', [])
require(len(entries)==2, 'Both editions require actual USB-only acceptance')
for entry,edition,kernel_sha in zip(entries,('Desktop','Console'),(manifest['KernelSha256'],manifest['ConsoleKernelSha256'])):
path=resolve_bound(composite_path.parent,entry,media);usb_paths.append(path)
usb=media.read_json(path)
require(usb.get('Kind')=='XfeIndependentUsbInputProof' and usb.get('Passed') is True and
usb.get('Edition')==edition and usb.get('KernelSha256')==kernel_sha and usb.get('Hub') is True and
usb.get('Ps2ControllerPresent') is False and usb.get('HardwareReportsQueued') is True and
usb.get('ActualKeyboardUnplugReplugPassed') is True and usb.get('SourcesUnchanged') is True and
usb.get('PhysicalCorsairVerified') is False and usb.get('GuestCudaVerified') is False and
usb.get('QemuExitCode')==0 and not usb.get('ForcedCleanup') and usb.get('ImageUnchanged') is True,
'Incomplete USB-only kernel, HID, cleanup or hotplug evidence: '+edition)
for name,sha in usb['SourceHashes'].items():
require(merged.get(name)==sha==media.digest(ROOT/name), 'USB proof source differs from dual delivery: '+name)
require(media.digest(usb['ImagePath'])==usb['ImageSha256'], 'The separate USB test image changed')
qtree=path.parent/'Qtree.txt';serial=path.parent/'Serial.log'
require(media.digest(qtree)==usb['QtreeSha256'] and media.digest(serial)==usb['SerialSha256'], 'USB hardware evidence changed')
tree=qtree.read_text(encoding='utf-8');log=serial.read_text(encoding='utf-8')
require('dev: i8042' not in tree and 'dev: ps2-kbd' not in tree and 'dev: ps2-mouse' not in tree and
'dev: usb-kbd' in tree and 'dev: usb-mouse' in tree and 'dev: usb-hub' in tree,
'USB input must not fall back to a PS/2 device')
require(all(x in log for x in ('XFE_USB_HID_READY','|Kind|1|','|Kind|2|',
'FIRST_HARDWARE_KEY_QUEUED','FIRST_HARDWARE_POINTER_QUEUED','|HUB_CONTEXT_CONFIGURED','|DISCONNECTED')), 'Missing actual hardware USB reports')
require(usb.get('HubContextReadbackVerified') is False and usb.get('QemuHubContextNotModeled') is True and
'|HUB_CONTEXT_QEMU_NOT_MODELED' in log, 'QEMU does not model physical hub/TT context readback; do not claim hardware proof')
require('usb-replug' in terminal_command_output(usb['RepluggedCommandReadback']['Value'],'echo usb-replug'),
'The replugged keyboard must submit and render the exact echo command')
require(usb.get('ActualUsbDesktopEditingPassed' if edition=='Desktop' else 'ActualUsbConsoleEditingPassed') is True,
'Actual USB application editing missing')
require(composite.get('EvidenceFiles') == evidence_files((desktop_path, console_path,*usb_paths), media),
'Dual evidence locks must contain the exact complete evidence set for both guests')
for artifact in composite['EvidenceFiles']:
resolve_bound(composite_path.parent, artifact, media)
require(composite.get('FullReleaseQualified') is False, 'Dual delivery remains experimental')
require(media.digest(str(image) + '.json') == manifest_hash == verified['ImageManifestSha256'] and
media.digest(composite_path) == composite_hash,
'The dual manifest or composite receipt changed during verification')
return {**verified, 'Profile': 'IndependentDualCSharp256', 'BootResultPath': str(composite_path),
'BootResultSha256': composite_hash, 'SourceSnapshotPath': str(merged_path),
'SourceSnapshotSha256': media.digest(merged_path), 'ConsoleFinalDeliveryGate': gate,
'DualEditionAcceptancePassed': True, 'AdditionalEvidenceLocks': composite['EvidenceFiles'],
'DualVerifierSha256': media.digest(__file__), 'BootMenuEntries': ['Desktop', 'Console']}
def prepare(image, desktop_path, console_path, output, usb_desktop=None, usb_console=None):
media = load_media()
image, desktop_path, console_path, output = map(lambda p: Path(p).resolve(), (image, desktop_path, console_path, output))
media.require(not output.exists(), 'Refusing to replace an existing dual delivery receipt')
output.parent.mkdir(parents=True, exist_ok=True)
merged = {}
for proof_path in (desktop_path, console_path):
proof = media.read_json(proof_path)
snapshot_path = Path(proof['SourceSnapshotPath'])
if not snapshot_path.is_absolute():
snapshot_path = proof_path.parent / snapshot_path
for name, expected in media.read_json(snapshot_path).items():
media.require(name not in merged or merged[name] == expected, 'Source maps conflict: ' + name)
merged[name] = expected
merged_path = output.parent / 'MergedSourceSnapshot.json'
with merged_path.open('x', encoding='utf-8') as stream:
json.dump(dict(sorted(merged.items())), stream, indent=2)
usb_paths=[Path(p).resolve() for p in (usb_desktop,usb_console) if p is not None]
composite = {'Kind': 'XfeIndependentDualBootProof', 'Passed': True, 'ImageSha256': media.digest(image),
'DesktopBoot': {'Path': str(desktop_path), 'Sha256': media.digest(desktop_path)},
'ConsoleBoot': {'Path': str(console_path), 'Sha256': media.digest(console_path)},
'MergedSourceSnapshot': {'Path': str(merged_path), 'Sha256': media.digest(merged_path)},
'UsbInputEvidence': [{'Path':str(p),'Sha256':media.digest(p)} for p in usb_paths],
'EvidenceFiles': evidence_files((desktop_path, console_path,*usb_paths), media), 'FullReleaseQualified': False}
with output.open('x', encoding='utf-8') as stream:
json.dump(composite, stream, indent=2)
try:
verified = verify_dual(image, output, media)
except Exception:
composite['Passed'] = False
output.write_text(json.dumps(composite, indent=2), encoding='utf-8')
raise
print(json.dumps(verified, ensure_ascii=True))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('image')
parser.add_argument('--desktop-proof', required=True)
parser.add_argument('--console-proof', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--usb-desktop-proof')
parser.add_argument('--usb-console-proof')
args = parser.parse_args()
prepare(args.image, args.desktop_proof, args.console_proof, args.output,args.usb_desktop_proof,args.usb_console_proof)
"""Bind two fully tested editions to one immutable, selectable USB image."""
import argparse
import importlib.util
import json
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[2]
def load_media():
spec = importlib.util.spec_from_file_location('xfe_media', ROOT / 'scripts/physical-media-verify.py')
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def resolve_bound(folder, entry, media):
path = Path(entry['Path'])
if not path.is_absolute():
path = folder / path
path = path.resolve()
media.require(media.digest(path) == entry['Sha256'], 'Dual delivery artifact changed: ' + str(path))
return path
def evidence_files(proof_paths, media):
artifacts = {}
for proof_path in proof_paths:
for artifact in proof_path.parent.iterdir():
if artifact.is_file() and artifact.suffix.lower() in ('.json', '.png', '.log', '.txt'):
artifacts[str(artifact.resolve())] = {'Path': str(artifact.resolve()), 'Sha256': media.digest(artifact)}
return [artifacts[name] for name in sorted(artifacts)]
def verify_dual(image, composite_path, media=None):
media = media or load_media()
require = media.require
image, composite_path = Path(image).resolve(), Path(composite_path).resolve()
composite, composite_hash = media.read_bound_json(composite_path)
require(composite.get('Kind') == 'XfeIndependentDualBootProof' and composite.get('Passed') is True,
'A dual edition acceptance receipt is required')
require(composite.get('ImageSha256') == media.digest(image), 'The dual image differs from its acceptance')
manifest, manifest_hash = media.read_bound_json(str(image) + '.json')
require(manifest.get('Edition') == 'Dual' and manifest.get('BootEntries') == ['Desktop', 'Console'] and
manifest.get('DefaultBootEntry') == 'Desktop' and manifest.get('BootMenuTimeoutSeconds') == 10,
'Expected the explicit Desktop/Console boot menu')
desktop_path = resolve_bound(composite_path.parent, composite['DesktopBoot'], media)
console_path = resolve_bound(composite_path.parent, composite['ConsoleBoot'], media)
desktop = media.read_json(desktop_path)
console = media.read_json(console_path)
for proof, index, label in ((desktop, 0, 'XFESystem Desktop'), (console, 1, 'XFESystem Console')):
menu = proof.get('BootMenuSelection', {})
require(type(proof.get('BootEntry')) is int and proof['BootEntry'] == index and
proof.get('SourcesUnchanged') is True and
menu.get('Input') == 'Actual QMP PS2 keyboard packets only' and
type(menu.get('Index')) is int and menu['Index'] == index and menu.get('Entry') == label and
type(menu.get('MenuSerialOffset')) is int and menu['MenuSerialOffset'] > 0,
'Actual source-bound QMP menu selection is required for ' + label)
require(desktop.get('Edition') == 'Desktop' and desktop.get('KernelImagePath') == 'System/Kernel.elf' and
desktop.get('BootEntry') == 0, 'Actual default desktop menu boot is required')
# Reuse every existing geometry, source, application, HTTPS, documents and
# real terminal guard for the desktop half. No Boolean-only shortcut.
verified = media.verify(image, desktop_path, 'IndependentCSharp128', _dual_image=True)
require(console.get('Kind') == 'XfeIndependentKernelBootProof' and console.get('Passed') is True and
console.get('Edition') == 'Console' and console.get('KernelImagePath') == 'System/Console.elf' and
console.get('BootEntry') == 1 and console.get('ConsoleAcceptanceRequired') is True and
console.get('ConsoleAcceptancePassed') is True and console.get('ManagedRuntimeAcceptancePassed') is True and
console.get('ContainsCosmos') is False and console.get('ImageUnchanged') is True and
type(console.get('QemuExitCode')) is int and console['QemuExitCode'] == 0 and not console.get('ForcedCleanup'),
'Actual cleaned-up standalone console menu boot is required')
require(console.get('ImageSha256') == verified['ImageSha256'] and
console.get('KernelSha256') == manifest.get('ConsoleKernelSha256') and
console.get('KernelSha256') == verified['IndependentImageInspection']['Files'].get('System/Console.elf') and
console.get('ImageVerification', {}).get('Files') == verified['IndependentImageInspection']['Files'],
'Console proof does not bind the exact dual image inventory')
console_snapshot_path = Path(console['SourceSnapshotPath'])
if not console_snapshot_path.is_absolute():
console_snapshot_path = console_path.parent / console_snapshot_path
console_snapshot, console_snapshot_hash = media.read_bound_json(console_snapshot_path)
require(console_snapshot_hash == console['SourceSnapshotSha256'], 'Console source snapshot changed')
for relative, expected in console_snapshot.items():
source = (ROOT / relative).resolve()
require(source.is_relative_to(ROOT) and source.is_file() and media.digest(source) == expected,
'Console source changed: ' + relative)
console_build = manifest.get('IndependentConsoleKernelBuild', {})
require(console_build.get('Kind') == 'XfeIndependentKernelBuild' and console_build.get('Passed') is True and
console_build.get('Edition') == 'Console' and console_build.get('ContainsCosmos') is False and
console_build.get('BootstrapOnly') is False and
console_build.get('Configuration') == manifest['IndependentKernelBuild']['Configuration'],
'Both editions must use the same RAM-only self-developed kernel configuration')
gate = media.verify_independent_delivery(image, console_path, console_snapshot, verified['ImageSha256'],
media.digest(console_path), 'Console')
merged_path = resolve_bound(composite_path.parent, composite['MergedSourceSnapshot'], media)
merged = media.read_json(merged_path)
expected_merged = media.read_json(verified['SourceSnapshotPath'])
for name, expected in console_snapshot.items():
require(name not in expected_merged or expected_merged[name] == expected, 'Edition source snapshots conflict: ' + name)
expected_merged[name] = expected
require(merged == expected_merged, 'Dual source snapshot is not the exact union of both tested editions')
for relative in ('Tools/IndependentKernel/TestBoot.py', 'Tools/IndependentKernel/VerifyDualDelivery.py',
'scripts/physical-media-verify.py', 'Tools/CSharpBoot/Program.cs', 'Tools/CSharpBoot/ReadImage.py'):
require(merged.get(relative) == media.digest(ROOT / relative), 'Missing or stale dual delivery harness: ' + relative)
usb_paths=[]
if manifest['IndependentKernelBuild']['Configuration'].get('UsbHidEnabled') is True:
from IndependentConsoleEvidence import terminal_command_output
entries=composite.get('UsbInputEvidence', [])
require(len(entries)==2, 'Both editions require actual USB-only acceptance')
for entry,edition,kernel_sha in zip(entries,('Desktop','Console'),(manifest['KernelSha256'],manifest['ConsoleKernelSha256'])):
path=resolve_bound(composite_path.parent,entry,media);usb_paths.append(path)
usb=media.read_json(path)
require(usb.get('Kind')=='XfeIndependentUsbInputProof' and usb.get('Passed') is True and
usb.get('Edition')==edition and usb.get('KernelSha256')==kernel_sha and usb.get('Hub') is True and
usb.get('Ps2ControllerPresent') is False and usb.get('HardwareReportsQueued') is True and
usb.get('ActualKeyboardUnplugReplugPassed') is True and usb.get('SourcesUnchanged') is True and
usb.get('PhysicalCorsairVerified') is False and usb.get('GuestCudaVerified') is False and
usb.get('QemuExitCode')==0 and not usb.get('ForcedCleanup') and usb.get('ImageUnchanged') is True,
'Incomplete USB-only kernel, HID, cleanup or hotplug evidence: '+edition)
for name,sha in usb['SourceHashes'].items():
require(merged.get(name)==sha==media.digest(ROOT/name), 'USB proof source differs from dual delivery: '+name)
require(media.digest(usb['ImagePath'])==usb['ImageSha256'], 'The separate USB test image changed')
qtree=path.parent/'Qtree.txt';serial=path.parent/'Serial.log'
require(media.digest(qtree)==usb['QtreeSha256'] and media.digest(serial)==usb['SerialSha256'], 'USB hardware evidence changed')
tree=qtree.read_text(encoding='utf-8');log=serial.read_text(encoding='utf-8')
require('dev: i8042' not in tree and 'dev: ps2-kbd' not in tree and 'dev: ps2-mouse' not in tree and
'dev: usb-kbd' in tree and 'dev: usb-mouse' in tree and 'dev: usb-hub' in tree,
'USB input must not fall back to a PS/2 device')
require(all(x in log for x in ('XFE_USB_HID_READY','|Kind|1|','|Kind|2|',
'FIRST_HARDWARE_KEY_QUEUED','FIRST_HARDWARE_POINTER_QUEUED','|HUB_CONTEXT_CONFIGURED','|DISCONNECTED')), 'Missing actual hardware USB reports')
require(usb.get('HubContextReadbackVerified') is False and usb.get('QemuHubContextNotModeled') is True and
'|HUB_CONTEXT_QEMU_NOT_MODELED' in log, 'QEMU does not model physical hub/TT context readback; do not claim hardware proof')
require('usb-replug' in terminal_command_output(usb['RepluggedCommandReadback']['Value'],'echo usb-replug'),
'The replugged keyboard must submit and render the exact echo command')
require(usb.get('ActualUsbDesktopEditingPassed' if edition=='Desktop' else 'ActualUsbConsoleEditingPassed') is True,
'Actual USB application editing missing')
require(composite.get('EvidenceFiles') == evidence_files((desktop_path, console_path,*usb_paths), media),
'Dual evidence locks must contain the exact complete evidence set for both guests')
for artifact in composite['EvidenceFiles']:
resolve_bound(composite_path.parent, artifact, media)
require(composite.get('FullReleaseQualified') is False, 'Dual delivery remains experimental')
require(media.digest(str(image) + '.json') == manifest_hash == verified['ImageManifestSha256'] and
media.digest(composite_path) == composite_hash,
'The dual manifest or composite receipt changed during verification')
return {**verified, 'Profile': 'IndependentDualCSharp256', 'BootResultPath': str(composite_path),
'BootResultSha256': composite_hash, 'SourceSnapshotPath': str(merged_path),
'SourceSnapshotSha256': media.digest(merged_path), 'ConsoleFinalDeliveryGate': gate,
'DualEditionAcceptancePassed': True, 'AdditionalEvidenceLocks': composite['EvidenceFiles'],
'DualVerifierSha256': media.digest(__file__), 'BootMenuEntries': ['Desktop', 'Console']}
def prepare(image, desktop_path, console_path, output, usb_desktop=None, usb_console=None):
media = load_media()
image, desktop_path, console_path, output = map(lambda p: Path(p).resolve(), (image, desktop_path, console_path, output))
media.require(not output.exists(), 'Refusing to replace an existing dual delivery receipt')
output.parent.mkdir(parents=True, exist_ok=True)
merged = {}
for proof_path in (desktop_path, console_path):
proof = media.read_json(proof_path)
snapshot_path = Path(proof['SourceSnapshotPath'])
if not snapshot_path.is_absolute():
snapshot_path = proof_path.parent / snapshot_path
for name, expected in media.read_json(snapshot_path).items():
media.require(name not in merged or merged[name] == expected, 'Source maps conflict: ' + name)
merged[name] = expected
merged_path = output.parent / 'MergedSourceSnapshot.json'
with merged_path.open('x', encoding='utf-8') as stream:
json.dump(dict(sorted(merged.items())), stream, indent=2)
usb_paths=[Path(p).resolve() for p in (usb_desktop,usb_console) if p is not None]
composite = {'Kind': 'XfeIndependentDualBootProof', 'Passed': True, 'ImageSha256': media.digest(image),
'DesktopBoot': {'Path': str(desktop_path), 'Sha256': media.digest(desktop_path)},
'ConsoleBoot': {'Path': str(console_path), 'Sha256': media.digest(console_path)},
'MergedSourceSnapshot': {'Path': str(merged_path), 'Sha256': media.digest(merged_path)},
'UsbInputEvidence': [{'Path':str(p),'Sha256':media.digest(p)} for p in usb_paths],
'EvidenceFiles': evidence_files((desktop_path, console_path,*usb_paths), media), 'FullReleaseQualified': False}
with output.open('x', encoding='utf-8') as stream:
json.dump(composite, stream, indent=2)
try:
verified = verify_dual(image, output, media)
except Exception:
composite['Passed'] = False
output.write_text(json.dumps(composite, indent=2), encoding='utf-8')
raise
print(json.dumps(verified, ensure_ascii=True))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('image')
parser.add_argument('--desktop-proof', required=True)
parser.add_argument('--console-proof', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--usb-desktop-proof')
parser.add_argument('--usb-console-proof')
args = parser.parse_args()
prepare(args.image, args.desktop_proof, args.console_proof, args.output,args.usb_desktop_proof,args.usb_console_proof)