|
1 | 1 | import os |
2 | 2 | import sys |
| 3 | +import hashlib |
3 | 4 | import platform |
4 | 5 | import stat |
5 | 6 | import shutil |
@@ -68,6 +69,34 @@ def get_binary_path(version: str) -> Path: |
68 | 69 | # For now, let's put it in a versioned folder |
69 | 70 | return get_cache_dir() / version / filename |
70 | 71 |
|
| 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 | + |
71 | 100 | def download_binary(version: str) -> Path: |
72 | 101 | """ |
73 | 102 | Download the binary for the current platform and version. |
@@ -110,6 +139,23 @@ def download_binary(version: str) -> Path: |
110 | 139 | st = os.stat(target_path) |
111 | 140 | os.chmod(target_path, st.st_mode | stat.S_IEXEC) |
112 | 141 |
|
| 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 | + |
113 | 159 | console.print(f"[green]Successfully installed CapiscIO Core v{version}[/green]") |
114 | 160 | return target_path |
115 | 161 |
|
|
0 commit comments