Skip to content

Commit 2e2285a

Browse files
authored
feat(security): verify binary SHA-256 checksums after download (B5)
- Fetch checksums.txt from release assets after binary download - Compute SHA-256 of downloaded binary and compare - On mismatch: delete binary and raise RuntimeError with clear message - Graceful fallback: warn if checksums.txt not available (older releases) Addresses: design-partner-eval B5 (P1 security)
1 parent 8e0552d commit 2e2285a

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

src/capiscio/manager.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import hashlib
34
import platform
45
import stat
56
import shutil
@@ -68,6 +69,34 @@ def get_binary_path(version: str) -> Path:
6869
# For now, let's put it in a versioned folder
6970
return get_cache_dir() / version / filename
7071

72+
def _fetch_expected_checksum(version: str, filename: str) -> Optional[str]:
73+
"""Fetch the expected SHA-256 checksum from the release checksums.txt."""
74+
url = f"https://github.com/{GITHUB_REPO}/releases/download/v{version}/checksums.txt"
75+
try:
76+
resp = requests.get(url, timeout=30)
77+
resp.raise_for_status()
78+
for line in resp.text.strip().splitlines():
79+
parts = line.split()
80+
if len(parts) == 2 and parts[1] == filename:
81+
return parts[0]
82+
logger.warning(f"Binary {filename} not found in checksums.txt")
83+
return None
84+
except requests.exceptions.RequestException as e:
85+
logger.warning(f"Could not fetch checksums.txt: {e}")
86+
return None
87+
88+
def _verify_checksum(file_path: Path, expected_hash: str) -> bool:
89+
"""Verify SHA-256 checksum of a downloaded file."""
90+
sha256 = hashlib.sha256()
91+
with open(file_path, "rb") as f:
92+
for chunk in iter(lambda: f.read(8192), b""):
93+
sha256.update(chunk)
94+
actual = sha256.hexdigest()
95+
if actual != expected_hash:
96+
logger.error(f"Checksum mismatch: expected {expected_hash}, got {actual}")
97+
return False
98+
return True
99+
71100
def download_binary(version: str) -> Path:
72101
"""
73102
Download the binary for the current platform and version.
@@ -110,6 +139,23 @@ def download_binary(version: str) -> Path:
110139
st = os.stat(target_path)
111140
os.chmod(target_path, st.st_mode | stat.S_IEXEC)
112141

142+
# Verify checksum integrity
143+
expected_hash = _fetch_expected_checksum(version, filename)
144+
if expected_hash is not None:
145+
if not _verify_checksum(target_path, expected_hash):
146+
target_path.unlink()
147+
raise RuntimeError(
148+
f"Binary integrity check failed for {filename}. "
149+
"The downloaded file does not match the published checksum. "
150+
"This may indicate a tampered or corrupted download."
151+
)
152+
logger.info(f"Checksum verified for {filename}")
153+
else:
154+
logger.warning(
155+
"Could not verify binary integrity (checksums.txt not available). "
156+
"Consider upgrading capiscio-core to a version that publishes checksums."
157+
)
158+
113159
console.print(f"[green]Successfully installed CapiscIO Core v{version}[/green]")
114160
return target_path
115161

0 commit comments

Comments
 (0)