initial commit
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the one reviewed Linux candidate, or the unchanged provider install.
|
||||
|
||||
The reviewed-v2 lock is a repository input, never supplied by the package being
|
||||
checked. An absent lock deliberately prevents candidate configuration/packaging.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from stage_pdfium_candidate import Tree, digest, json_bytes, relative, require
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCK = 'cmake/pdfium-candidate-v2.lock.json'
|
||||
DOC = 'share/doc/docview/pdfium/'
|
||||
CANDIDATE = DOC + 'candidate/'
|
||||
SUPPLEMENT = DOC + 'supplemental/'
|
||||
MAX_METADATA = 4 * 1024 * 1024
|
||||
MAX_TOTAL = 128 * 1024 * 1024
|
||||
METADATA_PATHS = (CANDIDATE + 'BUILDINFO.json', SUPPLEMENT + 'sources.json')
|
||||
|
||||
|
||||
def root_path(root):
|
||||
return Path(root) if root is not None else ROOT
|
||||
|
||||
|
||||
def source_metadata(root=None):
|
||||
with Tree(root_path(root)) as tree:
|
||||
raw = tree.read('resources/licenses/pdfium-supplemental/sources.json', limit=65536)
|
||||
source = json_bytes(raw)
|
||||
upstream = json_bytes(tree.read('cmake/pdfium.lock.json', limit=65536))
|
||||
require(source['pdfiumVersion'] == upstream['version'] and
|
||||
source['pdfiumUpstreamCommit'] == upstream['upstreamCommit'],
|
||||
'Provider source pin differs from supplemental notices')
|
||||
return raw, source
|
||||
|
||||
|
||||
def reviewed_lock(root=None):
|
||||
with Tree(root_path(root)) as tree:
|
||||
try:
|
||||
raw = tree.read(LOCK, limit=65536)
|
||||
except FileNotFoundError as error:
|
||||
raise ValueError('The reviewed-v2 PDFium lock is not available; candidate rejected') from error
|
||||
lock = json_bytes(raw)
|
||||
hashes = ('librarySha256', 'buildInfoSha256', 'anchorRecordSha256', 'patchSha256',
|
||||
'gnArgsSha256', 'supplementalSourceSha256')
|
||||
require(set(lock) == {'schemaVersion', 'candidateId', 'sourceRevision', 'anchorRecordPath', *hashes}
|
||||
and lock['schemaVersion'] == 1 and lock['candidateId'] == 'reviewed-v2',
|
||||
'Unexpected reviewed PDFium candidate lock')
|
||||
for field in hashes:
|
||||
require(isinstance(lock[field], str) and re.fullmatch('[0-9a-f]{64}', lock[field]),
|
||||
'Invalid candidate digest: ' + field)
|
||||
require(isinstance(lock['sourceRevision'], str) and
|
||||
re.fullmatch('[0-9a-f]{40}', lock['sourceRevision']), 'Invalid candidate revision')
|
||||
relative(lock['anchorRecordPath'])
|
||||
require(re.fullmatch(r'provenance/[A-Za-z0-9_-]+[.]json', lock['anchorRecordPath']),
|
||||
'Invalid candidate anchor path')
|
||||
base_raw, base = source_metadata(root)
|
||||
require(lock['sourceRevision'] == base['pdfiumUpstreamCommit'] and
|
||||
lock['supplementalSourceSha256'] == digest(base_raw),
|
||||
'Candidate notices do not match the reviewed source pin')
|
||||
return lock
|
||||
|
||||
|
||||
def buildinfo(raw, lock):
|
||||
require(len(raw) <= MAX_METADATA and digest(raw) == lock['buildInfoSha256'],
|
||||
'Candidate BUILDINFO differs from reviewed-v2')
|
||||
info = json_bytes(raw)
|
||||
require(info['schemaVersion'] == 1 and info['candidateOnly'] is True and
|
||||
info['sourceRevision'] == lock['sourceRevision'] and
|
||||
info['librarySha256'] == lock['librarySha256'] and
|
||||
info['patchSha256'] == lock['patchSha256'] and
|
||||
info['gnArgsSha256'] == lock['gnArgsSha256'], 'Candidate BUILDINFO identity mismatch')
|
||||
files = info['files']
|
||||
require(isinstance(files, dict) and 1 <= len(files) <= 256, 'Invalid candidate file list')
|
||||
total = 0
|
||||
for name, row in files.items():
|
||||
relative(name)
|
||||
require(re.fullmatch(r'[A-Za-z0-9_./+-]+', name) and name != 'BUILDINFO.json' and
|
||||
(name in ('lib/libpdfium.so', 'LICENSE', 'VERSION', 'args.gn') or
|
||||
name.startswith(('include/', 'licenses/', 'provenance/'))),
|
||||
'Unexpected candidate file path')
|
||||
require(isinstance(row, dict) and isinstance(row.get('sha256'), str) and
|
||||
re.fullmatch('[0-9a-f]{64}', row['sha256']) and type(row.get('bytes')) is int and
|
||||
0 <= row['bytes'] <= MAX_TOTAL, 'Invalid candidate file identity')
|
||||
total += row['bytes']
|
||||
require(total <= MAX_TOTAL and 'include/fpdfview.h' in files and 'LICENSE' in files and
|
||||
'VERSION' in files, 'Incomplete or oversized candidate prefix')
|
||||
for path, expected in {
|
||||
'lib/libpdfium.so': lock['librarySha256'], 'args.gn': lock['gnArgsSha256'],
|
||||
lock['anchorRecordPath']: lock['anchorRecordSha256'],
|
||||
'provenance/rendering-intent.patch': lock['patchSha256'],
|
||||
'provenance/supplemental-notices.json': lock['supplementalSourceSha256']}.items():
|
||||
require(files.get(path, {}).get('sha256') == expected, 'Missing candidate correspondence: ' + path)
|
||||
return info
|
||||
|
||||
|
||||
def installed_path(name):
|
||||
relative(name)
|
||||
if name == 'lib/libpdfium.so':
|
||||
return name
|
||||
if name == 'LICENSE' or name.startswith('licenses/'):
|
||||
return DOC + name
|
||||
return CANDIDATE + name
|
||||
|
||||
|
||||
def candidate_supplement(lock, base_raw):
|
||||
value = json_bytes(base_raw)
|
||||
value['pdfiumLibrarySha256'] = lock['librarySha256']
|
||||
value['scope'] = ('Fixed-source supplemental notices for the reviewed-v2 PDFium candidate. '
|
||||
'The provider source notice record is retained unchanged in candidate/provenance. '
|
||||
'This is not human rendering approval or a complete license assessment.')
|
||||
value['candidateCorrespondence'] = {key: lock[key] for key in
|
||||
('candidateId', 'buildInfoSha256', 'anchorRecordSha256', 'patchSha256', 'supplementalSourceSha256')}
|
||||
return (json.dumps(value, indent=2, sort_keys=True) + '\n').encode()
|
||||
|
||||
|
||||
def verify_rows(raw, rows, lock, installed=False):
|
||||
info = buildinfo(raw, lock)
|
||||
expected = {}
|
||||
for name, entry in info['files'].items():
|
||||
path = installed_path(name) if installed else name
|
||||
expected[path] = {'sha256': entry['sha256'], 'size': entry['bytes']}
|
||||
expected[(CANDIDATE if installed else '') + 'BUILDINFO.json'] = {
|
||||
'sha256': lock['buildInfoSha256'], 'size': len(raw)}
|
||||
for name, entry in expected.items():
|
||||
actual = rows.get(name, {})
|
||||
require(all(actual.get(field) == value for field, value in entry.items()),
|
||||
'Candidate file missing or changed: ' + name)
|
||||
if installed:
|
||||
require({p for p in rows if p.startswith(CANDIDATE)} ==
|
||||
{p for p in expected if p.startswith(CANDIDATE)}, 'Unregistered installed candidate metadata')
|
||||
else:
|
||||
require(set(rows) == set(expected), 'Unregistered candidate prefix file')
|
||||
return info
|
||||
|
||||
|
||||
def verify_payload(rows, contents, root=None):
|
||||
"""Check hashed files plus bounded metadata read from an install or a deb."""
|
||||
base_raw, base = source_metadata(root)
|
||||
library_hash = rows.get('lib/libpdfium.so', {}).get('sha256')
|
||||
is_candidate = any(name.startswith(CANDIDATE) for name in rows)
|
||||
if is_candidate:
|
||||
lock = reviewed_lock(root)
|
||||
verify_rows(contents.get(CANDIDATE + 'BUILDINFO.json', b''), rows, lock, installed=True)
|
||||
require(library_hash == lock['librarySha256'], 'Installed candidate library differs from reviewed-v2')
|
||||
expected_raw = candidate_supplement(lock, base_raw)
|
||||
identity = {'kind': 'reviewed-candidate', 'candidateId': lock['candidateId'],
|
||||
'buildInfoSha256': lock['buildInfoSha256'],
|
||||
'anchorRecordSha256': lock['anchorRecordSha256'], 'patchSha256': lock['patchSha256']}
|
||||
else:
|
||||
require(library_hash == base['pdfiumLibrarySha256'],
|
||||
'Unknown PDFium library or missing candidate provenance')
|
||||
expected_raw = base_raw
|
||||
identity = {'kind': 'original-provider'}
|
||||
require(contents.get(SUPPLEMENT + 'sources.json') == expected_raw,
|
||||
'Installed supplemental notice correspondence differs from reviewed input')
|
||||
require(len(base['files']) == 2 and {row['name'] for row in base['files']} ==
|
||||
{'libcxx-LICENSE.txt', 'libcxxabi-LICENSE.txt'}, 'Unexpected supplemental notice set')
|
||||
for row in base['files']:
|
||||
item = rows.get(SUPPLEMENT + row['name'], {})
|
||||
require(item.get('sha256') == row['sha256'] and item.get('size') == row['size'],
|
||||
'Installed supplemental notice text differs from reviewed input')
|
||||
if is_candidate:
|
||||
candidate_item = rows.get(DOC + 'licenses/' + row['name'], {})
|
||||
require(candidate_item.get('sha256') == row['sha256'] and candidate_item.get('size') == row['size'],
|
||||
'Candidate notice copy differs from reviewed source')
|
||||
return {**identity, 'librarySha256': library_hash, 'sourceRevision': base['pdfiumUpstreamCommit'],
|
||||
'supplementalManifestSha256': digest(expected_raw)}
|
||||
|
||||
|
||||
def files_beneath(path):
|
||||
"""Finite list without following any directory or file links."""
|
||||
pending, files, count = [Path(path)], [], 0
|
||||
while pending:
|
||||
directory = pending.pop()
|
||||
with os.scandir(directory) as entries:
|
||||
for entry in entries:
|
||||
count += 1
|
||||
require(count <= 512, 'Candidate file count exceeds limit')
|
||||
require(not entry.is_symlink(), 'Candidate links are not accepted')
|
||||
if entry.is_dir(follow_symlinks=False): pending.append(Path(entry.path))
|
||||
else:
|
||||
require(entry.is_file(follow_symlinks=False), 'Candidate special file rejected')
|
||||
files.append(Path(entry.path).relative_to(path).as_posix())
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def verify_prefix(prefix, library, include_dir, root=None):
|
||||
prefix = Path(os.path.abspath(prefix))
|
||||
require(Path(os.path.abspath(library)) == prefix / 'lib/libpdfium.so' and
|
||||
Path(os.path.abspath(include_dir)) == prefix / 'include',
|
||||
'CMake PDFium library/headers differ from the selected candidate prefix')
|
||||
lock = reviewed_lock(root)
|
||||
rows = {}
|
||||
with Tree(prefix) as tree:
|
||||
raw = tree.read('BUILDINFO.json', limit=MAX_METADATA)
|
||||
info = buildinfo(raw, lock)
|
||||
names = files_beneath(prefix)
|
||||
require(set(names) == set(info['files']) | {'BUILDINFO.json'}, 'Unregistered candidate prefix file')
|
||||
for name in names:
|
||||
data = tree.read(name, limit=MAX_TOTAL)
|
||||
rows[name] = {'sha256': digest(data), 'size': len(data)}
|
||||
verify_rows(raw, rows, lock)
|
||||
base_raw, base = source_metadata(root)
|
||||
for row in base['files']:
|
||||
require(rows.get('licenses/' + row['name']) == {'sha256': row['sha256'], 'size': row['size']},
|
||||
'Candidate supplemental notice differs from fixed source')
|
||||
return {'candidateId': lock['candidateId'], 'librarySha256': lock['librarySha256'],
|
||||
'buildInfoSha256': lock['buildInfoSha256'],
|
||||
'installFiles': [{'source': name, 'destination': installed_path(name)}
|
||||
for name in sorted(info['files'])
|
||||
if name != 'lib/libpdfium.so' and name != 'LICENSE' and not name.startswith('licenses/')]
|
||||
+ [{'source': 'BUILDINFO.json', 'destination': CANDIDATE + 'BUILDINFO.json'}]}, \
|
||||
candidate_supplement(lock, base_raw)
|
||||
|
||||
|
||||
def verify_installed(prefix, root=None):
|
||||
prefix = Path(os.path.abspath(prefix))
|
||||
rows, contents, total = {}, {}, 0
|
||||
with Tree(prefix) as tree:
|
||||
paths = ['lib/libpdfium.so'] + [DOC + name for name in files_beneath(prefix / DOC)]
|
||||
for name in paths:
|
||||
data = tree.read(name, limit=MAX_TOTAL)
|
||||
total += len(data)
|
||||
require(total <= MAX_TOTAL, 'Installed PDFium correspondence exceeds limit')
|
||||
rows[name] = {'sha256': digest(data), 'size': len(data)}
|
||||
if name in METADATA_PATHS:
|
||||
require(len(data) <= MAX_METADATA, 'Installed PDFium metadata exceeds limit')
|
||||
contents[name] = data
|
||||
return verify_payload(rows, contents, root)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--prefix', type=Path, required=True)
|
||||
parser.add_argument('--library', type=Path, required=True)
|
||||
parser.add_argument('--include-dir', type=Path, required=True)
|
||||
parser.add_argument('--notice-output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
report, supplemental = verify_prefix(args.prefix, args.library, args.include_dir)
|
||||
# Only CMake's own generated metadata is written, after all inputs pass.
|
||||
args.notice_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.notice_output.write_bytes(supplemental)
|
||||
print(json.dumps(report))
|
||||
return 0
|
||||
except (ValueError, OSError, KeyError, TypeError) as error:
|
||||
print('PDFium candidate rejected: ' + str(error), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user