From cd13f9217092d43a771cf9ba7b09bdd3da8d7c4d Mon Sep 17 00:00:00 2001 From: zaniebot Date: Tue, 1 Sep 2026 10:07:32 -0500 Subject: [PATCH] Verify downloads with astral-sh/versions checksums (#1033) `setup-uv` currently ignores the `sha256` supplied by the default `astral-sh/versions` manifest when a selected artifact is newer than its bundled checksum table, allowing that download to proceed without validation. Use the manifest checksum as a fallback after explicit and bundled checksums, and reject manifest entries that do not provide one. This preserves the stronger pinned hashes for known releases while verifying newer releases without requiring an action update. Part of #1032. --------- Co-authored-by: Zanie Blue Co-authored-by: William Woodruff Co-authored-by: Kevin Stillhammer --- __tests__/download/checksum/checksum.test.ts | 40 ++++++++++++++++++++ __tests__/download/download-version.test.ts | 7 +++- dist/setup/index.cjs | 25 +++++++++--- docs/customization.md | 5 ++- src/download/checksum/checksum.ts | 15 +++++++- src/download/download-version.ts | 18 +++++++-- 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/__tests__/download/checksum/checksum.test.ts b/__tests__/download/checksum/checksum.test.ts index 8b56d0f..b3d13a6 100644 --- a/__tests__/download/checksum/checksum.test.ts +++ b/__tests__/download/checksum/checksum.test.ts @@ -26,9 +26,49 @@ test("provided checksum beats known checksums", async () => { "x86_64", "unknown-linux-gnu", "0.3.0", + "incorrect-manifest-checksum", ); }); +test("known checksums beat manifest checksums", async () => { + await expect( + validateChecksum( + undefined, + filePath, + "x86_64", + "unknown-linux-gnu", + "0.3.0", + validChecksum, + ), + ).rejects.toThrow("did not match"); +}); + +test("manifest checksums are used when no known checksum exists", async () => { + await expect( + validateChecksum( + undefined, + filePath, + "aarch64", + "pc-windows-msvc", + "1.2.3", + "incorrect-manifest-checksum", + ), + ).rejects.toThrow("did not match"); +}); + +test("empty manifest checksums are rejected", async () => { + await expect( + validateChecksum( + undefined, + filePath, + "aarch64", + "pc-windows-msvc", + "1.2.3", + "", + ), + ).rejects.toThrow("No checksum found"); +}); + type KnownVersionFixture = { version: string; known: boolean }; it.each([ diff --git a/__tests__/download/download-version.test.ts b/__tests__/download/download-version.test.ts index e45d7e3..b2352dc 100644 --- a/__tests__/download/download-version.test.ts +++ b/__tests__/download/download-version.test.ts @@ -227,10 +227,10 @@ describe("download-version", () => { expect(mockValidateChecksum).not.toHaveBeenCalled(); }); - it("uses built-in checksums for default manifest downloads", async () => { + it("uses the default manifest checksum as a fallback", async () => { mockGetArtifact.mockResolvedValue({ archiveFormat: "tar.gz", - checksum: "manifest-checksum-that-should-be-ignored", + checksum: "manifest-checksum", downloadUrl: "https://example.com/uv.tar.gz", }); @@ -248,6 +248,7 @@ describe("download-version", () => { "x86_64", "unknown-linux-gnu", "0.9.26", + "manifest-checksum", ); }); @@ -400,6 +401,7 @@ describe("download-version", () => { "x86_64", "unknown-linux-gnu", "0.9.26", + "manifest-checksum", ); }); @@ -425,6 +427,7 @@ describe("download-version", () => { "x86_64", "unknown-linux-gnu", "0.9.26", + "manifest-checksum", ); }); diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index 94b8954..afce4ce 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -99730,15 +99730,20 @@ var known_checksums_default = { var KNOWN_CHECKSUMS = known_checksums_default; // src/download/checksum/checksum.ts -async function validateChecksum(checksum, downloadPath, arch3, platform2, version3) { +async function validateChecksum(checksum, downloadPath, arch3, platform2, version3, manifestChecksum) { const key = `${arch3}-${platform2}-${version3}`; const hasProvidedChecksum = checksum !== void 0 && checksum !== ""; - const checksumToUse = hasProvidedChecksum ? checksum : KNOWN_CHECKSUMS[key]; + const knownChecksum = KNOWN_CHECKSUMS[key]; + const hasManifestChecksum = manifestChecksum !== void 0 && manifestChecksum !== ""; + const checksumToUse = hasProvidedChecksum ? checksum : knownChecksum ?? (hasManifestChecksum ? manifestChecksum : void 0); if (checksumToUse === void 0) { + if (manifestChecksum !== void 0) { + throw new Error(`No checksum found for ${key} in manifest.`); + } debug(`No checksum found for ${key}.`); return; } - const checksumSource = hasProvidedChecksum ? "provided checksum" : `KNOWN_CHECKSUMS entry for ${key}`; + const checksumSource = hasProvidedChecksum ? "provided checksum" : knownChecksum !== void 0 ? `KNOWN_CHECKSUMS entry for ${key}` : "manifest checksum"; debug(`Validating checksum using ${checksumSource}.`); const isValid = await validateFileCheckSum(downloadPath, checksumToUse); if (!isValid) { @@ -101656,6 +101661,7 @@ async function downloadVersion(platform2, arch3, version3, checksum, githubToken ); } const resolvedChecksum = manifestUrl === void 0 ? checksum : resolveChecksum(checksum, artifact.checksum); + const manifestChecksum = artifact.checksum; const mirrorUrl = downloadFromAstralMirror ? rewriteToMirror(artifact.downloadUrl) : void 0; const downloadUrl = mirrorUrl ?? artifact.downloadUrl; try { @@ -101666,6 +101672,7 @@ async function downloadVersion(platform2, arch3, version3, checksum, githubToken arch3, version3, resolvedChecksum, + manifestChecksum, githubTokenForUrl(downloadUrl, githubToken) ); } catch (err) { @@ -101682,6 +101689,7 @@ async function downloadVersion(platform2, arch3, version3, checksum, githubToken arch3, version3, resolvedChecksum, + manifestChecksum, githubTokenForUrl(artifact.downloadUrl, githubToken) ); } @@ -101699,14 +101707,21 @@ function githubTokenForUrl(downloadUrl, githubToken) { return void 0; } } -async function downloadArtifact(downloadUrl, artifactName, platform2, arch3, version3, checksum, githubToken) { +async function downloadArtifact(downloadUrl, artifactName, platform2, arch3, version3, checksum, manifestChecksum, githubToken) { info2(`Downloading uv from "${downloadUrl}" ...`); const downloadPath = await downloadTool( downloadUrl, void 0, githubToken ); - await validateChecksum(checksum, downloadPath, arch3, platform2, version3); + await validateChecksum( + checksum, + downloadPath, + arch3, + platform2, + version3, + manifestChecksum + ); let uvDir; if (platform2 === "pc-windows-msvc") { try { diff --git a/docs/customization.md b/docs/customization.md index fa3ad2d..cea511e 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -4,8 +4,9 @@ This document covers advanced customization options including checksum validatio ## Validate checksum -You can specify a checksum to validate the downloaded executable. Checksums up to the default version -are automatically verified by this action. The sha256 hashes can be found on the +Downloaded executables are automatically verified using checksums bundled with this action or, +for newer, not yet bundled versions, the checksum from [`astral-sh/versions`](https://github.com/astral-sh/versions). +You can specify a checksum to override those values. The sha256 hashes can also be found on the [releases page](https://github.com/astral-sh/uv/releases) of the uv repo. ```yaml diff --git a/src/download/checksum/checksum.ts b/src/download/checksum/checksum.ts index ef8876c..11c2278 100644 --- a/src/download/checksum/checksum.ts +++ b/src/download/checksum/checksum.ts @@ -11,19 +11,30 @@ export async function validateChecksum( arch: Architecture, platform: Platform, version: string, + manifestChecksum?: string, ): Promise { const key = `${arch}-${platform}-${version}`; const hasProvidedChecksum = checksum !== undefined && checksum !== ""; - const checksumToUse = hasProvidedChecksum ? checksum : KNOWN_CHECKSUMS[key]; + const knownChecksum = KNOWN_CHECKSUMS[key]; + const hasManifestChecksum = + manifestChecksum !== undefined && manifestChecksum !== ""; + const checksumToUse = hasProvidedChecksum + ? checksum + : (knownChecksum ?? (hasManifestChecksum ? manifestChecksum : undefined)); if (checksumToUse === undefined) { + if (manifestChecksum !== undefined) { + throw new Error(`No checksum found for ${key} in manifest.`); + } core.debug(`No checksum found for ${key}.`); return; } const checksumSource = hasProvidedChecksum ? "provided checksum" - : `KNOWN_CHECKSUMS entry for ${key}`; + : knownChecksum !== undefined + ? `KNOWN_CHECKSUMS entry for ${key}` + : "manifest checksum"; core.debug(`Validating checksum using ${checksumSource}.`); const isValid = await validateFileCheckSum(downloadPath, checksumToUse); diff --git a/src/download/download-version.ts b/src/download/download-version.ts index 2c54c3a..dc808c2 100644 --- a/src/download/download-version.ts +++ b/src/download/download-version.ts @@ -47,12 +47,14 @@ export async function downloadVersion( ); } - // For the default astral-sh/versions source, checksum validation relies on - // user input or the built-in KNOWN_CHECKSUMS table, not manifest sha256 values. + // Custom manifests are explicitly selected by the user, so their checksum + // takes precedence over the built-in table. For the default manifest, pass + // its checksum as a fallback after user input and KNOWN_CHECKSUMS. const resolvedChecksum = manifestUrl === undefined ? checksum : resolveChecksum(checksum, artifact.checksum); + const manifestChecksum = artifact.checksum; const mirrorUrl = downloadFromAstralMirror ? rewriteToMirror(artifact.downloadUrl) @@ -67,6 +69,7 @@ export async function downloadVersion( arch, version, resolvedChecksum, + manifestChecksum, githubTokenForUrl(downloadUrl, githubToken), ); } catch (err) { @@ -85,6 +88,7 @@ export async function downloadVersion( arch, version, resolvedChecksum, + manifestChecksum, githubTokenForUrl(artifact.downloadUrl, githubToken), ); } @@ -122,6 +126,7 @@ async function downloadArtifact( arch: Architecture, version: string, checksum: string | undefined, + manifestChecksum: string | undefined, githubToken: string | undefined, ): Promise<{ version: string; cachedToolDir: string }> { log.info(`Downloading uv from "${downloadUrl}" ...`); @@ -130,7 +135,14 @@ async function downloadArtifact( undefined, githubToken, ); - await validateChecksum(checksum, downloadPath, arch, platform, version); + await validateChecksum( + checksum, + downloadPath, + arch, + platform, + version, + manifestChecksum, + ); let uvDir: string; if (platform === "pc-windows-msvc") {