Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions .github/workflows/publish-current-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,20 @@ jobs:
PY
cc -O2 -Wall -Wextra -Werror \
-I"$evidence/brotli-headers" \
source/server/security/ubuntu-backports/bundle/installer/testdata/consumer-brotli-probe.c \
source/server/artifacts/brotli-runtime-probe.c \
-ldl -o "$evidence/brotli-runtime-probe"
# brotli-runtime-probe.c reuses consumer-brotli-probe.c unchanged.
mkdir -p "$evidence/brotli-official"
docker run --rm --network none --entrypoint /bin/sh "$CANDIDATE_IMAGE" \
/usr/local/libexec/pasturestack/verify-brotli-runtime.sh \
> "$evidence/brotli-official/runtime.json"
docker run --rm --network none \
--mount "type=bind,source=$evidence/brotli-runtime-probe,target=/tmp/brotli-runtime-probe,readonly" \
--entrypoint /tmp/brotli-runtime-probe "$CANDIDATE_IMAGE" |
tee "$evidence/brotli-runtime.txt"
grep -Fx 'PASS: three installed Brotli SONAMEs RTLD_NOW, ordinary compression/decompression' \
"$evidence/brotli-runtime.txt"
grep -Fx 'PASS: truncated ordinary input rejected' "$evidence/brotli-runtime.txt"

- name: Start, restart, and export the candidate
shell: bash
Expand Down Expand Up @@ -636,13 +642,43 @@ jobs:
(cd "$evidence/bundle-tools" && \
test "$(grep -Ec '^[0-9a-f]{64} verify-consumer-evidence\.py$' SHA256SUMS)" -eq 1 && \
grep -E '^[0-9a-f]{64} verify-consumer-evidence\.py$' SHA256SUMS | sha256sum --check -)
python3 "$evidence/bundle-tools/verify-consumer-evidence.py" \
# Keep the immutable helper and seven original evidence files intact.
# The Server wrapper admits one fully checked official replacement;
# all other bundle packages retain the original exact checks.
cp "$RUNNER_TEMP/server-rootfs/usr/share/pasturestack/security/brotli-official/lock.json" \
"$evidence/brotli-official/lock.json"
cmp source/server/security/official-brotli.lock.json "$evidence/brotli-official/lock.json"
mkdir -p "$evidence/brotli-official/signed-index"
cp source/server/security/official-brotli-evidence/InRelease \
source/server/security/official-brotli-evidence/Packages.xz "$evidence/brotli-official/signed-index/"
python3 - "$evidence/brotli-official" <<'PY'
import hashlib, json, pathlib, sys, urllib.request
directory = pathlib.Path(sys.argv[1])
lock = json.loads((directory / 'lock.json').read_bytes())
assert lock['archiveUrl'] == 'https://security.ubuntu.com/ubuntu/pool/main/b/brotli/libbrotli1_1.2.0-3ubuntu0.1_amd64.deb'
with urllib.request.urlopen(lock['archiveUrl'], timeout=60) as response:
assert response.url == lock['archiveUrl']
data = response.read(lock['archiveBytes'] + 1)
assert len(data) == lock['archiveBytes'] and hashlib.sha256(data).hexdigest() == lock['archiveSha256']
(directory / 'libbrotli1.deb').write_bytes(data)
PY
(cd "$evidence/brotli-official" && \
python3 "$GITHUB_WORKSPACE/source/scripts/test-server-official-brotli.py")
python3 source/server/security/verify-server-backport-consumer.py \
--evidence-dir "$evidence/ubuntu-backports" --sbom "$evidence/server.cdx.json" \
--expected-bundle-digest "${bundle_image##*@}" \
--bundle-checksums "$evidence/bundle-tools/SHA256SUMS" \
--legacy-helper "$evidence/bundle-tools/verify-consumer-evidence.py" \
--rootfs "$RUNNER_TEMP/server-rootfs" --official-brotli-dir "$evidence/brotli-official" \
>"$evidence/ubuntu-backports-validation.json"
mkdir -p "$evidence/server-consumer-tools/ubuntu-backports/bundle"
cp source/server/security/verify-server-backport-consumer.py "$evidence/server-consumer-tools/"
cp source/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py \
"$evidence/server-consumer-tools/ubuntu-backports/bundle/"
(cd "$evidence/server-consumer-tools" && sha256sum verify-server-backport-consumer.py \
ubuntu-backports/bundle/verify-consumer-evidence.py > SHA256SUMS)
tar -cf "$evidence/ubuntu-backports-evidence.tar" -C "$evidence" \
ubuntu-backports bundle-tools ubuntu-backports-validation.json
ubuntu-backports bundle-tools ubuntu-backports-validation.json brotli-official server-consumer-tools

