diff --git a/deployment/install.ps1 b/deployment/install.ps1 index e8ea9a96..f62edc70 100644 --- a/deployment/install.ps1 +++ b/deployment/install.ps1 @@ -23,6 +23,9 @@ function Install-DocsGPT { $ErrorActionPreference = 'Stop' $UvVersion = '0.12.15' $UvMinVersion = [version]'0.8.0' + # sha256 of https://astral.sh/uv/$UvVersion/install.ps1, checked before it runs. Bump it with + # $UvVersion: (Invoke-WebRequest "https://astral.sh/uv//install.ps1").Content | … + $UvInstallerSha256 = '63f2d7e2ccc347cc018127b22f56f569080e5730a390dcb702b654daf6822193' function Say([string]$Message) { Write-Host "==> $Message" } @@ -50,9 +53,20 @@ function Install-DocsGPT { $env:UV_INSTALL_DIR = $uvDir $env:UV_NO_MODIFY_PATH = '1' $env:UV_PRINT_QUIET = '1' - # A child PowerShell, so nothing the uv installer does can end this session. - $shell = (Get-Process -Id $PID).Path - & $shell -NoProfile -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/$UvVersion/install.ps1 | iex" + # Downloaded and checked before it runs, and run in a child PowerShell so nothing the + # uv installer does can end this session. + $installer = Join-Path ([System.IO.Path]::GetTempPath()) "uv-installer-$UvVersion.ps1" + try { + Invoke-WebRequest -Uri "https://astral.sh/uv/$UvVersion/install.ps1" -OutFile $installer -UseBasicParsing + $actual = (Get-FileHash -Path $installer -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $UvInstallerSha256) { + throw "The uv installer does not match its pinned sha256: got $actual, expected $UvInstallerSha256. Refusing to run it." + } + $shell = (Get-Process -Id $PID).Path + & $shell -NoProfile -ExecutionPolicy Bypass -File $installer + } finally { + Remove-Item $installer -Force -ErrorAction SilentlyContinue + } $uv = Join-Path $uvDir 'uv.exe' if (-not (Test-Path $uv)) { throw "uv did not install into $uvDir" } } diff --git a/deployment/install.sh b/deployment/install.sh index 018bb761..c7b81dee 100755 --- a/deployment/install.sh +++ b/deployment/install.sh @@ -20,6 +20,9 @@ UV_VERSION="0.12.15" UV_MIN_VERSION="0.8.0" +# sha256 of https://astral.sh/uv/$UV_VERSION/install.sh, checked before it runs. Bump it with +# UV_VERSION: curl -fsSL https://astral.sh/uv//install.sh | shasum -a 256 +UV_INSTALLER_SHA256="716a1d6844740756c68770fcec2f79c2013fb9b03869a113f61e15f6f482a6a1" main() { set -euo pipefail @@ -47,16 +50,33 @@ main() { die "curl or wget is needed to download $1" fi } - # run_downloaded URL COMMAND...: save URL to a file, then run COMMAND with the file as its last - # argument. A transfer cut short fails before anything runs. + sha256_of() { + if has shasum; then + shasum -a 256 "$1" | awk '{print $1}' + elif has sha256sum; then + sha256sum "$1" | awk '{print $1}' + else + die "neither shasum nor sha256sum is available to check $1" + fi + } + # run_downloaded URL SHA256 COMMAND...: save URL to a file, check it against SHA256 ("-" to + # skip), then run COMMAND with the file as its last argument. A transfer cut short, or content + # that does not match, fails before anything runs. run_downloaded() { - local url="$1" script status=0 - shift + local url="$1" expected="$2" script status=0 actual + shift 2 script="$(mktemp)" if ! download "$url" >"$script"; then rm -f "$script" die "could not download $url" fi + if [ "$expected" != "-" ]; then + actual="$(sha256_of "$script")" + if [ "$actual" != "$expected" ]; then + rm -f "$script" + die "$url does not match its pinned sha256: got $actual, expected $expected. Refusing to run it." + fi + fi "$@" "$script" || status=$? rm -f "$script" return "$status" @@ -96,7 +116,9 @@ main() { sudo="sudo" fi say "Installing Docker" - run_downloaded https://get.docker.com $sudo sh + # Docker's script changes over time and publishes no checksum, so it is only downloaded + # in full before it runs. + run_downloaded https://get.docker.com - $sudo sh $sudo systemctl enable --now docker >/dev/null 2>&1 || true if [ -n "$sudo" ]; then $sudo usermod -aG docker "$(id -un)" @@ -122,7 +144,8 @@ main() { if [ -z "$uv" ]; then local uv_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" say "Installing uv $UV_VERSION into $uv_dir" - run_downloaded "https://astral.sh/uv/$UV_VERSION/install.sh" env UV_INSTALL_DIR="$uv_dir" UV_NO_MODIFY_PATH=1 UV_PRINT_QUIET=1 sh + run_downloaded "https://astral.sh/uv/$UV_VERSION/install.sh" "$UV_INSTALLER_SHA256" \ + env UV_INSTALL_DIR="$uv_dir" UV_NO_MODIFY_PATH=1 UV_PRINT_QUIET=1 sh uv="$uv_dir/uv" [ -x "$uv" ] || die "uv did not install into $uv_dir" fi