Revoke scoped credential policies before package downgrade
This commit is contained in:
1 parent
bf79a79342
commit
5682508644
8 files changed
+138
-17
No files matched your search
@@ -30,7 +30,7 @@
|
||||
},
|
||||
{
|
||||
"heading": "Updating and removal",
|
||||
"body": "Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data.",
|
||||
"body": "Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.",
|
||||
"commands": [
|
||||
"sudo dpkg --remove framebridge-original-expanded"
|
||||
]
|
||||
@@ -64,7 +64,7 @@
|
||||
],
|
||||
"read_only": true,
|
||||
"capabilities_verified": false,
|
||||
"guide_sha256": "ce03094ef55168ab22d393ce1f9193aa0a26bc3524eaaecfc8f48bed1c788cf1"
|
||||
"guide_sha256": "d6ef35f7879fa290843fbfcadf403270ec0e70e403b0230b3469659492385928"
|
||||
},
|
||||
"commands": {
|
||||
"ok": true,
|
||||
@@ -567,5 +567,5 @@
|
||||
"guide_sha256": "54b70093617e6992de04d41ddd367ffdd9a6bd6c317c99a886d6391277c55f6b"
|
||||
}
|
||||
},
|
||||
"guide_sha256": "b6060e0572a612d8d7d8a988bc9b0bca7aa8db8ef267004e6a1495e9ab5f34a4"
|
||||
"guide_sha256": "ab3cce8d01d63a34fa2634738a9e0b37c89f5e1f51ba88d2b33abf9a872ad7ab"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ fb connect
|
||||
|
||||
### Updating and removal
|
||||
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data.
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.
|
||||
|
||||
~~~sh
|
||||
sudo dpkg --remove framebridge-original-expanded
|
||||
|
||||
@@ -42,7 +42,7 @@ fb connect
|
||||
|
||||
### Updating and removal
|
||||
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data.
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.
|
||||
|
||||
~~~sh
|
||||
sudo dpkg --remove framebridge-original-expanded
|
||||
|
||||
@@ -25,7 +25,7 @@ sudo apt install ./framebridge\-expanded_0.3.2~compat.20261027.1_amd64.deb
|
||||
fb connect
|
||||
.fi
|
||||
.SS Updating and removal
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package\-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential\-forgetting operation. On systemd 255, package removal also revokes the administrative boot\-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user\-profile data.
|
||||
Install an official update through the normal package manager when that update has been published. Normal removal stops package\-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential\-forgetting operation. On systemd 255, package removal also revokes the administrative boot\-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user\-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper\-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.
|
||||
.PP
|
||||
.nf
|
||||
sudo dpkg \-\-remove framebridge\-original\-expanded
|
||||
|
||||
@@ -19,7 +19,7 @@ GUIDE = {'start': {'title': 'Start with FrameBridge',
|
||||
'body': 'Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings.',
|
||||
'commands': ["sudo apt install ./framebridge-expanded_" + __version__ + "_amd64.deb", 'fb connect']},
|
||||
{'heading': 'Updating and removal',
|
||||
'body': 'Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data.',
|
||||
'body': 'Install an official update through the normal package manager when that update has been published. Normal removal stops package-owned managed processes and can retain the user profile and encrypted connection material; it is not a credential-forgetting operation. On systemd 255, package removal also revokes the administrative boot-credential grant. After reinstalling, run fb connect interactively to provision that grant again; retained files alone do not restore startup. Use the explicit credentials forget action first when that is your intended outcome. Do not assume purge erases user-profile data. An explicit downgrade also revokes administrative helper grants before the older package files replace the helper. Helper-backed profiles require a new interactive connect with a compatible package before managed startup can be restored.',
|
||||
'commands': ['sudo dpkg --remove framebridge-original-expanded']},
|
||||
{'heading': 'Human authentication',
|
||||
'body': 'The normal connection asks for an optional account label, a '
|
||||
|
||||
@@ -10,6 +10,7 @@ import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from build_profiles import CANDIDATE_VERSION, PROFILES, SOURCE_VENDOR, VENDOR_RELATIVE, target_profile, validate_vendor, vendor_versions
|
||||
@@ -136,6 +137,38 @@ def install_legal_material(docs):
|
||||
"dependency_source_archives": DEPENDENCY_SOURCE_ASSETS
|
||||
}, indent=2) + "\n")
|
||||
|
||||
def maintainer_prerm(version):
|
||||
"""Revoke helper grants before a downgrade removes the helper's code.
|
||||
|
||||
Equal-version reinstalls and forward upgrades retain managed credentials.
|
||||
The fixed installed cleanup module validates the helper's root ownership,
|
||||
permissions and ancestors before running its bounded purge-all command.
|
||||
"""
|
||||
if not re.fullmatch(r"[0-9][0-9A-Za-z.+:~\-]*", version):
|
||||
raise ValueError("Invalid Debian package version for maintainer script")
|
||||
helper_cleanup = (
|
||||
'import runpy; runpy.run_path('
|
||||
'"/usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py")'
|
||||
'["purge_credential_helpers"]()'
|
||||
)
|
||||
return (
|
||||
"#!/bin/sh\nset -eu\n"
|
||||
'case "$1" in\n'
|
||||
" remove|deconfigure)\n"
|
||||
" /usr/bin/python3 -I -S /usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py --all-users\n"
|
||||
" if [ -e /etc/framebridge-expanded/screen-access.json ] || [ -L /etc/framebridge-expanded/screen-access.json ]; then\n"
|
||||
" /usr/lib/framebridge-expanded/bin/framebridge-screen-access disable\n"
|
||||
" fi\n"
|
||||
" ;;\n"
|
||||
" upgrade)\n"
|
||||
' /usr/bin/dpkg --validate-version "$2"\n'
|
||||
' if /usr/bin/dpkg --compare-versions "$2" lt ' + shlex.quote(version) + "; then\n"
|
||||
" /usr/bin/python3 -I -S -c " + shlex.quote(helper_cleanup) + "\n"
|
||||
" fi\n"
|
||||
" ;;\nesac\n"
|
||||
)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
global VERSION, PUBLIC_FILENAME, RELEASE, STAGE, PREFIX
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
@@ -246,15 +279,7 @@ def main(argv=None):
|
||||
" Optional virtual displays are explicitly identified as virtual.\n"
|
||||
" No phone or Owner Device pairing. Services start after connection setup.\n"
|
||||
" Installing the package alone does not activate a connection.\n")
|
||||
(control / "prerm").write_text(
|
||||
"#!/bin/sh\nset -eu\n"
|
||||
"case \"$1\" in\n"
|
||||
" remove|deconfigure)\n"
|
||||
" /usr/bin/python3 -I -S /usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py --all-users\n"
|
||||
" if [ -e /etc/framebridge-expanded/screen-access.json ] || [ -L /etc/framebridge-expanded/screen-access.json ]; then\n"
|
||||
" /usr/lib/framebridge-expanded/bin/framebridge-screen-access disable\n"
|
||||
" fi\n"
|
||||
" ;;\nesac\n")
|
||||
(control / "prerm").write_text(maintainer_prerm(VERSION))
|
||||
(control / "prerm").chmod(0o755)
|
||||
rows = []
|
||||
for path in sorted(STAGE.rglob("*")):
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Execute maintscript branches with cleanup commands intercepted in a tempdir."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
|
||||
from build_expanded import maintainer_prerm
|
||||
from build_profiles import CANDIDATE_VERSION
|
||||
|
||||
|
||||
class MaintainerScriptTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.trace = self.root / "calls.jsonl"
|
||||
self.stub = self.root / "cleanup-stub"
|
||||
self.stub.write_text(
|
||||
"#!" + sys.executable + "\n"
|
||||
"import json,os,sys\n"
|
||||
"with open(os.environ['TEST_CALLS'], 'a') as stream: stream.write(json.dumps(sys.argv[1:])+'\\n')\n"
|
||||
"raise SystemExit(int(os.environ.get('TEST_CLEANUP_RC','0')))\n"
|
||||
)
|
||||
self.stub.chmod(0o755)
|
||||
self.screen = self.root / "screen-access.json"
|
||||
|
||||
def run_script(self, args, *, fail=0, screen=False):
|
||||
script = maintainer_prerm(CANDIDATE_VERSION)
|
||||
# Only side-effecting commands and config probes are redirected. The
|
||||
# real dpkg comparisons are read-only and verify Debian version ordering.
|
||||
script = script.replace("/usr/bin/python3", shlex.quote(str(self.stub)))
|
||||
script = script.replace("/usr/lib/framebridge-expanded/bin/framebridge-screen-access", shlex.quote(str(self.stub)))
|
||||
script = script.replace("/etc/framebridge-expanded/screen-access.json", shlex.quote(str(self.screen)))
|
||||
if screen:
|
||||
self.screen.write_text("{}")
|
||||
env = dict(os.environ, TEST_CALLS=str(self.trace), TEST_CLEANUP_RC=str(fail))
|
||||
result = subprocess.run(["/bin/sh", "-s", "--", *args], input=script, text=True,
|
||||
capture_output=True, env=env, timeout=10)
|
||||
calls = [json.loads(row) for row in self.trace.read_text().splitlines()] if self.trace.exists() else []
|
||||
return result, calls
|
||||
|
||||
def test_downgrade_revokes_only_helper_before_replacement(self):
|
||||
result, calls = self.run_script(["upgrade", "0.3.1"])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0][:3], ["-I", "-S", "-c"])
|
||||
self.assertEqual(calls[0][3], 'import runpy; runpy.run_path("/usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py")["purge_credential_helpers"]()')
|
||||
self.assertNotIn("--all-users", calls[0])
|
||||
|
||||
def test_forward_upgrade_retains_helper_state(self):
|
||||
result, calls = self.run_script(["upgrade", "0.3.2"])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_same_version_reinstall_retains_helper_state(self):
|
||||
result, calls = self.run_script(["upgrade", CANDIDATE_VERSION])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_remove_and_deconfigure_keep_existing_cleanup(self):
|
||||
for operation in ("remove", "deconfigure"):
|
||||
if self.trace.exists():
|
||||
self.trace.unlink()
|
||||
result, calls = self.run_script([operation], screen=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [["-I", "-S", "/usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py", "--all-users"], ["disable"]])
|
||||
|
||||
def test_failed_helper_cleanup_blocks_downgrade(self):
|
||||
result, calls = self.run_script(["upgrade", "0.3.1"], fail=17)
|
||||
self.assertEqual(result.returncode, 17)
|
||||
self.assertEqual(len(calls), 1)
|
||||
|
||||
def test_invalid_target_does_not_execute_or_interpolate(self):
|
||||
marker = self.root / "must-not-exist"
|
||||
result, calls = self.run_script(["upgrade", "0.3.1;$(touch " + str(marker) + ")"])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(calls, [])
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_missing_target_fails_closed(self):
|
||||
result, calls = self.run_script(["upgrade"])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_current_version_cannot_inject_script_paths(self):
|
||||
with self.assertRaises(ValueError):
|
||||
maintainer_prerm("0.3.2'; /tmp/untrusted-script; '")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user