docker run --rm \
-v "$RUNNER_TEMP/server-rootfs:/rootfs:ro" \
Expand Down
3 changes: 3 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ The candidate uses Web Console 1.6.200/optional bridge 0.2.2, Engine 0.183.359,
Catalog 0.20.17 and Compose 0.14.40 from qualified original CI/public bytes.
See the [candidate note](docs/releases/server-1.6.559.md) for exact source,
archive/binary/SBOM/lock identities and four independent opt-ins.
The candidate prefers the official Ubuntu Brotli safety package; exact final
rootfs/SBOM validation preserves the immutable earlier bundle proof and all other
package checks. No new VEX exemption or weaker scan threshold is introduced.
Core200, Catalog/Archive, affected roles/layouts/weak-model checks, three
Hosts/Agents and an actual restart remain pending. Existing performance,
origin, volume and rollback rules remain; this is not full frontend coverage.
Expand Down
15 changes: 15 additions & 0 deletions docs/releases/server-1.6.559.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ No unused ARG is invented to imply an additional consumer check.

## Included behavior and boundaries

Server559 adopts Canonical Ubuntu 26.04 `libbrotli1 1.2.0-3ubuntu0.1`
after complete immutable-bundle verification. The earlier backport retains its
reviewed upstream-fix evidence; this switch follows the official-package
preference and does not assert that the earlier backport remained vulnerable.
The final official deb, signed index, dpkg identity, three SO hashes and native
controls are verified separately against actual rootfs/SBOM bytes. Original
bundle/helper/evidence bytes remain intact; other package checks stay exact.


The Compose OpenPGP applicability decision is bound to the reviewed 0.14.40
binary above. Its qualified source gate rejects OpenPGP dependencies; inspection
of the shipped Go function and inline-name table confirms absence of the affected
package while retaining positive maintained-crypto controls. Only this exact
consumer hash is refreshed; target/module scope and all severity gates stay.

