diff --git a/skill/scripts/impeccable b/skill/scripts/impeccable index 108aa9a22..a161dcc53 100755 --- a/skill/scripts/impeccable +++ b/skill/scripts/impeccable @@ -112,17 +112,32 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then fetch_url "$url" && fetched=1 fi if [ "$fetched" = 1 ]; then - if command -v curl >/dev/null 2>&1 && curl -fsSL -o "$tmp.sha256" "$url.sha256" 2>/dev/null; then - expected=$(cut -d' ' -f1 < "$tmp.sha256") - actual="" - if command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$tmp" | cut -d' ' -f1) - elif command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$tmp" | cut -d' ' -f1); fi - if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then - rm -f "$tmp" "$tmp.sha256" - echo "impeccable: checksum mismatch downloading $url" >&2 - exit 127 - fi - rm -f "$tmp.sha256" + # Fail closed: a freshly downloaded binary runs only after verifying + # against its .sha256 sidecar. A sidecar that cannot be fetched, or a + # machine with no sha256 tool, refuses the download instead of exec'ing + # an unverified binary. (A binary already on PATH or in the cache that + # passes engine-probe is unaffected.) + sidecar_ok=0 + if command -v curl >/dev/null 2>&1; then + curl -fsSL --retry 2 -o "$tmp.sha256" "$url.sha256" 2>/dev/null && sidecar_ok=1 + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$tmp.sha256" "$url.sha256" 2>/dev/null && sidecar_ok=1 + fi + expected="" + [ "$sidecar_ok" = 1 ] && expected=$(cut -d' ' -f1 < "$tmp.sha256") + actual="" + if command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$tmp" | cut -d' ' -f1) + elif command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$tmp" | cut -d' ' -f1); fi + rm -f "$tmp.sha256" + if [ -z "$expected" ] || [ -z "$actual" ]; then + rm -f "$tmp" + echo "impeccable: cannot verify $url against $url.sha256 (sidecar unavailable or no sha256 tool); refusing the unverified download" >&2 + exit 127 + fi + if [ "$actual" != "$expected" ]; then + rm -f "$tmp" + echo "impeccable: checksum mismatch downloading $url" >&2 + exit 127 fi chmod +x "$tmp" 2>/dev/null mv -f "$tmp" "$cached" && exec "$cached" "$@" diff --git a/skill/scripts/impeccable.cmd b/skill/scripts/impeccable.cmd index bde13d5ea..f9a843ed7 100644 --- a/skill/scripts/impeccable.cmd +++ b/skill/scripts/impeccable.cmd @@ -13,9 +13,10 @@ rem - The unversioned user binary and the PATH candidate are validated with rem the engine-probe handshake (see :probe) so the retired 3.x npm CLI, rem whose bin is also named impeccable, is never exec'd. IMPECCABLE_BIN, rem the sibling binary, and the version-pinned cache stay trusted. -rem - Downloads are verified against the .sha256 sidecar via certutil when -rem the sidecar is served; on ARM64 the arm64 asset is tried first and the -rem x64 asset is the fallback (Windows on ARM runs x64 binaries). +rem - Downloads are verified against the .sha256 sidecar via certutil and +rem fail closed: a missing sidecar or hash tool refuses the download. On +rem ARM64 the arm64 asset is tried first and the x64 asset is the +rem fallback (Windows on ARM runs x64 binaries). if not defined IMPECCABLE_SKILL_DIR set "IMPECCABLE_SKILL_DIR=%~dp0.." if not defined IMPECCABLE_SELF set "IMPECCABLE_SELF=%~f0" set "arch=x64" @@ -81,27 +82,32 @@ curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul if errorlevel 1 goto fail :verify -rem Mirrors the sh launcher: a missing sidecar or hash tool skips -rem verification; a mismatch is fatal. +rem Mirrors the sh launcher and fails closed: a freshly downloaded binary +rem runs only after verifying against its .sha256 sidecar. A sidecar that +rem cannot be fetched, or an empty certutil result, refuses the download +rem instead of running an unverified binary. curl.exe -fsSL -o "%cached%.sha256" "%url%.sha256" >nul 2>nul -if not errorlevel 1 goto have_sidecar -del "%cached%.sha256" >nul 2>nul -goto place -:have_sidecar +if errorlevel 1 goto verify_refuse set "expected=" set /p expected=<"%cached%.sha256" for /f "tokens=1" %%h in ("%expected%") do set "expected=%%h" set "actual=" for /f "skip=1 delims=" %%h in ('certutil -hashfile "%cached%.part" SHA256 2^>nul') do if not defined actual set "actual=%%h" del "%cached%.sha256" >nul 2>nul -if not defined expected goto place -if not defined actual goto place +if not defined expected goto verify_refuse +if not defined actual goto verify_refuse set "actual=%actual: =%" if /I "%actual%"=="%expected%" goto place del "%cached%.part" >nul 2>nul echo impeccable: checksum mismatch downloading %url% 1>&2 exit /b 127 +:verify_refuse +del "%cached%.part" >nul 2>nul +del "%cached%.sha256" >nul 2>nul +echo impeccable: cannot verify %url% against %url%.sha256; refusing the unverified download 1>&2 +exit /b 127 + :place move /y "%cached%.part" "%cached%" >nul 2>nul if not exist "%cached%" goto fail