From 6b14fc57c0dcdf4c41d6fa8170d558cc7d90ddb0 Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:18:08 +0000 Subject: [PATCH] fix(security): Implement mandatory SHA256 verification for cached native binaries --- Makefile | 140 ++++++++++++++++++++++++-- binary_digests.txt | 32 ++++++ scripts/check-digests-hook.sh | 19 ++++ scripts/compute_binary_digests.py | 135 +++++++++++++++++++++++++ scripts/init_digests_from_upstream.py | 127 +++++++++++++++++++++++ 5 files changed, 442 insertions(+), 11 deletions(-) create mode 100644 binary_digests.txt create mode 100644 scripts/check-digests-hook.sh create mode 100644 scripts/compute_binary_digests.py create mode 100644 scripts/init_digests_from_upstream.py diff --git a/Makefile b/Makefile index 0dfd05eeb..f79d408a9 100644 --- a/Makefile +++ b/Makefile @@ -47,19 +47,16 @@ benchmark: # Binaries cache : BASE_URL = https://github.com/AikidoSec/zen-internals/releases/download/v0.1.60 -FILES = \ +BINARY_FILES = \ libzen_internals_aarch64-apple-darwin.dylib \ - libzen_internals_aarch64-apple-darwin.dylib.sha256sum \ libzen_internals_aarch64-unknown-linux-gnu.so \ - libzen_internals_aarch64-unknown-linux-gnu.so.sha256sum \ libzen_internals_x86_64-apple-darwin.dylib \ - libzen_internals_x86_64-apple-darwin.dylib.sha256sum \ libzen_internals_x86_64-pc-windows-gnu.dll \ - libzen_internals_x86_64-pc-windows-gnu.dll.sha256sum \ - libzen_internals_x86_64-unknown-linux-gnu.so \ - libzen_internals_x86_64-unknown-linux-gnu.so.sha256sum + libzen_internals_x86_64-unknown-linux-gnu.so -binaries: binaries_make_dir $(addprefix .cache/binaries/, $(FILES)) +DIGEST_FILE = binary_digests.txt + +binaries: binaries_make_dir $(addprefix .cache/binaries/, $(BINARY_FILES)) verify_binaries binaries_make_dir: rm -rf .cache/binaries mkdir -p .cache/binaries/ @@ -67,15 +64,136 @@ binaries_make_dir: @echo "Downloading $*..." curl -L -o $@ $(BASE_URL)/$* +.PHONY: verify_binaries +verify_binaries: + @echo "Verifying binary integrity against trusted digests..." + @if [ ! -f "$(DIGEST_FILE)" ]; then \ + echo "ERROR: Trusted digest file $(DIGEST_FILE) not found."; \ + echo "Cannot verify binary integrity. Build aborted."; \ + exit 1; \ + fi + @if grep -q "PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD" "$(DIGEST_FILE)"; then \ + echo "ERROR: $(DIGEST_FILE) contains placeholder digests."; \ + echo ""; \ + echo "To initialize trusted digests, choose one of:"; \ + echo ""; \ + echo "Option 1 (Recommended): Download and compute digests independently"; \ + echo " python3 scripts/compute_binary_digests.py"; \ + echo ""; \ + echo "Option 2: Initialize from upstream .sha256sum files (faster, less secure)"; \ + echo " python3 scripts/init_digests_from_upstream.py"; \ + echo ""; \ + echo "After initialization:"; \ + echo " 1. Review the computed digests in $(DIGEST_FILE)"; \ + echo " 2. Verify binaries are from a trusted source"; \ + echo " 3. Commit the changes with a clear audit trail"; \ + echo ""; \ + echo "Build aborted for security reasons."; \ + exit 1; \ + fi + @cd .cache/binaries && \ + for binary in $(BINARY_FILES); do \ + if [ ! -f "$$binary" ]; then \ + echo "ERROR: Binary $$binary not found in cache."; \ + exit 1; \ + fi; \ + expected_digest=$$(grep "$$binary$$" ../../$(DIGEST_FILE) | awk '{print $$1}'); \ + if [ -z "$$expected_digest" ]; then \ + echo "ERROR: No trusted digest found for $$binary in $(DIGEST_FILE)."; \ + echo "Cannot verify binary integrity. Build aborted."; \ + exit 1; \ + fi; \ + if command -v sha256sum >/dev/null 2>&1; then \ + actual_digest=$$(sha256sum "$$binary" | awk '{print $$1}'); \ + elif command -v shasum >/dev/null 2>&1; then \ + actual_digest=$$(shasum -a 256 "$$binary" | awk '{print $$1}'); \ + else \ + echo "ERROR: Neither sha256sum nor shasum found. Cannot verify binaries."; \ + exit 1; \ + fi; \ + if [ "$$actual_digest" != "$$expected_digest" ]; then \ + echo "ERROR: Digest mismatch for $$binary"; \ + echo " Expected: $$expected_digest"; \ + echo " Actual: $$actual_digest"; \ + echo ""; \ + echo "Binary integrity verification FAILED. Build aborted."; \ + echo "This may indicate:"; \ + echo " - A compromised download or man-in-the-middle attack"; \ + echo " - Corrupted cache files"; \ + echo " - Outdated digest file"; \ + echo ""; \ + echo "To resolve:"; \ + echo " 1. Clear the cache: make clean"; \ + echo " 2. Verify $(DIGEST_FILE) is up to date"; \ + echo " 3. Re-run the build"; \ + exit 1; \ + fi; \ + echo "✓ $$binary verified successfully"; \ + done + @echo "All binaries verified successfully against trusted digests." + .PHONY: check_binaries check_binaries: - @if [ -d "$(CACHE_DIR)" ]; then \ - echo "Directory $(CACHE_DIR) exists."; \ + @if [ -d "$(CACHE_DIR)" ] && [ -n "$$(ls -A $(CACHE_DIR) 2>/dev/null)" ]; then \ + echo "Cache directory $(CACHE_DIR) exists. Verifying cached binaries..."; \ + $(MAKE) verify_binaries || { \ + echo "Cached binaries failed verification. Re-downloading..."; \ + $(MAKE) binaries; \ + }; \ else \ - echo "Directory $(CACHE_DIR) is empty. Running 'make binaries'..."; \ + echo "Cache directory $(CACHE_DIR) is empty or missing. Running 'make binaries'..."; \ $(MAKE) binaries; \ fi +.PHONY: download_and_compute_digests +download_and_compute_digests: + @echo "Downloading binaries and computing digests..." + @echo "WARNING: This will download binaries from $(BASE_URL)" + @echo "Ensure you trust this source before proceeding." + @read -p "Continue? [y/N] " -n 1 -r; \ + echo; \ + if [[ ! $$REPLY =~ ^[Yy]$$ ]]; then \ + echo "Aborted."; \ + exit 1; \ + fi + @$(MAKE) binaries_make_dir + @for binary in $(BINARY_FILES); do \ + echo "Downloading $$binary..."; \ + curl -L -o .cache/binaries/$$binary $(BASE_URL)/$$binary || { \ + echo "ERROR: Failed to download $$binary"; \ + exit 1; \ + }; \ + done + @echo "" + @echo "Computing SHA256 digests..." + @echo "# Trusted SHA256 digests for zen-internals v0.1.60 binaries" > $(DIGEST_FILE).new + @echo "# Format: " >> $(DIGEST_FILE).new + @echo "# These digests are independently verified and committed to the repository" >> $(DIGEST_FILE).new + @echo "# Any mismatch will cause the build to fail" >> $(DIGEST_FILE).new + @echo "" >> $(DIGEST_FILE).new + @echo "# Generated on $$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $(DIGEST_FILE).new + @echo "# Source: $(BASE_URL)" >> $(DIGEST_FILE).new + @echo "" >> $(DIGEST_FILE).new + @cd .cache/binaries && \ + for binary in $(BINARY_FILES); do \ + if command -v sha256sum >/dev/null 2>&1; then \ + digest=$$(sha256sum "$$binary" | awk '{print $$1}'); \ + elif command -v shasum >/dev/null 2>&1; then \ + digest=$$(shasum -a 256 "$$binary" | awk '{print $$1}'); \ + else \ + echo "ERROR: Neither sha256sum nor shasum found. Cannot compute digests."; \ + exit 1; \ + fi; \ + echo "$$digest $$binary" >> ../../$(DIGEST_FILE).new; \ + echo " $$binary: $$digest"; \ + done + @echo "" + @echo "Digests computed and saved to $(DIGEST_FILE).new" + @echo "IMPORTANT: Review the digests, verify they match trusted sources," + @echo "then replace $(DIGEST_FILE) with $(DIGEST_FILE).new" + @echo "" + @echo "To apply: mv $(DIGEST_FILE).new $(DIGEST_FILE)" + # Replace version number automatically on publish : replace_version: diff --git a/binary_digests.txt b/binary_digests.txt new file mode 100644 index 000000000..afcc4c7f3 --- /dev/null +++ b/binary_digests.txt @@ -0,0 +1,32 @@ +# Trusted SHA256 digests for zen-internals v0.1.60 binaries +# Format: +# These digests are independently verified and committed to the repository +# Any mismatch will cause the build to fail + +# INITIALIZATION REQUIRED: +# Before merging this security fix, run one of: +# python3 scripts/init_digests_from_upstream.py (faster, uses upstream checksums) +# python3 scripts/compute_binary_digests.py (more thorough, computes independently) +# +# Then review the digests and commit the updated file. + +# When upgrading zen-internals version: +# 1. Update BASE_URL in Makefile and scripts/*.py +# 2. Verify the release authenticity (GPG signature, release notes, security advisory) +# 3. Run: python3 scripts/compute_binary_digests.py +# 4. Review and commit the updated digests + +# macOS ARM64 (Apple Silicon) +PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD libzen_internals_aarch64-apple-darwin.dylib + +# Linux ARM64 +PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD libzen_internals_aarch64-unknown-linux-gnu.so + +# macOS x86_64 (Intel) +PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD libzen_internals_x86_64-apple-darwin.dylib + +# Windows x86_64 +PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD libzen_internals_x86_64-pc-windows-gnu.dll + +# Linux x86_64 +PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD libzen_internals_x86_64-unknown-linux-gnu.so diff --git a/scripts/check-digests-hook.sh b/scripts/check-digests-hook.sh new file mode 100644 index 000000000..3f140222c --- /dev/null +++ b/scripts/check-digests-hook.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Pre-commit hook to prevent committing placeholder digests +# Install: cp scripts/check-digests-hook.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit + +DIGEST_FILE="binary_digests.txt" + +if [ -f "$DIGEST_FILE" ]; then + if grep -q "PLACEHOLDER_DIGEST_MUST_BE_REPLACED_BEFORE_BUILD" "$DIGEST_FILE"; then + echo "ERROR: Cannot commit $DIGEST_FILE with placeholder digests" + echo "" + echo "Please initialize the digests first:" + echo " python3 scripts/compute_binary_digests.py" + echo "" + echo "Then review and stage the updated file." + exit 1 + fi +fi + +exit 0 diff --git a/scripts/compute_binary_digests.py b/scripts/compute_binary_digests.py new file mode 100644 index 000000000..25460efea --- /dev/null +++ b/scripts/compute_binary_digests.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +Script to download zen-internals binaries and compute their SHA256 digests. +This script should be run by a trusted maintainer when updating binary versions. + +The script downloads binaries, computes their digests, and also downloads the +upstream .sha256sum files for comparison. The maintainer should independently +verify the binaries before committing the digests. +""" + +import hashlib +import sys +import urllib.request +from pathlib import Path + +BASE_URL = "https://github.com/AikidoSec/zen-internals/releases/download/v0.1.60" +BINARY_FILES = [ + "libzen_internals_aarch64-apple-darwin.dylib", + "libzen_internals_aarch64-unknown-linux-gnu.so", + "libzen_internals_x86_64-apple-darwin.dylib", + "libzen_internals_x86_64-pc-windows-gnu.dll", + "libzen_internals_x86_64-unknown-linux-gnu.so", +] + + +def compute_sha256(file_path): + """Compute SHA256 digest of a file.""" + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + + +def download_file(url, dest_path): + """Download a file from URL to destination path.""" + print(f"Downloading {dest_path.name}...") + try: + urllib.request.urlretrieve(url, dest_path) + return True + except Exception as e: + print(f"ERROR: Failed to download {url}: {e}", file=sys.stderr) + return False + + +def main(): + # Create temporary directory for downloads + temp_dir = Path(".cache/binaries_temp") + temp_dir.mkdir(parents=True, exist_ok=True) + + print(f"Downloading binaries from {BASE_URL}") + print("=" * 70) + + digests = [] + upstream_checksums = {} + + for binary_file in BINARY_FILES: + # Download binary + url = f"{BASE_URL}/{binary_file}" + dest_path = temp_dir / binary_file + + if not download_file(url, dest_path): + print("\nERROR: Failed to download all binaries", file=sys.stderr) + sys.exit(1) + + # Compute digest + digest = compute_sha256(dest_path) + digests.append((digest, binary_file)) + print(f" Computed SHA256: {digest}") + + # Try to download upstream checksum for comparison + checksum_url = f"{BASE_URL}/{binary_file}.sha256sum" + checksum_path = temp_dir / f"{binary_file}.sha256sum" + if download_file(checksum_url, checksum_path): + try: + with open(checksum_path, "r", encoding="utf-8") as f: + upstream_checksum = f.read().strip().split()[0] + upstream_checksums[binary_file] = upstream_checksum + print(f" Upstream SHA256: {upstream_checksum}") + if digest == upstream_checksum: + print(" ✓ Checksums match") + else: + print(" ✗ WARNING: Checksums DO NOT match!") + except Exception as e: + print(f" Warning: Could not read upstream checksum: {e}") + print() + + print("=" * 70) + print("Writing digests to binary_digests.txt") + + # Write digest file + digest_file = Path("binary_digests.txt") + with open(digest_file, "w", encoding="utf-8") as f: + f.write("# Trusted SHA256 digests for zen-internals v0.1.60 binaries\n") + f.write("# Format: \n") + f.write( + "# These digests are independently verified and committed to the repository\n" + ) + f.write("# Any mismatch will cause the build to fail\n") + f.write("\n") + f.write( + "# When upgrading zen-internals version:\n" + "# 1. Update BASE_URL in Makefile and this script to point to the new release\n" + "# 2. Verify the release authenticity (GPG signature, release notes, security advisory)\n" + "# 3. Run: python3 scripts/compute_binary_digests.py\n" + "# 4. Review and commit the updated digests\n" + ) + f.write("\n") + + for digest, filename in digests: + f.write(f"{digest} {filename}\n") + + print(f"\nDigests written to {digest_file}") + print("\nIMPORTANT SECURITY VERIFICATION STEPS:") + print("1. Review the digests above") + print("2. Verify the upstream release is authentic:") + print( + f" - Check release notes at: https://github.com/AikidoSec/zen-internals/releases/tag/v0.1.60" + ) + print(" - Verify GPG signatures if available") + print(" - Check for security advisories") + print("3. If upstream checksums matched, this provides some assurance") + print( + "4. Consider additional verification methods (code review, reproducible builds)" + ) + print("5. Commit the digest file with a clear audit trail") + + # Clean up + import shutil + + shutil.rmtree(temp_dir) + + +if __name__ == "__main__": + main() diff --git a/scripts/init_digests_from_upstream.py b/scripts/init_digests_from_upstream.py new file mode 100644 index 000000000..7a588dd33 --- /dev/null +++ b/scripts/init_digests_from_upstream.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Script to initialize binary_digests.txt using upstream .sha256sum files. + +This script downloads the .sha256sum files published by the zen-internals release +and uses them to populate binary_digests.txt. While these checksums come from the +same release (and thus don't provide independent assurance against a compromised +publisher), they establish a baseline for verification and are better than no +verification at all. + +IMPORTANT: These digests should be independently verified when possible through: +- Reproducible builds +- Multiple independent sources +- Code review of zen-internals +- GPG signature verification +""" + +import sys +import urllib.request +from pathlib import Path + +BASE_URL = "https://github.com/AikidoSec/zen-internals/releases/download/v0.1.60" +BINARY_FILES = [ + "libzen_internals_aarch64-apple-darwin.dylib", + "libzen_internals_aarch64-unknown-linux-gnu.so", + "libzen_internals_x86_64-apple-darwin.dylib", + "libzen_internals_x86_64-pc-windows-gnu.dll", + "libzen_internals_x86_64-unknown-linux-gnu.so", +] + + +def download_checksum(binary_file): + """Download the .sha256sum file for a binary.""" + url = f"{BASE_URL}/{binary_file}.sha256sum" + try: + with urllib.request.urlopen(url) as response: + content = response.read().decode("utf-8").strip() + # Format is typically: " " or " " + parts = content.split() + if len(parts) >= 1: + return parts[0] # Return just the hash + return None + except Exception as e: + print( + f"ERROR: Failed to download checksum for {binary_file}: {e}", + file=sys.stderr, + ) + return None + + +def main(): + print(f"Downloading upstream checksums from {BASE_URL}") + print("=" * 70) + + digests = [] + failed = [] + + for binary_file in BINARY_FILES: + print(f"Fetching checksum for {binary_file}...") + digest = download_checksum(binary_file) + if digest: + digests.append((digest, binary_file)) + print(f" SHA256: {digest}") + else: + failed.append(binary_file) + print(f" FAILED") + + if failed: + print("\n" + "=" * 70) + print("ERROR: Failed to download checksums for:") + for binary_file in failed: + print(f" - {binary_file}") + print("\nCannot initialize binary_digests.txt") + sys.exit(1) + + print("\n" + "=" * 70) + print("Writing digests to binary_digests.txt") + + # Write digest file + digest_file = Path("binary_digests.txt") + with open(digest_file, "w", encoding="utf-8") as f: + f.write("# Trusted SHA256 digests for zen-internals v0.1.60 binaries\n") + f.write("# Format: \n") + f.write( + "# These digests are independently verified and committed to the repository\n" + ) + f.write("# Any mismatch will cause the build to fail\n") + f.write("\n") + f.write( + "# IMPORTANT: These digests were initialized from upstream .sha256sum files.\n" + "# While this provides a baseline for verification, these checksums come from\n" + "# the same release and do not provide independent assurance against a\n" + "# compromised release publisher.\n" + "#\n" + "# For stronger assurance, consider:\n" + "# - Verifying through reproducible builds\n" + "# - Comparing with digests from multiple independent sources\n" + "# - Reviewing zen-internals source code\n" + "# - Verifying GPG signatures if available\n" + ) + f.write("\n") + f.write( + "# When upgrading zen-internals version:\n" + "# 1. Update BASE_URL in Makefile and scripts/compute_binary_digests.py\n" + "# 2. Verify the release authenticity (GPG signature, release notes, security advisory)\n" + "# 3. Run: python3 scripts/compute_binary_digests.py\n" + "# 4. Review and commit the updated digests\n" + ) + f.write("\n") + + for digest, filename in digests: + f.write(f"{digest} {filename}\n") + + print(f"\nDigests written to {digest_file}") + print("\nIMPORTANT:") + print("- These digests were sourced from upstream .sha256sum files") + print( + "- They provide a baseline but should be independently verified when possible" + ) + print("- Review the digests before committing") + print( + f"- Check release notes: https://github.com/AikidoSec/zen-internals/releases/tag/v0.1.60" + ) + + +if __name__ == "__main__": + main()