93 lines
5.2 KiB
Python
93 lines
5.2 KiB
Python
"""Inspect a downloaded Windows ZIP without extracting or running its contents.
|
|
|
|
Expected archive/binary hashes must come from separately reviewed build evidence.
|
|
This checks artifact integrity and provenance assertions, not native acceptance.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import stat
|
|
import zipfile
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('archive', type=Path)
|
|
for option in ('archive-sha256', 'source', 'version', 'desktop-sha256', 'server-sha256', 'runtime-sha256'):
|
|
parser.add_argument('--' + option, required=True)
|
|
parser.add_argument('--documents-receipt', required=True, type=Path)
|
|
args = parser.parse_args()
|
|
|
|
def sha(raw):
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
for value in (args.archive_sha256, args.desktop_sha256, args.server_sha256, args.runtime_sha256):
|
|
assert re.fullmatch(r'[0-9a-f]{64}', value), 'Expected lowercase SHA256 required'
|
|
assert re.fullmatch(r'[0-9a-f]{40}', args.source), 'Exact source commit required'
|
|
assert sha(args.archive.read_bytes()) == args.archive_sha256, 'Archive checksum differs'
|
|
target = 'x86_64-pc-windows-gnullvm'
|
|
root = f'ArcadiaStudio-{args.version}-{target}/'
|
|
expected_binaries = {'arcadia-studio.exe': args.desktop_sha256,
|
|
'arcadia-server.exe': args.server_sha256,
|
|
'libunwind.dll': args.runtime_sha256}
|
|
with zipfile.ZipFile(args.archive) as archive:
|
|
infos = archive.infolist()
|
|
assert sum(row.file_size for row in infos) <= 1024 ** 3, 'Unexpected package size'
|
|
assert len({row.filename.casefold() for row in infos}) == len(infos), 'Duplicate/case-colliding archive members'
|
|
files = {}
|
|
for row in infos:
|
|
name = row.filename
|
|
path = PurePosixPath(name)
|
|
assert not path.is_absolute() and '..' not in path.parts and '\\' not in name and ':' not in name
|
|
assert name.startswith(root), 'Unexpected archive root'
|
|
assert not stat.S_ISLNK(row.external_attr >> 16), 'Symlink archive member'
|
|
if not row.is_dir():
|
|
relative = name[len(root):]
|
|
assert relative and relative not in files
|
|
files[relative] = sha(archive.read(row))
|
|
|
|
def read(name):
|
|
assert name in files, 'Missing required package member: ' + name
|
|
return archive.read(root + name)
|
|
|
|
checksums = {}
|
|
for line in read('SHA256SUMS.txt').decode('utf-8-sig').splitlines():
|
|
match = re.fullmatch(r'([0-9a-f]{64}) (.+)', line)
|
|
assert match, 'Malformed package checksum line'
|
|
digest, name = match.groups()
|
|
assert name not in checksums, 'Duplicate package checksum path'
|
|
checksums[name] = digest
|
|
assert set(files) == set(checksums) | {'SHA256SUMS.txt'}, 'Missing or unexpected package files'
|
|
assert 'SHA256SUMS.txt' not in checksums
|
|
assert all(files[name] == digest for name, digest in checksums.items()), 'Package file checksum differs'
|
|
metadata = json.loads(read('package.json'))
|
|
assert metadata['version'] == args.version and metadata['target'] == target
|
|
for name, digest in metadata['input_sha256'].items():
|
|
assert files.get(name) == digest, 'Package input differs: ' + name
|
|
for name, digest in expected_binaries.items():
|
|
assert files.get(name) == metadata['input_sha256'].get(name) == digest, 'Build artifact differs: ' + name
|
|
build = metadata['build_evidence']['evidence']
|
|
assert build['source_identity'] == args.source, 'Source assertion needs exact commit or explicit review'
|
|
assert build['profile'] == 'dev' and build['target'] == target and build['compiler']
|
|
for name in ('arcadia-studio.exe', 'arcadia-server.exe'):
|
|
assert build['binary_sha256'][name] == expected_binaries[name]
|
|
for name in ('README.md', 'START-HERE.txt', 'LICENSE', 'docs/windows-packaging.md',
|
|
'docs/preview-human-checks.md', 'THIRD-PARTY-NOTICES.txt', 'dependency-notices.json'):
|
|
read(name)
|
|
document_check = json.loads(read('documentation-check.json'))
|
|
assert 'checked_local_document_links' in document_check
|
|
notices = json.loads(read('dependency-notices.json'))
|
|
assert notices['target'] == target and notices['runtime']['sha256'] == args.runtime_sha256
|
|
documents = json.loads(args.documents_receipt.read_text(encoding='utf-8-sig'))
|
|
assert documents['result'] == 'PASS' and len(documents['files']) == documents['file_count']
|
|
for row in documents['files']:
|
|
assert files.get(row['package_path']) == row['sha256'], 'Reviewed document bytes differ: ' + row['package_path']
|
|
print(json.dumps({'archive': args.archive.name, 'archive_sha256': args.archive_sha256,
|
|
'version': args.version, 'source_assertion': args.source,
|
|
'package_files': len(files), 'uncompressed_bytes': sum(row.file_size for row in infos),
|
|
'verified_binary_sha256': expected_binaries,
|
|
'reviewed_document_files': len(documents['files']),
|
|
'document_links': document_check['checked_local_document_links'],
|
|
'compiler_assertion': build['compiler'], 'imports': metadata['imports'],
|
|
'scope': 'Archive integrity, supplied build hashes and reviewed document bytes; no binary execution or native acceptance.'}, indent=2))
|