From e9c182c361ea3f5c17907db3c98d81a1fa041925 Mon Sep 17 00:00:00 2001 From: OAC Core Date: Sat, 3 Oct 2026 16:45:40 +0800 Subject: [PATCH] fix(installer): verify release files without --ignore-missing CentOS 7 ships coreutils 8.22, where sha256sum rejects --ignore-missing (added in 8.25), so a real install failed before verifying anything. Compare each downloaded Compose file against its listed digest directly; a missing entry or a mismatch still fails the install. Co-authored-by: multica-agent --- deploy/install.sh | 24 +++++++++++++++++++++++- deploy/test_install.py | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 39ad532e..4c166928 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -85,11 +85,33 @@ trap cleanup EXIT mkdir -p "$install_dir" chmod 700 "$install_dir" files=(compose.yaml ports.yaml) + +# CentOS 7 ships coreutils 8.22, which predates `sha256sum --ignore-missing` +# (8.25). Compare each downloaded file with its listed digest directly. +verify_sha256() { + local list="$1" name="$2" expected actual + expected="$(awk -v target="$name" '{ file = $2; sub(/^\*/, "", file); sub(/\r$/, "", file) } file == target { print $1 }' "$list")" + if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then + echo "No valid checksum for $name in $list." >&2 + return 1 + fi + actual="$(sha256sum "$name" | awk '{ print $1 }')" + if [[ "$actual" != "$expected" ]]; then + echo "$name: FAILED" >&2 + return 1 + fi +} + curl --fail --silent --show-error --location "$asset_base/compose-sha256sums.txt" --output "$install_dir/compose-sha256sums.txt" for name in "${files[@]}"; do curl --fail --silent --show-error --location "$asset_base/$name" --output "$install_dir/$name" done -(cd "$install_dir" && sha256sum --check --ignore-missing --quiet compose-sha256sums.txt) +( + cd "$install_dir" + for name in "${files[@]}"; do + verify_sha256 compose-sha256sums.txt "$name" + done +) compose_file="$(IFS=:; echo "${files[*]}")" umask 077 diff --git a/deploy/test_install.py b/deploy/test_install.py index 9ff0d304..783d2c68 100644 --- a/deploy/test_install.py +++ b/deploy/test_install.py @@ -10,13 +10,17 @@ ROOT = Path(__file__).resolve().parents[1] INSTALL = ROOT / "deploy/install.sh" +CHECKSUM = "a" * 64 class InstallScriptTests(unittest.TestCase): - def install(self, root, *args, compose_up=0, check_config=0, init=0): + def install(self, root, *args, compose_up=0, check_config=0, init=0, checksum=CHECKSUM, listed=None): bin_dir = root / "bin" bin_dir.mkdir(exist_ok=True) log = root / "docker.log" + if listed is None: + listed = {"compose.yaml": CHECKSUM, "ports.yaml": CHECKSUM} + checksums = "".join(f"{digest} {name}\\n" for name, digest in listed.items()) self.write_executable(bin_dir / "docker", textwrap.dedent(f"""\ #!/bin/sh printf '%s\\n' "$*" >> {log} @@ -36,16 +40,19 @@ def install(self, root, *args, compose_up=0, check_config=0, init=0): if [ "$1" = compose ] && [ "$2" = up ]; then exit {compose_up}; fi exit 0 """)) - self.write_executable(bin_dir / "curl", textwrap.dedent("""\ + self.write_executable(bin_dir / "curl", textwrap.dedent(f"""\ #!/bin/sh output="" while [ $# -gt 0 ]; do if [ "$1" = --output ]; then output="$2"; shift 2; continue; fi shift done - printf 'fixture\\n' > "$output" + case "$output" in + *compose-sha256sums.txt) printf '%b' '{checksums}' > "$output" ;; + *) printf 'fixture\\n' > "$output" ;; + esac """)) - self.write_executable(bin_dir / "sha256sum", "#!/bin/sh\nexit 0\n") + self.write_executable(bin_dir / "sha256sum", f'#!/bin/sh\ncase "$1" in --*) echo "sha256sum: unrecognized option: $1" >&2; exit 1 ;; esac\nprintf \'%s %s\\n\' {checksum} "$1"\n') self.write_executable(bin_dir / "ss", "#!/bin/sh\nexit 0\n") env = dict(os.environ, PATH=str(bin_dir) + os.pathsep + os.environ["PATH"], HOME=str(root)) completed = subprocess.run(["bash", str(INSTALL), "--install-dir", str(root / "oac"), *args], @@ -114,6 +121,33 @@ def test_env_holds_only_the_installation_choices(self): self.assertEqual(env["OAC_PUBLIC_URL"], "https://core.example") self.assertEqual(sorted(env), ["COMPOSE_FILE", "COMPOSE_PROJECT_NAME", "OAC_HOST", "OAC_INSTALL_DIR", "OAC_PUBLIC_URL", "OAC_WEB_PORT"]) + def test_a_checksum_mismatch_stops_before_the_stack_starts(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + completed, recorded = self.install(root, checksum="b" * 64) + self.assertNotEqual(completed.returncode, 0) + self.assertIn("compose.yaml: FAILED", completed.stderr) + self.assertNotIn("OpenAgentCore is running.", completed.stdout) + self.assertNotIn("compose up -d --wait", recorded) + self.assertFalse((root / "oac").exists(), "a failed checksum must remove the directory") + + def test_a_missing_checksum_entry_stops_the_install(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + completed, recorded = self.install(root, listed={"compose.yaml": CHECKSUM}) + self.assertNotEqual(completed.returncode, 0) + self.assertIn("No valid checksum for ports.yaml", completed.stderr) + self.assertNotIn("compose up -d --wait", recorded) + self.assertFalse((root / "oac").exists()) + + def test_checksum_entries_for_other_release_files_are_ignored(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + listed = {"compose.yaml": CHECKSUM, "ports.yaml": CHECKSUM, "https.yaml": CHECKSUM} + completed, _ = self.install(root, listed=listed) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertIn("OpenAgentCore is running.", completed.stdout) + def test_help_does_not_need_docker(self): help_text = subprocess.run(["bash", str(INSTALL), "--help"], capture_output=True, text=True, check=True) self.assertIn("--web-port", help_text.stdout)