initial commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""qpdf notice/package boundaries with a synthetic pin; real Ubuntu tests use dpkg-deb."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import collect_ubuntu_validation_runtime as collector
|
||||
import package_ubuntu as package
|
||||
import verify_ubuntu_package as verifier
|
||||
|
||||
|
||||
def digest(data):
|
||||
return {'sha256': hashlib.sha256(data).hexdigest(), 'size': len(data)}
|
||||
|
||||
|
||||
class QpdfNoticePackageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(); self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.library = b'synthetic reviewed qpdf runtime'
|
||||
self.texts = {'LICENSE.txt': b'synthetic license', 'NOTICE.md': b'synthetic notice'}
|
||||
self.pin = {'version': '12.4.1', 'archive': {'name': 'fixed.tar.gz', **digest(b'source archive')},
|
||||
'sourceFiles': 2, 'sourceBytes': 27, 'sourceInventorySha256': 'a' * 64,
|
||||
'library': digest(self.library), 'notices': {k: digest(v) for k, v in self.texts.items()}}
|
||||
self.summary = {k: copy.deepcopy(self.pin[k]) for k in ('version', 'archive', 'sourceFiles', 'sourceBytes', 'sourceInventorySha256')}
|
||||
self.summary.update(status='verified', sourceRoot='source-0:',
|
||||
library={'path': 'dependencies:lib/libqpdf.so.30', **self.pin['library']},
|
||||
notices=[{'source': k, **v, 'copiedPath': 'notices/' + v['sha256'] + '.txt'} for k, v in self.pin['notices'].items()])
|
||||
self.payload = {'opt/docview/lib/libqpdf.so.30': self.library}
|
||||
for row in self.summary['notices']:
|
||||
self.payload['opt/docview/share/doc/docview/third-party/runtime/' + row['copiedPath']] = self.texts[row['source']]
|
||||
self.patch = patch.object(collector, 'QPDF_NOTICE_PIN', self.pin); self.patch.start(); self.addCleanup(self.patch.stop)
|
||||
|
||||
def inputs(self, summary=None):
|
||||
summary = copy.deepcopy(self.summary if summary is None else summary)
|
||||
return ({name: digest(data) for name, data in self.payload.items()},
|
||||
{verifier.RUNTIME_RECORD: json.dumps({'qpdfNoticeCorrespondence': summary}).encode()},
|
||||
{'version': '0.1.0~validation5', 'qpdfNoticeCorrespondence': summary})
|
||||
|
||||
def test_correspondence_binds_runtime_and_both_notice_texts(self):
|
||||
self.assertEqual(verifier.verify_qpdf_payload(*self.inputs()), self.summary)
|
||||
|
||||
def test_missing_or_changed_notice_and_library_rejected_even_with_new_payload_hashes(self):
|
||||
original = copy.deepcopy(self.payload)
|
||||
for name in original:
|
||||
for mode in ('missing', 'changed'):
|
||||
with self.subTest(name=name, mode=mode):
|
||||
self.payload = dict(original)
|
||||
if mode == 'missing': del self.payload[name]
|
||||
else: self.payload[name] += b'changed'
|
||||
with self.assertRaises(ValueError): verifier.verify_qpdf_payload(*self.inputs())
|
||||
self.payload = original
|
||||
|
||||
def test_forged_source_identity_and_notice_claims_rejected(self):
|
||||
for key in ('archive', 'sourceFiles', 'sourceBytes', 'sourceInventorySha256', 'version'):
|
||||
with self.subTest(key=key):
|
||||
summary = copy.deepcopy(self.summary); summary[key] = 'forged'
|
||||
with self.assertRaises(ValueError): verifier.verify_qpdf_payload(*self.inputs(summary))
|
||||
for mutate in ('duplicate', 'escape', 'name'):
|
||||
summary = copy.deepcopy(self.summary)
|
||||
if mutate == 'duplicate': summary['notices'][1] = summary['notices'][0]
|
||||
elif mutate == 'escape': summary['notices'][0]['copiedPath'] = '../../NOTICE.md'
|
||||
else: summary['notices'][0]['source'] = 'OTHER'
|
||||
with self.assertRaises(ValueError): verifier.verify_qpdf_payload(*self.inputs(summary))
|
||||
|
||||
def test_correct_unused_alias_does_not_authorize_changed_loaded_soname(self):
|
||||
self.payload['opt/docview/lib/libqpdf.so.30.4.1'] = self.library
|
||||
summary = copy.deepcopy(self.summary)
|
||||
summary['library']['path'] = 'dependencies:lib/libqpdf.so.30.4.1'
|
||||
self.payload['opt/docview/lib/libqpdf.so.30'] = b'changed actual loader dependency'
|
||||
with self.assertRaisesRegex(ValueError, 'SONAME'): verifier.verify_qpdf_payload(*self.inputs(summary))
|
||||
|
||||
def test_runtime_record_cannot_disagree_with_summary(self):
|
||||
data, metadata, manifest = self.inputs(); metadata[verifier.RUNTIME_RECORD] = b'{}'
|
||||
with self.assertRaisesRegex(ValueError, 'differs'): verifier.verify_qpdf_payload(data, metadata, manifest)
|
||||
|
||||
def test_all_versions_require_correspondence_for_current_verification(self):
|
||||
for version in ('0.1.0~validation5', '0.1.0~validation4', 'forged'):
|
||||
with self.subTest(version=version), self.assertRaises(ValueError):
|
||||
verifier.verify_qpdf_payload({}, {}, {'version': version})
|
||||
|
||||
def test_package_passes_archive_to_collector(self):
|
||||
class ReachedCollector(Exception): pass
|
||||
args = SimpleNamespace(version='0.1.0~validation5', output=self.root/'new-package',
|
||||
prefix=self.root/'install', qtpaths=self.root/'qtpaths', dependency_prefix=self.root/'deps',
|
||||
source_root=[], qpdf_source_archive=self.root/'fixed.tar.gz')
|
||||
with patch.object(package.platform, 'freedesktop_os_release', return_value={'ID':'ubuntu','VERSION_ID':'24.04'}), \
|
||||
patch.object(package.subprocess, 'check_output', return_value='amd64\n'), \
|
||||
patch.object(package, 'Collector', side_effect=ReachedCollector) as construct:
|
||||
with self.assertRaises(ReachedCollector): package.create(args)
|
||||
self.assertEqual(construct.call_args.kwargs['qpdf_source_archive'], args.qpdf_source_archive)
|
||||
args.version = '0.1.0~unreviewed'
|
||||
with self.assertRaisesRegex(ValueError, 'Unsupported'): package.create(args)
|
||||
|
||||
def test_cli_accepts_fixed_new_version_and_archive(self):
|
||||
args = ['package_ubuntu.py', '--prefix','p','--qtpaths','q','--dependency-prefix','d', '--input-manifest','i',
|
||||
'--sdk-notices','n','--sdk-supplement','s','--qt-licenses','l','--output','o',
|
||||
'--version','0.1.0~validation5','--qpdf-source-archive','fixed.tar.gz']
|
||||
with patch.object(sys, 'argv', args), patch.object(package, 'create') as create:
|
||||
package.main()
|
||||
self.assertEqual(create.call_args.args[0].qpdf_source_archive, Path('fixed.tar.gz'))
|
||||
self.assertEqual(create.call_args.args[0].version, '0.1.0~validation5')
|
||||
|
||||
@unittest.skipUnless(shutil.which('dpkg-deb'), 'dpkg-deb is required')
|
||||
def test_real_deb_rejects_omitted_notice_with_self_consistent_manifest(self):
|
||||
stage = self.root/'stage'; stage.mkdir()
|
||||
def make(manifest_version='0.1.0~validation5', control_version='0.1.0~validation5', summary=True):
|
||||
for name, data in self.payload.items():
|
||||
p=stage/name; p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data);p.chmod(0o644)
|
||||
record=stage/'opt/docview'/verifier.RUNTIME_RECORD; record.parent.mkdir(parents=True,exist_ok=True)
|
||||
record.write_text(json.dumps({'qpdfNoticeCorrespondence': self.summary}));record.chmod(0o644)
|
||||
control=stage/'DEBIAN/control';control.parent.mkdir(exist_ok=True)
|
||||
control.write_text('Package: docview\nVersion: ' + control_version + '\nArchitecture: amd64\nMaintainer: Fixture\nDescription: qpdf notice boundary\n');control.chmod(0o644)
|
||||
rows=[{'path':p.relative_to(stage).as_posix(), **digest(p.read_bytes()), 'mode':'0o644'} for p in sorted(stage.rglob('*')) if p.is_file() and p.relative_to(stage).as_posix()!=package.MANIFEST]
|
||||
manifest=stage/package.MANIFEST;manifest.write_text(json.dumps({'schemaVersion':1,'package':'docview','version':manifest_version,'files':rows, **({'qpdfNoticeCorrespondence':self.summary} if summary else {})}));manifest.chmod(0o644)
|
||||
archive=self.root/'fixture.deb';subprocess.run(['dpkg-deb','--root-owner-group','-Znone','--build',str(stage),str(archive)],capture_output=True,check=True,timeout=30)
|
||||
return archive
|
||||
with patch.object(verifier, 'verify_pdfium_payload', return_value={'kind':'original-provider'}):
|
||||
self.assertEqual(verifier.verify(make())['qpdfNoticeCorrespondence'],self.summary)
|
||||
with self.assertRaisesRegex(ValueError, 'control identity'):
|
||||
verifier.verify(make(manifest_version='0.1.0~validation4'))
|
||||
with self.assertRaisesRegex(ValueError, 'Missing verified'):
|
||||
verifier.verify(make(manifest_version='0.1.0~validation4', control_version='0.1.0~validation4', summary=False))
|
||||
missing=next(name for name in self.payload if digest(self.texts['NOTICE.md'])['sha256'] in name)
|
||||
del self.payload[missing];(stage/missing).unlink()
|
||||
with self.assertRaisesRegex(ValueError,'absent or changed'):verifier.verify(make())
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Reference in New Issue
Block a user