The assembly includes opt-in typed Archive/restore and the first bounded
Catalog-to-new-Stack workflow with matching Catalog 0.20.17/Compose 0.14.40 owners.
Public tools never receive private Compose, question defaults, human secrets or
Expand Down
23 changes: 22 additions & 1 deletion scripts/test-server-backport-consumer-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,20 @@ def verify(files):
'Server absent-package boundary is missing')
require('install -m 0644 pcre2-backport-provenance.json pcre2-backport.lock.json pcre2-runtime.sha256 /usr/share/pasturestack/security/' in runtime,
'Legacy verifier metadata is not installed')
for marker in ["libbrotli1)\" = 1.2.0-3build1+pasturestack.1", 'dpkg --verify libbrotli1',
for marker in ['dpkg --verify libbrotli1',
'libbrotlicommon.so.1 libbrotlidec.so.1 libbrotlienc.so.1']:
require(marker in runtime and marker in build, 'Missing final-rootfs Brotli version/integrity/linkage: ' + marker)
require('libbrotli1)" = "${BROTLI_PACKAGE_VERSION}"' in runtime
and 'libbrotli1)" = 1.2.0-3ubuntu0.1' in build,
'Final Brotli oracles must select the official version')
require('verify-brotli-runtime.sh --install' in runtime, 'Official replacement installation is missing')
require(runtime.index('"${data}/install-existing-backports" --verify-installed')
< runtime.index('verify-brotli-runtime.sh --install'), 'Official replacement must follow complete bundle verification')
require('official-brotli/libbrotli1.deb' in native and 'sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8244606' in native,
'Official Brotli archive must be separately checksum-pinned, outside the early package batch')
require('verify-server-backport-consumer.py' in publisher and '--legacy-helper' in publisher
and '--official-brotli-dir' in publisher and '--rootfs' in publisher,
'Actual final official replacement closure is missing')
require('brotli_linkage="$(ldd -r ' in build and 'require_resolved_linkage "$brotli_linkage"' in build,
'Brotli ldd failure must propagate before the linkage text check')
require(all('brotli_integrity="$(dpkg --verify libbrotli1)"' in source
Expand Down Expand Up @@ -99,6 +110,16 @@ class ServerBackportContract(unittest.TestCase):
def test_current_source_contract(self):
verify(FILES)

def test_official_replacement_pin_order_and_actual_rootfs_closure_are_required(self):
for name, old, new in [
(DOCKER, 'sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8244606', 'sha256:' + 'a' * 64),
(DOCKER, 'verify-brotli-runtime.sh --install', 'verify-brotli-runtime.sh --skip'),
(BUILD, 'libbrotli1)" = 1.2.0-3ubuntu0.1', 'libbrotli1)" = 1.2.0-3build1+pasturestack.1'),
(PUBLISH, '--rootfs', '--missing-final-rootfs'),
(PUBLISH, '--legacy-helper', '--unbound-helper')]:
with self.subTest(name=name, old=old), self.assertRaises(AssertionError):
verify(dict(FILES, **{name: FILES[name].replace(old, new)}))

def test_source_compile_or_absent_package_boundary_regression_is_rejected(self):
for old, new in [('target=/bundle,ro', 'target=/bundle,ro # build-pcre2-backport.py'),
('for package in rust-coreutils jq libjq1 libc6-dev', 'for package in libc6-dev')]:
Expand Down
144 changes: 144 additions & 0 deletions scripts/test-server-official-brotli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Only the new exact official replacement seam; a real checksum-pinned deb is required."""
import copy
import importlib.util
import json
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
BUNDLE = ROOT / 'server/security/ubuntu-backports/bundle'
spec = importlib.util.spec_from_file_location('existing_consumer_fixture', BUNDLE / 'test_verify_consumer_evidence.py')
fixture = importlib.util.module_from_spec(spec)
spec.loader.exec_module(fixture)
guard = fixture.guard


class OfficialBrotliTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='official-brotli-seam-')
self.addCleanup(self.temp.cleanup)
self.work = Path(self.temp.name)
self.root = self.work / 'rootfs'
self.deb = (Path.cwd() / 'libbrotli1.deb').resolve(strict=True)
self.lock = ROOT / 'server/security/official-brotli.lock.json'
lock = json.loads(self.lock.read_bytes())
self.assertEqual(guard.sha(self.deb.read_bytes()), lock['archiveSha256'])
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
subprocess.run(['dpkg-deb', '-x', str(self.deb), str(self.root)], check=True)
control = self.work / 'control'
subprocess.run(['dpkg-deb', '-e', str(self.deb), str(control)], check=True)
info = self.root / 'var/lib/dpkg/info'
info.mkdir(parents=True)
shutil.copyfile(control / 'md5sums', info / 'libbrotli1:amd64.md5sums')
(info.parent / 'status').write_text('Package: libbrotli1\nStatus: install ok installed\n'
'Source: brotli\nVersion: 1.2.0-3ubuntu0.1\nArchitecture: amd64\nDepends: libc6 (>= 2.29)\n\n')
history = self.root / 'usr/share/pasturestack/security/brotli-official'
history.mkdir(parents=True)
(history / 'install.json').write_text(json.dumps({'schemaVersion': 1, 'status': 'installed-and-verified',
'beforeVersion': lock['previousVersion'], 'version': lock['version'],
'packageSetUnchanged': True, 'dpkgConfigurationUnchanged': True}))
self.receipt = self.work / 'runtime.json'
self.receipt.write_text(json.dumps({'schemaVersion': 1, 'status': 'verified', 'package': 'libbrotli1',
'source': 'brotli', 'version': lock['version'], 'architecture': 'amd64', 'archiveSha256': lock['archiveSha256'],
'dpkgVerify': 'PASS', 'linkage': 'PASS', 'sonames': {Path(p).name: lock['payloadSha256'][
(Path(p).parent / target).as_posix()] for p, target in lock['symlinks'].items()}}))
self.proof = {'rootfs': self.root, 'lock': self.lock, 'receipt': self.receipt, 'deb': self.deb,
'signedIndex': ROOT / 'server/security/official-brotli-evidence'}
self.files, self.sbom = fixture.fixture()
manifest = json.loads(self.files['manifest.json'])
for family in manifest['families']:
if family['name'] == 'brotli': family['version'] = lock['previousVersion']
for package in manifest['packages']:
if package['name'] == 'libbrotli1': package['version'] = lock['previousVersion']
self.files['manifest.json'] = fixture.encoded(manifest)
fixture.change(self.files, 'openvex.json', lambda value: value['statements'][0].update(
products=[{'@id': guard.package_purl(p)} for p in manifest['packages']]))
names = sorted(p['name'] for p in manifest['packages'])
for name in ('install.json', 'verify.json'):
fixture.change(self.files, name, lambda value: value.update(packages=names))
self.old_sbom = {'bomFormat': 'CycloneDX', 'components': [{'name': p['name'], 'version': p['version'],
'purl': guard.package_purl(p)} for p in manifest['packages']]}
self.sbom = copy.deepcopy(self.old_sbom)
package = next(p for p in self.sbom['components'] if p['name'] == 'libbrotli1')
package.update(version=lock['version'], purl=guard.package_purl({'name': 'libbrotli1', 'version': lock['version']}))

def verify(self, sbom=None, proof=None, files=None, producer=None):
files = self.files if files is None else files
return guard.verify(files, fixture.checksums(files), self.sbom if sbom is None else sbom,
fixture.DIGEST, fixture.producer_checksums(files) if producer is None else producer,
official_brotli=self.proof if proof is None else proof)

def test_exact_real_deb_payload_and_final_sbom_pass_without_mutation(self):
original = copy.deepcopy((self.files, self.sbom))
result = self.verify()
self.assertEqual(len(result['selectedPackages']), 13)
self.assertEqual(result['officialReplacements'][0]['version'], '1.2.0-3ubuntu0.1')
self.assertEqual((self.files, self.sbom), original)

def test_boolean_incomplete_changed_lock_or_changed_deb_is_rejected(self):
for proof in (True, False, {}, {'rootfs': self.root}):
with self.subTest(proof=proof), self.assertRaises(ValueError): self.verify(proof=proof)
changed = self.work / 'changed-lock.json'
changed.write_bytes(self.lock.read_bytes() + b' ')
with self.assertRaises(ValueError): self.verify(proof={**self.proof, 'lock': changed})
changed = self.work / 'changed.deb'
changed.write_bytes(b'not the official deb')
with self.assertRaises(ValueError): self.verify(proof={**self.proof, 'deb': changed})
changed = self.work / 'changed-index'
shutil.copytree(self.proof['signedIndex'], changed)
(changed / 'InRelease').write_bytes((changed / 'InRelease').read_bytes() + b' ')
with self.assertRaises(ValueError): self.verify(proof={**self.proof, 'signedIndex': changed})

def test_each_library_dpkg_state_or_final_receipt_tamper_is_rejected(self):
lock = json.loads(self.lock.read_bytes())
for relative in lock['payloadSha256']:
path = self.root / relative
original = path.read_bytes()
path.write_bytes(original + b'corrupt')
with self.subTest(path=relative), self.assertRaises(ValueError): self.verify()
path.write_bytes(original)
for path in (self.root / 'var/lib/dpkg/status', self.root / 'var/lib/dpkg/info/libbrotli1:amd64.md5sums', self.receipt):
original = path.read_bytes()
path.write_bytes(b'{}')
with self.subTest(path=path.name), self.assertRaises(ValueError): self.verify()
path.write_bytes(original)

def test_symlink_escape_unverified_install_and_receipt_boolean_are_rejected(self):
path = self.root / next(iter(json.loads(self.lock.read_bytes())['symlinks']))
original = path.readlink()
path.unlink(); path.symlink_to('/etc/passwd')
with self.assertRaises(ValueError): self.verify()
path.unlink(); path.symlink_to(original)
install = self.root / 'usr/share/pasturestack/security/brotli-official/install.json'
value = json.loads(install.read_text()); value['packageSetUnchanged'] = False
install.write_text(json.dumps(value))
with self.assertRaises(ValueError): self.verify()
self.receipt.write_text('true')
with self.assertRaises(ValueError): self.verify()

def test_other_twelve_package_versions_and_official_scope_remain_exact(self):
for index, package in enumerate(self.sbom['components']):
changed = copy.deepcopy(self.sbom)
changed['components'][index]['version'] = 'wrong-version'
with self.subTest(package=package['name']), self.assertRaises(ValueError): self.verify(sbom=changed)
changed = copy.deepcopy(self.sbom)
changed['components'].append(copy.deepcopy(next(p for p in changed['components'] if p['name'] == 'libbrotli1')))
with self.assertRaises(ValueError): self.verify(sbom=changed)
changed = copy.deepcopy(self.sbom)
next(p for p in changed['components'] if p['name'] == 'libbrotli1')['purl'] = 'pkg:deb/ubuntu/libbrotli1@1.2.0-3ubuntu0.1?arch=arm64&distro=ubuntu-26.04'
with self.assertRaises(ValueError): self.verify(sbom=changed)

def test_default_producer_and_original_seven_file_binding_are_unchanged(self):
result = guard.verify(self.files, fixture.checksums(self.files), self.old_sbom, fixture.DIGEST, fixture.producer_checksums(self.files))
self.assertNotIn('officialReplacements', result)
with self.assertRaises(ValueError):
guard.verify(self.files, fixture.checksums(self.files), self.sbom, fixture.DIGEST, fixture.producer_checksums(self.files))
original = fixture.producer_checksums(self.files)
changed = copy.deepcopy(self.files)
fixture.change(changed, 'manifest.json', lambda value: value['packages'][0]['archive'].update(sha256='f'*64))
with self.assertRaisesRegex(ValueError, 'immutable OCI producer'): self.verify(files=changed, producer=original)


if __name__ == '__main__': unittest.main(verbosity=2)
Loading