From f16f5283e07880657a030237d96832373619be66 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 01:11:21 -0400 Subject: [PATCH 1/8] Verify JDK downloads with vendor checksums Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- .github/workflows/e2e-versions.yml | 19 ++ README.md | 10 +- __tests__/checksum.test.ts | 111 ++++++++++++ .../distributors/adopt-installer.test.ts | 8 + __tests__/distributors/base-installer.test.ts | 78 ++++++++ .../distributors/corretto-installer.test.ts | 6 + .../distributors/dragonwell-installer.test.ts | 4 + __tests__/distributors/kona-installer.test.ts | 9 + .../distributors/sapmachine-installer.test.ts | 4 + .../distributors/semeru-installer.test.ts | 8 + .../distributors/temurin-installer.test.ts | 8 + action.yml | 2 +- dist/setup/index.js | 168 ++++++++++++++---- src/checksum.ts | 72 ++++++++ src/distributions/adopt/installer.ts | 9 +- src/distributions/base-installer.ts | 34 ++++ src/distributions/base-models.ts | 9 + src/distributions/corretto/installer.ts | 18 +- src/distributions/dragonwell/installer.ts | 10 +- src/distributions/graalvm/installer.ts | 2 +- src/distributions/jetbrains/installer.ts | 2 +- src/distributions/kona/installer.ts | 23 ++- src/distributions/liberica-nik/installer.ts | 2 +- src/distributions/liberica/installer.ts | 2 +- src/distributions/microsoft/installer.ts | 2 +- src/distributions/openjdk/installer.ts | 2 +- src/distributions/oracle/installer.ts | 2 +- src/distributions/sapmachine/installer.ts | 8 +- src/distributions/semeru/installer.ts | 9 +- src/distributions/temurin/installer.ts | 9 +- src/distributions/zulu/installer.ts | 2 +- 31 files changed, 587 insertions(+), 65 deletions(-) create mode 100644 __tests__/checksum.test.ts create mode 100644 src/checksum.ts diff --git a/.github/workflows/e2e-versions.yml b/.github/workflows/e2e-versions.yml index 1e8fbd45a..47f93d9fd 100644 --- a/.github/workflows/e2e-versions.yml +++ b/.github/workflows/e2e-versions.yml @@ -125,6 +125,25 @@ jobs: run: bash __tests__/verify-java.sh "$JAVA_VERSION" "$JAVA_PATH" shell: bash + setup-java-checksum-verification: + name: Corretto checksum verification - ubuntu-latest + runs-on: ubuntu-latest + steps: + - *checkout_step + - name: setup-java with forced download + uses: ./ + id: setup-java + with: + java-version: '21' + distribution: corretto + force-download: true + - name: Verify Java + env: + JAVA_VERSION: '21' + JAVA_PATH: ${{ steps.setup-java.outputs.path }} + run: bash __tests__/verify-java.sh "$JAVA_VERSION" "$JAVA_PATH" + shell: bash + setup-java-alpine-linux: name: ${{ matrix.distribution }} ${{ matrix.version }} (jdk-${{ contains(matrix.os, 'macos') && !contains(matrix.os, 'intel') && 'arm64' || 'x64' }}) - alpine-linux - ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/README.md b/README.md index 40b6af49e..acdb5f81b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ For more details, see the full release notes on the [releases page](https://git - `problem-matcher`: Set to `false` to disable Java problem matcher annotations (compiler diagnostics and uncaught exceptions). Default value: `true`. See [Java problem matcher](docs/advanced-usage.md#java-problem-matcher-compiler-annotations) for details and annotation limits. - - `verify-signature`: Verifies downloaded Java package signatures when supported by the selected distribution. Currently supported for `temurin` and `microsoft`. If set to `true` for unsupported distributions, the action fails. + - `verify-signature`: Verifies downloaded Java package signatures when supported by the selected distribution. Currently supported for `temurin` and `microsoft`. Signature verification provides an authenticity check in addition to automatic checksum verification. If set to `true` for unsupported distributions, the action fails. - `verify-signature-public-key`: ASCII-armored GPG public key used to verify the downloaded package signature. Overrides the default bundled key for the selected distribution. @@ -95,6 +95,14 @@ For more details, see the full release notes on the [releases page](https://git - `show-download-progress`: Set to `true` to keep Maven artifact download and transfer progress in build logs. Default value: `false`. By default, the action adds `-ntp` (`--no-transfer-progress`) to `MAVEN_ARGS`. This input has no effect on non-Maven builds. See [Maven transfer progress](docs/advanced-usage.md#maven-transfer-progress-download-logs) for more details. +### Download integrity verification + +When a selected distribution publishes an authoritative checksum in its release metadata, `setup-java` automatically verifies each downloaded JDK, JRE, or JMOD archive before extraction and caching. No input is required. Automatic checksum verification is currently available for `temurin`, `semeru`, `adopt`, `corretto`, `dragonwell`, `kona`, and `sapmachine`. + +Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported only in debug logs. Archives resolved directly from the runner tool cache are not downloaded again and therefore are not reverified. + +Checksums detect corrupted or unexpectedly modified downloads, while GPG signatures additionally authenticate the publisher. For supported distributions, enable `verify-signature` when that stronger authenticity guarantee is required; signature verification complements rather than replaces the automatic checksum check. + ### Basic Configuration #### Eclipse Temurin diff --git a/__tests__/checksum.test.ts b/__tests__/checksum.test.ts new file mode 100644 index 000000000..2c5e13ed2 --- /dev/null +++ b/__tests__/checksum.test.ts @@ -0,0 +1,111 @@ +import {afterEach, describe, expect, it, jest} from '@jest/globals'; +import {createHash} from 'crypto'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {calculateChecksum, verifyChecksum} from '../src/checksum.js'; +import type {ChecksumMetadata} from '../src/distributions/base-models.js'; + +const temporaryPaths: string[] = []; + +async function temporaryFile(contents: string): Promise { + const directory = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'setup-java-checksum-') + ); + const file = path.join(directory, 'archive'); + await fs.promises.writeFile(file, contents); + temporaryPaths.push(directory); + return file; +} + +afterEach(async () => { + await Promise.all( + temporaryPaths + .splice(0) + .map(item => fs.promises.rm(item, {recursive: true, force: true})) + ); + jest.restoreAllMocks(); +}); + +describe('verifyChecksum', () => { + it.each(['sha256', 'sha512'] as const)( + 'verifies a matching %s digest', + async algorithm => { + const contents = `jdk archive for ${algorithm}`; + const file = await temporaryFile(contents); + const value = createHash(algorithm).update(contents).digest('hex'); + + await expect( + verifyChecksum( + file, + {algorithm, value: value.toUpperCase()}, + {distribution: 'Test', version: '21.0.1'} + ) + ).resolves.toBeUndefined(); + } + ); + + it('reports mismatch context and both digests', async () => { + const file = await temporaryFile('corrupt archive'); + const expected = 'a'.repeat(64); + const actual = await calculateChecksum(file, 'sha256'); + + await expect( + verifyChecksum( + file, + {algorithm: 'sha256', value: expected}, + {distribution: 'Corretto', version: '21.0.8'} + ) + ).rejects.toThrow( + `Checksum verification failed for Corretto version 21.0.8: sha256 expected ${expected}, actual ${actual}.` + ); + }); + + it('rejects malformed digest metadata before reading the file', async () => { + await expect( + verifyChecksum( + '/missing/archive', + {algorithm: 'sha512', value: 'not-a-digest'}, + {distribution: 'Test', version: '17'} + ) + ).rejects.toThrow( + 'Malformed sha512 checksum metadata: expected a 128-character hexadecimal digest.' + ); + }); + + it('rejects unsupported algorithms without leaking source query parameters', async () => { + const checksum = { + algorithm: 'md5', + value: 'a'.repeat(32), + source: 'https://vendor.example/checksum.txt?token=secret-value#private' + } as unknown as ChecksumMetadata; + + let message = ''; + try { + await verifyChecksum('/missing/archive', checksum, { + distribution: 'Test', + version: '17' + }); + } catch (error) { + message = (error as Error).message; + } + + expect(message).toContain( + "Unsupported checksum algorithm 'md5' from https://vendor.example/checksum.txt" + ); + expect(message).not.toContain('secret-value'); + expect(message).not.toContain('token='); + expect(message).not.toContain('#private'); + }); + + it('surfaces file read errors', async () => { + await expect( + verifyChecksum( + '/missing/archive', + {algorithm: 'sha256', value: 'a'.repeat(64)}, + {distribution: 'Test', version: '17'} + ) + ).rejects.toMatchObject({code: 'ENOENT'}); + }); +}); diff --git a/__tests__/distributors/adopt-installer.test.ts b/__tests__/distributors/adopt-installer.test.ts index 0d10b0438..b59451b26 100644 --- a/__tests__/distributors/adopt-installer.test.ts +++ b/__tests__/distributors/adopt-installer.test.ts @@ -357,6 +357,14 @@ describe('findPackageForDownload', () => { distribution['getAvailableVersions'] = async () => manifestData as any; const resolvedVersion = await distribution['findPackageForDownload'](input); expect(resolvedVersion.version).toBe(expected); + const vendorPackage = (manifestData as any[]).find( + item => item.version_data.semver === expected + ).binaries[0].package; + expect(resolvedVersion.checksum).toEqual({ + algorithm: 'sha256', + value: vendorPackage.checksum, + source: vendorPackage.checksum_link + }); }); it('version is found but binaries list is empty', async () => { diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index baa156d26..a6bb1e155 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -16,6 +16,8 @@ import type { import path from 'path'; import * as semver from 'semver'; +import fs from 'fs'; +import {createHash} from 'crypto'; import os from 'os'; @@ -117,6 +119,10 @@ class EmptyJavaBase extends JavaBase { url: `some/random_url/java/${availableVersion}` }; } + + public downloadRelease(javaRelease: JavaDownloadRelease): Promise { + return this.downloadAndVerify(javaRelease); + } } describe('findInToolcache', () => { @@ -817,6 +823,78 @@ describe('setupJava', () => { }); }); +describe('downloadAndVerify', () => { + const options: JavaInstallerOptions = { + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }; + let temporaryDirectory: string; + let archivePath: string; + + beforeEach(async () => { + temporaryDirectory = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'setup-java-base-') + ); + archivePath = path.join(temporaryDirectory, 'archive'); + await fs.promises.writeFile(archivePath, 'downloaded archive'); + (tc.downloadTool as jest.Mock).mockResolvedValue(archivePath); + }); + + afterEach(async () => { + await fs.promises.rm(temporaryDirectory, {recursive: true, force: true}); + jest.resetAllMocks(); + }); + + it('returns a download after successful verification', async () => { + const distribution = new EmptyJavaBase(options); + const result = await distribution.downloadRelease({ + version: '21.0.8', + url: 'https://vendor.example/jdk.tar.gz', + checksum: { + algorithm: 'sha256', + value: createHash('sha256').update('downloaded archive').digest('hex') + } + }); + + expect(result).toBe(archivePath); + expect(fs.existsSync(archivePath)).toBe(true); + expect(core.debug).toHaveBeenCalledWith( + 'Verified sha256 checksum for Empty version 21.0.8.' + ); + }); + + it('removes the download after verification failure', async () => { + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.downloadRelease({ + version: '21.0.8', + url: 'https://vendor.example/jdk.tar.gz?token=secret', + checksum: {algorithm: 'sha256', value: 'a'.repeat(64)} + }) + ).rejects.toThrow('Checksum verification failed for Empty version 21.0.8'); + + expect(fs.existsSync(archivePath)).toBe(false); + }); + + it('logs when authoritative checksum metadata is unavailable', async () => { + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.downloadRelease({ + version: '21.0.8', + url: 'https://vendor.example/jdk.tar.gz' + }) + ).resolves.toBe(archivePath); + + expect(core.debug).toHaveBeenCalledWith( + 'No authoritative checksum is available for Empty version 21.0.8; skipping checksum verification.' + ); + }); +}); + describe('normalizeVersion', () => { const DummyJavaBase = JavaBase as any; diff --git a/__tests__/distributors/corretto-installer.test.ts b/__tests__/distributors/corretto-installer.test.ts index 57e660818..371ac86c5 100644 --- a/__tests__/distributors/corretto-installer.test.ts +++ b/__tests__/distributors/corretto-installer.test.ts @@ -202,6 +202,12 @@ describe('getAvailableVersions', () => { await distribution['findPackageForDownload'](version); expect(availableVersion).not.toBeNull(); expect(availableVersion.url).toBe(expectedLink); + expect(availableVersion.checksum).toEqual({ + algorithm: 'sha256', + value: expect.stringMatching(/^[a-f0-9]{64}$/), + source: + 'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json' + }); }); it('with latest resolves to the newest available major version', async () => { diff --git a/__tests__/distributors/dragonwell-installer.test.ts b/__tests__/distributors/dragonwell-installer.test.ts index 032ba323e..69bada844 100644 --- a/__tests__/distributors/dragonwell-installer.test.ts +++ b/__tests__/distributors/dragonwell-installer.test.ts @@ -259,6 +259,10 @@ describe('getAvailableVersions', () => { await distribution['findPackageForDownload'](jdkVersion); expect(availableVersion).not.toBeNull(); expect(availableVersion.url).toBe(expectedLink); + expect(availableVersion.checksum).toEqual({ + algorithm: 'sha256', + value: expect.stringMatching(/^[a-f0-9]{64}$/) + }); } ); diff --git a/__tests__/distributors/kona-installer.test.ts b/__tests__/distributors/kona-installer.test.ts index 88a6d5cb6..ba5a83266 100644 --- a/__tests__/distributors/kona-installer.test.ts +++ b/__tests__/distributors/kona-installer.test.ts @@ -216,6 +216,15 @@ describe('Check findPackageForDownload', () => { await distribution['findPackageForDownload'](version); expect(availableRelease).not.toBeNull(); expect(availableRelease.url).toBe(expectedUrl); + if (availableRelease.checksum) { + expect(availableRelease.checksum).toEqual({ + algorithm: 'sha256', + value: expect.stringMatching(/^[a-f0-9]{64}$/), + source: 'https://tencent.github.io/konajdk/releases/kona-v1.json' + }); + } else { + expect(version).toBe('8.0.20'); + } } ); }); diff --git a/__tests__/distributors/sapmachine-installer.test.ts b/__tests__/distributors/sapmachine-installer.test.ts index 17dae8d2a..f778e4a47 100644 --- a/__tests__/distributors/sapmachine-installer.test.ts +++ b/__tests__/distributors/sapmachine-installer.test.ts @@ -282,6 +282,10 @@ describe('getAvailableVersions', () => { await distribution['findPackageForDownload'](normalizedVersion); expect(availableVersion).not.toBeNull(); expect(availableVersion.url).toBe(expectedLink); + expect(availableVersion.checksum).toEqual({ + algorithm: 'sha256', + value: expect.stringMatching(/^[a-f0-9]{64}$/) + }); } ); diff --git a/__tests__/distributors/semeru-installer.test.ts b/__tests__/distributors/semeru-installer.test.ts index dd9a126a6..7dde47d03 100644 --- a/__tests__/distributors/semeru-installer.test.ts +++ b/__tests__/distributors/semeru-installer.test.ts @@ -208,6 +208,14 @@ describe('findPackageForDownload', () => { distribution['getAvailableVersions'] = async () => manifestData as any; const resolvedVersion = await distribution['findPackageForDownload'](input); expect(resolvedVersion.version).toBe(expected); + const vendorPackage = (manifestData as any[]).find( + item => item.version_data.semver === expected + ).binaries[0].package; + expect(resolvedVersion.checksum).toEqual({ + algorithm: 'sha256', + value: vendorPackage.checksum, + source: vendorPackage.checksum_link + }); }); it('version is found but binaries list is empty', async () => { diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 06b5ce0b5..d32c01142 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -347,6 +347,14 @@ describe('findPackageForDownload', () => { const resolvedVersion = await distribution['findPackageForDownload'](input); expect(resolvedVersion.version).toBe(expected); expect(resolvedVersion.signatureUrl).toBeDefined(); + const vendorPackage = (manifestData as any[]).find( + item => item.version_data.semver === expected + ).binaries[0].package; + expect(resolvedVersion.checksum).toEqual({ + algorithm: 'sha256', + value: vendorPackage.checksum, + source: vendorPackage.checksum_link + }); }); it('version "latest" is normalized to the newest available version', async () => { diff --git a/action.yml b/action.yml index d980a9dd2..f52a69596 100644 --- a/action.yml +++ b/action.yml @@ -39,7 +39,7 @@ inputs: required: false default: true verify-signature: - description: 'Verify downloaded Java package signatures when supported by the selected distribution' + description: 'Verify downloaded Java package signatures, in addition to automatic checksum verification, when supported by the selected distribution' required: false default: false verify-signature-public-key: diff --git a/dist/setup/index.js b/dist/setup/index.js index bd129e371..f3f3bc2b4 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -12040,7 +12040,7 @@ exports.NodeListStaticImpl = NodeListStaticImpl; /***/ }), -/***/ 2256: +/***/ 9875: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { @@ -13485,7 +13485,7 @@ const NodeListImpl_1 = __nccwpck_require__(5788); Object.defineProperty(exports, "NodeList", ({ enumerable: true, get: function () { return NodeListImpl_1.NodeListImpl; } })); const NodeListStaticImpl_1 = __nccwpck_require__(7654); Object.defineProperty(exports, "NodeListStatic", ({ enumerable: true, get: function () { return NodeListStaticImpl_1.NodeListStaticImpl; } })); -const NonDocumentTypeChildNodeImpl_1 = __nccwpck_require__(2256); +const NonDocumentTypeChildNodeImpl_1 = __nccwpck_require__(9875); const NonElementParentNodeImpl_1 = __nccwpck_require__(5325); const ParentNodeImpl_1 = __nccwpck_require__(1824); const ProcessingInstructionImpl_1 = __nccwpck_require__(2755); @@ -129411,6 +129411,50 @@ function retrying_http_client_getErrorMessage(error) { return error instanceof Error ? error.message : 'network error'; } +;// CONCATENATED MODULE: external "stream/promises" +const promises_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("stream/promises"); +;// CONCATENATED MODULE: ./src/checksum.ts + + + +function sanitizedSource(source) { + if (!source) { + return ''; + } + try { + const url = new URL(source); + return ` from ${url.origin}${url.pathname}`; + } + catch { + return ' from an invalid checksum source'; + } +} +function normalizeExpectedDigest(checksum) { + const algorithm = checksum.algorithm; + const digest = checksum.value.trim().toLowerCase(); + const expectedLength = algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; + if (expectedLength === 0) { + throw new Error(`Unsupported checksum algorithm '${String(algorithm)}'${sanitizedSource(checksum.source)}. Supported algorithms are sha256 and sha512.`); + } + if (!new RegExp(`^[a-f0-9]{${expectedLength}}$`).test(digest)) { + throw new Error(`Malformed ${algorithm} checksum metadata${sanitizedSource(checksum.source)}: expected a ${expectedLength}-character hexadecimal digest.`); + } + return digest; +} +async function calculateChecksum(filePath, algorithm) { + const hash = (0,external_crypto_namespaceObject.createHash)(algorithm); + await (0,promises_namespaceObject.pipeline)((0,external_fs_namespaceObject.createReadStream)(filePath), hash); + return hash.digest('hex'); +} +async function verifyChecksum(filePath, checksum, context) { + const expected = normalizeExpectedDigest(checksum); + const actual = await calculateChecksum(filePath, checksum.algorithm); + const matches = (0,external_crypto_namespaceObject.timingSafeEqual)(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex')); + if (!matches) { + throw new Error(`Checksum verification failed for ${context.distribution} version ${context.version}: ${checksum.algorithm} expected ${expected}, actual ${actual}.`); + } +} + ;// CONCATENATED MODULE: ./src/distributions/base-installer.ts @@ -129422,6 +129466,7 @@ function retrying_http_client_getErrorMessage(error) { + class JavaBase { distribution; http; @@ -129454,6 +129499,30 @@ class JavaBase { this.verifySignature = installerOptions.verifySignature ?? false; this.verifySignaturePublicKey = installerOptions.verifySignaturePublicKey; } + async downloadAndVerify(javaRelease) { + const archivePath = await downloadTool(javaRelease.url); + if (!javaRelease.checksum) { + core_debug(`No authoritative checksum is available for ${this.distribution} version ${javaRelease.version}; skipping checksum verification.`); + return archivePath; + } + try { + await verifyChecksum(archivePath, javaRelease.checksum, { + distribution: this.distribution, + version: javaRelease.version + }); + core_debug(`Verified ${javaRelease.checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.`); + return archivePath; + } + catch (error) { + try { + await external_fs_namespaceObject.promises.rm(archivePath, { force: true }); + } + catch (cleanupError) { + throw new Error(`${error.message} Failed to remove the downloaded archive after verification failure: ${cleanupError.message}`, { cause: cleanupError }); + } + throw error; + } + } async setupJava() { if (this.verifySignature && !this.supportsSignatureVerification()) { throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); @@ -129834,7 +129903,7 @@ class ZuluDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130017,7 +130086,12 @@ class TemurinDistribution extends JavaBase { return { version: formattedVersion, url: item.binaries[0].package.link, - signatureUrl: item.binaries[0].package.signature_link + signatureUrl: item.binaries[0].package.signature_link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } }; }); const satisfiedVersions = availableVersionsWithBinaries @@ -130057,7 +130131,7 @@ class TemurinDistribution extends JavaBase { return true; } async downloadPackage(release) { - const archivePath = await downloadTool(release.url); + const archivePath = await this.downloadAndVerify(release); if (this.verifySignature) { if (!release.signatureUrl) { throw new Error(`Input 'verify-signature' is enabled, but no signature URL was found for Temurin version ${release.version}.`); @@ -130222,7 +130296,12 @@ class AdoptDistribution extends JavaBase { .map(item => { return { version: item.version_data.semver, - url: item.binaries[0].package.link + url: item.binaries[0].package.link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } }; }); const satisfiedVersions = availableVersionsWithBinaries @@ -130239,7 +130318,7 @@ class AdoptDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130348,7 +130427,7 @@ class LibericaDistributions extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130477,7 +130556,7 @@ class LibericaNikDistributions extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130623,7 +130702,7 @@ class MicrosoftDistributions extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); if (this.verifySignature) { if (!javaRelease.signatureUrl) { throw new Error(`Input 'verify-signature' is enabled, but no signature URL was found for Microsoft Build of OpenJDK version ${javaRelease.version}.`); @@ -130757,7 +130836,12 @@ class SemeruDistribution extends JavaBase { : item.version_data.semver.replace('-beta+', '+'); return { version: formattedVersion, - url: item.binaries[0].package.link + url: item.binaries[0].package.link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } }; }); const satisfiedVersions = availableVersionsWithBinaries @@ -130777,7 +130861,7 @@ class SemeruDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130872,13 +130956,14 @@ class SemeruDistribution extends JavaBase { +const CORRETTO_VERSIONS_URL = 'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json'; class CorrettoDistribution extends JavaBase { constructor(installerOptions) { super('Corretto', installerOptions); } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -130916,7 +131001,12 @@ class CorrettoDistribution extends JavaBase { .map(item => { return { version: convertVersionToSemver(item.correttoVersion), - url: item.downloadLink + url: item.downloadLink, + checksum: { + algorithm: 'sha256', + value: item.checksum_sha256, + source: CORRETTO_VERSIONS_URL + } }; }); const resolvedVersion = matchingVersions.length > 0 ? matchingVersions[0] : null; @@ -130933,11 +131023,10 @@ class CorrettoDistribution extends JavaBase { if (isDebug()) { console.time('Retrieving available versions for Corretto took'); // eslint-disable-line no-console } - const availableVersionsUrl = 'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json'; - const fetchCurrentVersions = await this.http.getJson(availableVersionsUrl); + const fetchCurrentVersions = await this.http.getJson(CORRETTO_VERSIONS_URL); const fetchedCurrentVersions = fetchCurrentVersions.result; if (!fetchedCurrentVersions) { - throw Error(`Could not fetch latest corretto versions from ${availableVersionsUrl}`); + throw Error(`Could not fetch latest corretto versions from ${CORRETTO_VERSIONS_URL}`); } const eligibleVersions = fetchedCurrentVersions?.[platform]?.[arch]?.[imageType]; const availableVersions = this.getAvailableVersionsForPlatform(eligibleVersions); @@ -131012,7 +131101,7 @@ class OracleDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -131119,7 +131208,13 @@ class DragonwellDistribution extends JavaBase { .map(item => { return { version: item.jdk_version, - url: item.download_link + url: item.download_link, + checksum: item.checksum + ? { + algorithm: 'sha256', + value: item.checksum + } + : undefined }; }); if (!matchedVersions.length) { @@ -131150,7 +131245,7 @@ class DragonwellDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -131276,7 +131371,11 @@ class SapMachineDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadLink + url: item.downloadLink, + checksum: { + algorithm: 'sha256', + value: item.checksum + } }; }); if (!matchedVersions.length) { @@ -131307,7 +131406,7 @@ class SapMachineDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -131443,7 +131542,7 @@ class GraalVMDistribution extends JavaBase { async downloadTool(javaRelease) { try { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (installer_IS_WINDOWS) { @@ -131752,7 +131851,7 @@ class JetBrainsDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - const javaArchivePath = await downloadTool(javaRelease.url); + const javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extractedJavaPath = await extractJdkFile(javaArchivePath, 'tar.gz'); const archiveName = external_fs_default().readdirSync(extractedJavaPath)[0]; @@ -131899,13 +131998,14 @@ class JetBrainsDistribution extends JavaBase { +const KONA_RELEASES_URL = 'https://tencent.github.io/konajdk/releases/kona-v1.json'; class KonaDistribution extends JavaBase { constructor(installerOptions) { super('Kona', installerOptions); } async downloadTool(javaRelease) { info(`Downloading Kona JDK ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - const javaArchivePath = await downloadTool(javaRelease.url); + const javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); const archivePath = process.platform === 'win32' @@ -131933,7 +132033,14 @@ class KonaDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadUrl + url: item.downloadUrl, + checksum: item.checksum + ? { + algorithm: 'sha256', + value: item.checksum, + source: KONA_RELEASES_URL + } + : undefined }; }) .sort((a, b) => -semver_default().compareBuild(a.version, b.version)); @@ -131960,14 +132067,13 @@ class KonaDistribution extends JavaBase { return availableReleases; } async fetchReleaseInfo() { - const releasesInfoUrl = 'https://tencent.github.io/konajdk/releases/kona-v1.json'; try { - core_debug(`Fetching Kona release info from URL: ${releasesInfoUrl}`); - return (await this.http.getJson(releasesInfoUrl)) + core_debug(`Fetching Kona release info from URL: ${KONA_RELEASES_URL}`); + return (await this.http.getJson(KONA_RELEASES_URL)) .result; } catch (err) { - core_debug(`Fetching Kona release info from the URL: ${releasesInfoUrl} failed with the error: ${err.message}`); + core_debug(`Fetching Kona release info from the URL: ${KONA_RELEASES_URL} failed with the error: ${err.message}`); return null; } } @@ -132051,7 +132157,7 @@ class OpenJdkDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); info(`Extracting Java archive...`); const extension = javaRelease.url.endsWith('.zip') ? 'zip' : 'tar.gz'; if (extension === 'zip') { diff --git a/src/checksum.ts b/src/checksum.ts new file mode 100644 index 000000000..d7baba48a --- /dev/null +++ b/src/checksum.ts @@ -0,0 +1,72 @@ +import {createHash, timingSafeEqual} from 'crypto'; +import {createReadStream} from 'fs'; +import {pipeline} from 'stream/promises'; + +import {ChecksumMetadata} from './distributions/base-models.js'; + +export interface ChecksumVerificationContext { + distribution: string; + version: string; +} + +function sanitizedSource(source: string | undefined): string { + if (!source) { + return ''; + } + + try { + const url = new URL(source); + return ` from ${url.origin}${url.pathname}`; + } catch { + return ' from an invalid checksum source'; + } +} + +function normalizeExpectedDigest(checksum: ChecksumMetadata): string { + const algorithm = checksum.algorithm; + const digest = checksum.value.trim().toLowerCase(); + const expectedLength = + algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; + + if (expectedLength === 0) { + throw new Error( + `Unsupported checksum algorithm '${String(algorithm)}'${sanitizedSource(checksum.source)}. Supported algorithms are sha256 and sha512.` + ); + } + + if (!new RegExp(`^[a-f0-9]{${expectedLength}}$`).test(digest)) { + throw new Error( + `Malformed ${algorithm} checksum metadata${sanitizedSource(checksum.source)}: expected a ${expectedLength}-character hexadecimal digest.` + ); + } + + return digest; +} + +export async function calculateChecksum( + filePath: string, + algorithm: ChecksumMetadata['algorithm'] +): Promise { + const hash = createHash(algorithm); + await pipeline(createReadStream(filePath), hash); + return hash.digest('hex'); +} + +export async function verifyChecksum( + filePath: string, + checksum: ChecksumMetadata, + context: ChecksumVerificationContext +): Promise { + const expected = normalizeExpectedDigest(checksum); + const actual = await calculateChecksum(filePath, checksum.algorithm); + const matches = timingSafeEqual( + Buffer.from(expected, 'hex'), + Buffer.from(actual, 'hex') + ); + + if (!matches) { + throw new Error( + `Checksum verification failed for ${context.distribution} version ${context.version}: ${checksum.algorithm} expected ${expected}, actual ${actual}.` + ); + } +} diff --git a/src/distributions/adopt/installer.ts b/src/distributions/adopt/installer.ts index c02df473b..84bb4f2c5 100644 --- a/src/distributions/adopt/installer.ts +++ b/src/distributions/adopt/installer.ts @@ -105,7 +105,12 @@ export class AdoptDistribution extends JavaBase { .map(item => { return { version: item.version_data.semver, - url: item.binaries[0].package.link + url: item.binaries[0].package.link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } } as JavaDownloadRelease; }); @@ -133,7 +138,7 @@ export class AdoptDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index f9cb55c4b..4207ac3c6 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -17,6 +17,7 @@ import { import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants.js'; import {RetryingHttpClient} from '../retrying-http-client.js'; import os from 'os'; +import {verifyChecksum} from '../checksum.js'; export abstract class JavaBase { protected http: httpm.HttpClient; @@ -61,6 +62,39 @@ export abstract class JavaBase { range: string ): Promise; + protected async downloadAndVerify( + javaRelease: JavaDownloadRelease + ): Promise { + const archivePath = await tc.downloadTool(javaRelease.url); + if (!javaRelease.checksum) { + core.debug( + `No authoritative checksum is available for ${this.distribution} version ${javaRelease.version}; skipping checksum verification.` + ); + return archivePath; + } + + try { + await verifyChecksum(archivePath, javaRelease.checksum, { + distribution: this.distribution, + version: javaRelease.version + }); + core.debug( + `Verified ${javaRelease.checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.` + ); + return archivePath; + } catch (error) { + try { + await fs.promises.rm(archivePath, {force: true}); + } catch (cleanupError) { + throw new Error( + `${(error as Error).message} Failed to remove the downloaded archive after verification failure: ${(cleanupError as Error).message}`, + {cause: cleanupError} + ); + } + throw error; + } + } + public async setupJava(): Promise { if (this.verifySignature && !this.supportsSignatureVerification()) { throw new Error( diff --git a/src/distributions/base-models.ts b/src/distributions/base-models.ts index 3bfe4bb9c..2bec34f41 100644 --- a/src/distributions/base-models.ts +++ b/src/distributions/base-models.ts @@ -14,8 +14,17 @@ export interface JavaInstallerResults { path: string; } +export type ChecksumAlgorithm = 'sha256' | 'sha512'; + +export interface ChecksumMetadata { + algorithm: ChecksumAlgorithm; + value: string; + source?: string; +} + export interface JavaDownloadRelease { version: string; url: string; signatureUrl?: string; + checksum?: ChecksumMetadata; } diff --git a/src/distributions/corretto/installer.ts b/src/distributions/corretto/installer.ts index 66516a220..065420007 100644 --- a/src/distributions/corretto/installer.ts +++ b/src/distributions/corretto/installer.ts @@ -19,6 +19,9 @@ import { ICorrettoAvailableVersions } from './models.js'; +const CORRETTO_VERSIONS_URL = + 'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json'; + export class CorrettoDistribution extends JavaBase { constructor(installerOptions: JavaInstallerOptions) { super('Corretto', installerOptions); @@ -30,7 +33,7 @@ export class CorrettoDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); @@ -86,7 +89,12 @@ export class CorrettoDistribution extends JavaBase { .map(item => { return { version: convertVersionToSemver(item.correttoVersion), - url: item.downloadLink + url: item.downloadLink, + checksum: { + algorithm: 'sha256', + value: item.checksum_sha256, + source: CORRETTO_VERSIONS_URL + } } as JavaDownloadRelease; }); @@ -110,16 +118,14 @@ export class CorrettoDistribution extends JavaBase { console.time('Retrieving available versions for Corretto took'); // eslint-disable-line no-console } - const availableVersionsUrl = - 'https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json'; const fetchCurrentVersions = await this.http.getJson( - availableVersionsUrl + CORRETTO_VERSIONS_URL ); const fetchedCurrentVersions = fetchCurrentVersions.result; if (!fetchedCurrentVersions) { throw Error( - `Could not fetch latest corretto versions from ${availableVersionsUrl}` + `Could not fetch latest corretto versions from ${CORRETTO_VERSIONS_URL}` ); } diff --git a/src/distributions/dragonwell/installer.ts b/src/distributions/dragonwell/installer.ts index 1f8df52ba..ad042a4d5 100644 --- a/src/distributions/dragonwell/installer.ts +++ b/src/distributions/dragonwell/installer.ts @@ -46,7 +46,13 @@ export class DragonwellDistribution extends JavaBase { .map(item => { return { version: item.jdk_version, - url: item.download_link + url: item.download_link, + checksum: item.checksum + ? { + algorithm: 'sha256', + value: item.checksum + } + : undefined } as JavaDownloadRelease; }); @@ -102,7 +108,7 @@ export class DragonwellDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts index d2255a22b..055caba41 100644 --- a/src/distributions/graalvm/installer.ts +++ b/src/distributions/graalvm/installer.ts @@ -66,7 +66,7 @@ export class GraalVMDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/jetbrains/installer.ts b/src/distributions/jetbrains/installer.ts index a93e43b59..d4f7647cd 100644 --- a/src/distributions/jetbrains/installer.ts +++ b/src/distributions/jetbrains/installer.ts @@ -60,7 +60,7 @@ export class JetBrainsDistribution extends JavaBase { `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - const javaArchivePath = await tc.downloadTool(javaRelease.url); + const javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extractedJavaPath = await extractJdkFile(javaArchivePath, 'tar.gz'); diff --git a/src/distributions/kona/installer.ts b/src/distributions/kona/installer.ts index f7992f600..f45accd44 100644 --- a/src/distributions/kona/installer.ts +++ b/src/distributions/kona/installer.ts @@ -19,6 +19,9 @@ import { renameWinArchive } from '../../util.js'; +const KONA_RELEASES_URL = + 'https://tencent.github.io/konajdk/releases/kona-v1.json'; + export class KonaDistribution extends JavaBase { constructor(installerOptions: JavaInstallerOptions) { super('Kona', installerOptions); @@ -30,7 +33,7 @@ export class KonaDistribution extends JavaBase { core.info( `Downloading Kona JDK ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - const javaArchivePath = await tc.downloadTool(javaRelease.url); + const javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); @@ -74,7 +77,14 @@ export class KonaDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadUrl + url: item.downloadUrl, + checksum: item.checksum + ? { + algorithm: 'sha256', + value: item.checksum, + source: KONA_RELEASES_URL + } + : undefined } as JavaDownloadRelease; }) .sort((a, b) => -semver.compareBuild(a.version, b.version)); @@ -115,16 +125,13 @@ export class KonaDistribution extends JavaBase { } private async fetchReleaseInfo(): Promise { - const releasesInfoUrl = - 'https://tencent.github.io/konajdk/releases/kona-v1.json'; - try { - core.debug(`Fetching Kona release info from URL: ${releasesInfoUrl}`); - return (await this.http.getJson(releasesInfoUrl)) + core.debug(`Fetching Kona release info from URL: ${KONA_RELEASES_URL}`); + return (await this.http.getJson(KONA_RELEASES_URL)) .result; } catch (err) { core.debug( - `Fetching Kona release info from the URL: ${releasesInfoUrl} failed with the error: ${ + `Fetching Kona release info from the URL: ${KONA_RELEASES_URL} failed with the error: ${ (err as Error).message }` ); diff --git a/src/distributions/liberica-nik/installer.ts b/src/distributions/liberica-nik/installer.ts index 2187a3cfa..d38af995a 100644 --- a/src/distributions/liberica-nik/installer.ts +++ b/src/distributions/liberica-nik/installer.ts @@ -32,7 +32,7 @@ export class LibericaNikDistributions extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/liberica/installer.ts b/src/distributions/liberica/installer.ts index eb2254d2e..3db9c6200 100644 --- a/src/distributions/liberica/installer.ts +++ b/src/distributions/liberica/installer.ts @@ -32,7 +32,7 @@ export class LibericaDistributions extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/microsoft/installer.ts b/src/distributions/microsoft/installer.ts index 3a25186a6..f3f3e9f6a 100644 --- a/src/distributions/microsoft/installer.ts +++ b/src/distributions/microsoft/installer.ts @@ -31,7 +31,7 @@ export class MicrosoftDistributions extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); if (this.verifySignature) { if (!javaRelease.signatureUrl) { diff --git a/src/distributions/openjdk/installer.ts b/src/distributions/openjdk/installer.ts index 8c8abea69..24a102a5c 100644 --- a/src/distributions/openjdk/installer.ts +++ b/src/distributions/openjdk/installer.ts @@ -59,7 +59,7 @@ export class OpenJdkDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = javaRelease.url.endsWith('.zip') ? 'zip' : 'tar.gz'; diff --git a/src/distributions/oracle/installer.ts b/src/distributions/oracle/installer.ts index 559718704..51ab8690b 100644 --- a/src/distributions/oracle/installer.ts +++ b/src/distributions/oracle/installer.ts @@ -32,7 +32,7 @@ export class OracleDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/sapmachine/installer.ts b/src/distributions/sapmachine/installer.ts index 6833067ec..ad82af86f 100644 --- a/src/distributions/sapmachine/installer.ts +++ b/src/distributions/sapmachine/installer.ts @@ -44,7 +44,11 @@ export class SapMachineDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadLink + url: item.downloadLink, + checksum: { + algorithm: 'sha256', + value: item.checksum + } } as JavaDownloadRelease; }); @@ -104,7 +108,7 @@ export class SapMachineDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/semeru/installer.ts b/src/distributions/semeru/installer.ts index 4f1ee7587..2668a7fe5 100644 --- a/src/distributions/semeru/installer.ts +++ b/src/distributions/semeru/installer.ts @@ -69,7 +69,12 @@ export class SemeruDistribution extends JavaBase { : item.version_data.semver.replace('-beta+', '+'); return { version: formattedVersion, - url: item.binaries[0].package.link + url: item.binaries[0].package.link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } } as JavaDownloadRelease; }); @@ -104,7 +109,7 @@ export class SemeruDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 8064e837b..4762b0cae 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -69,7 +69,12 @@ export class TemurinDistribution extends JavaBase { return { version: formattedVersion, url: item.binaries[0].package.link, - signatureUrl: item.binaries[0].package.signature_link + signatureUrl: item.binaries[0].package.signature_link, + checksum: { + algorithm: 'sha256', + value: item.binaries[0].package.checksum, + source: item.binaries[0].package.checksum_link + } } as JavaDownloadRelease; }); @@ -132,7 +137,7 @@ export class TemurinDistribution extends JavaBase { } private async downloadPackage(release: JavaDownloadRelease): Promise { - const archivePath = await tc.downloadTool(release.url); + const archivePath = await this.downloadAndVerify(release); if (this.verifySignature) { if (!release.signatureUrl) { diff --git a/src/distributions/zulu/installer.ts b/src/distributions/zulu/installer.ts index f587b193d..04b1e15ab 100644 --- a/src/distributions/zulu/installer.ts +++ b/src/distributions/zulu/installer.ts @@ -79,7 +79,7 @@ export class ZuluDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); + let javaArchivePath = await this.downloadAndVerify(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); From 92e4c59972f92c3ee92e03c784fac6d1a13498cb Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 01:17:50 -0400 Subject: [PATCH 2/8] Handle missing vendor checksum values Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- __tests__/distributors/base-installer.test.ts | 22 +++++++++++++++++++ dist/setup/index.js | 7 +++--- src/distributions/base-installer.ts | 7 +++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index a6bb1e155..35cfa68c6 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -893,6 +893,28 @@ describe('downloadAndVerify', () => { 'No authoritative checksum is available for Empty version 21.0.8; skipping checksum verification.' ); }); + + it.each([undefined, '', ' '])( + 'skips verification when the vendor digest is %p', + async value => { + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.downloadRelease({ + version: '21.0.8', + url: 'https://vendor.example/jdk.tar.gz', + checksum: { + algorithm: 'sha256', + value + } as JavaDownloadRelease['checksum'] + }) + ).resolves.toBe(archivePath); + + expect(core.debug).toHaveBeenCalledWith( + 'No authoritative checksum is available for Empty version 21.0.8; skipping checksum verification.' + ); + } + ); }); describe('normalizeVersion', () => { diff --git a/dist/setup/index.js b/dist/setup/index.js index f3f3bc2b4..523995320 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129501,16 +129501,17 @@ class JavaBase { } async downloadAndVerify(javaRelease) { const archivePath = await downloadTool(javaRelease.url); - if (!javaRelease.checksum) { + const checksum = javaRelease.checksum; + if (!checksum || !checksum.value?.trim()) { core_debug(`No authoritative checksum is available for ${this.distribution} version ${javaRelease.version}; skipping checksum verification.`); return archivePath; } try { - await verifyChecksum(archivePath, javaRelease.checksum, { + await verifyChecksum(archivePath, checksum, { distribution: this.distribution, version: javaRelease.version }); - core_debug(`Verified ${javaRelease.checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.`); + core_debug(`Verified ${checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.`); return archivePath; } catch (error) { diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 4207ac3c6..56c3b9b3d 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -66,7 +66,8 @@ export abstract class JavaBase { javaRelease: JavaDownloadRelease ): Promise { const archivePath = await tc.downloadTool(javaRelease.url); - if (!javaRelease.checksum) { + const checksum = javaRelease.checksum; + if (!checksum || !checksum.value?.trim()) { core.debug( `No authoritative checksum is available for ${this.distribution} version ${javaRelease.version}; skipping checksum verification.` ); @@ -74,12 +75,12 @@ export abstract class JavaBase { } try { - await verifyChecksum(archivePath, javaRelease.checksum, { + await verifyChecksum(archivePath, checksum, { distribution: this.distribution, version: javaRelease.version }); core.debug( - `Verified ${javaRelease.checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.` + `Verified ${checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.` ); return archivePath; } catch (error) { From 5f82de776a101b36eed702077391cc07ebe40a26 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 01:22:23 -0400 Subject: [PATCH 3/8] Preserve checksum error during cleanup failure Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- __tests__/distributors/base-installer.test.ts | 23 +++++++++++++++++++ src/distributions/base-installer.ts | 10 ++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index 35cfa68c6..cd0587bd6 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -879,6 +879,29 @@ describe('downloadAndVerify', () => { expect(fs.existsSync(archivePath)).toBe(false); }); + it('preserves the verification error when removing the download fails', async () => { + const distribution = new EmptyJavaBase(options); + const cleanupError = new Error('cleanup failed'); + jest.spyOn(fs.promises, 'rm').mockRejectedValueOnce(cleanupError); + + const result = distribution.downloadRelease({ + version: '21.0.8', + url: 'https://vendor.example/jdk.tar.gz', + checksum: {algorithm: 'sha256', value: 'a'.repeat(64)} + }); + + await expect(result).rejects.toMatchObject({ + message: expect.stringContaining( + 'Failed to remove the downloaded archive after verification failure: cleanup failed' + ), + cause: expect.objectContaining({ + message: expect.stringContaining( + 'Checksum verification failed for Empty version 21.0.8' + ) + }) + }); + }); + it('logs when authoritative checksum metadata is unavailable', async () => { const distribution = new EmptyJavaBase(options); diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 56c3b9b3d..874b6c4ea 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -84,12 +84,18 @@ export abstract class JavaBase { ); return archivePath; } catch (error) { + let cleanupError: unknown; + let cleanupFailed = false; try { await fs.promises.rm(archivePath, {force: true}); - } catch (cleanupError) { + } catch (caughtCleanupError) { + cleanupError = caughtCleanupError; + cleanupFailed = true; + } + if (cleanupFailed) { throw new Error( `${(error as Error).message} Failed to remove the downloaded archive after verification failure: ${(cleanupError as Error).message}`, - {cause: cleanupError} + {cause: error} ); } throw error; From eb35a583586bb1b895a835a3515a907885cae955 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 01:25:12 -0400 Subject: [PATCH 4/8] Validate checksum metadata value types Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- __tests__/checksum.test.ts | 19 +++++++++++++++++++ dist/setup/index.js | 14 +++++++++++--- src/checksum.ts | 5 ++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/__tests__/checksum.test.ts b/__tests__/checksum.test.ts index 2c5e13ed2..b8e3f0185 100644 --- a/__tests__/checksum.test.ts +++ b/__tests__/checksum.test.ts @@ -74,6 +74,25 @@ describe('verifyChecksum', () => { ); }); + it.each([undefined, null, 123])( + 'reports a malformed digest when the value is %p', + async value => { + const checksum = { + algorithm: 'sha256', + value + } as unknown as ChecksumMetadata; + + await expect( + verifyChecksum('/missing/archive', checksum, { + distribution: 'Test', + version: '17' + }) + ).rejects.toThrow( + 'Malformed sha256 checksum metadata: expected a 64-character hexadecimal digest.' + ); + } + ); + it('rejects unsupported algorithms without leaking source query parameters', async () => { const checksum = { algorithm: 'md5', diff --git a/dist/setup/index.js b/dist/setup/index.js index 523995320..54108ba10 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129431,7 +129431,9 @@ function sanitizedSource(source) { } function normalizeExpectedDigest(checksum) { const algorithm = checksum.algorithm; - const digest = checksum.value.trim().toLowerCase(); + const digest = typeof checksum.value === 'string' + ? checksum.value.trim().toLowerCase() + : ''; const expectedLength = algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; if (expectedLength === 0) { throw new Error(`Unsupported checksum algorithm '${String(algorithm)}'${sanitizedSource(checksum.source)}. Supported algorithms are sha256 and sha512.`); @@ -129515,11 +129517,17 @@ class JavaBase { return archivePath; } catch (error) { + let cleanupError; + let cleanupFailed = false; try { await external_fs_namespaceObject.promises.rm(archivePath, { force: true }); } - catch (cleanupError) { - throw new Error(`${error.message} Failed to remove the downloaded archive after verification failure: ${cleanupError.message}`, { cause: cleanupError }); + catch (caughtCleanupError) { + cleanupError = caughtCleanupError; + cleanupFailed = true; + } + if (cleanupFailed) { + throw new Error(`${error.message} Failed to remove the downloaded archive after verification failure: ${cleanupError.message}`, { cause: error }); } throw error; } diff --git a/src/checksum.ts b/src/checksum.ts index d7baba48a..d80efd4d8 100644 --- a/src/checksum.ts +++ b/src/checksum.ts @@ -24,7 +24,10 @@ function sanitizedSource(source: string | undefined): string { function normalizeExpectedDigest(checksum: ChecksumMetadata): string { const algorithm = checksum.algorithm; - const digest = checksum.value.trim().toLowerCase(); + const digest = + typeof checksum.value === 'string' + ? checksum.value.trim().toLowerCase() + : ''; const expectedLength = algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; From b48d411d527db424d9ab5091721cae6eb2408bd6 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 01:27:32 -0400 Subject: [PATCH 5/8] Clarify checksum documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- README.md | 4 ++-- action.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index acdb5f81b..fced83753 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ For more details, see the full release notes on the [releases page](https://git - `problem-matcher`: Set to `false` to disable Java problem matcher annotations (compiler diagnostics and uncaught exceptions). Default value: `true`. See [Java problem matcher](docs/advanced-usage.md#java-problem-matcher-compiler-annotations) for details and annotation limits. - - `verify-signature`: Verifies downloaded Java package signatures when supported by the selected distribution. Currently supported for `temurin` and `microsoft`. Signature verification provides an authenticity check in addition to automatic checksum verification. If set to `true` for unsupported distributions, the action fails. + - `verify-signature`: Verifies downloaded Java package signatures when supported by the selected distribution. Currently supported for `temurin` and `microsoft`. If set to `true` for unsupported distributions, the action fails. - `verify-signature-public-key`: ASCII-armored GPG public key used to verify the downloaded package signature. Overrides the default bundled key for the selected distribution. @@ -101,7 +101,7 @@ When a selected distribution publishes an authoritative checksum in its release Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported only in debug logs. Archives resolved directly from the runner tool cache are not downloaded again and therefore are not reverified. -Checksums detect corrupted or unexpectedly modified downloads, while GPG signatures additionally authenticate the publisher. For supported distributions, enable `verify-signature` when that stronger authenticity guarantee is required; signature verification complements rather than replaces the automatic checksum check. +Checksums detect corrupted or unexpectedly modified downloads before they are persisted in the runner tool cache. ### Basic Configuration diff --git a/action.yml b/action.yml index f52a69596..d980a9dd2 100644 --- a/action.yml +++ b/action.yml @@ -39,7 +39,7 @@ inputs: required: false default: true verify-signature: - description: 'Verify downloaded Java package signatures, in addition to automatic checksum verification, when supported by the selected distribution' + description: 'Verify downloaded Java package signatures when supported by the selected distribution' required: false default: false verify-signature-public-key: From a17e5f7a6b430351ce280dd6a0099c75ab60e8cb Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 02:13:32 -0400 Subject: [PATCH 6/8] Expand vendor checksum verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- README.md | 2 +- __tests__/distributors/base-installer.test.ts | 121 ++++++++++++++++ .../distributors/graalvm-installer.test.ts | 133 +++++++++++++++++- .../distributors/jetbrains-installer.test.ts | 62 ++++++++ .../distributors/microsoft-installer.test.ts | 40 ++++++ .../distributors/openjdk-installer.test.ts | 36 ++++- .../distributors/oracle-installer.test.ts | 32 +++++ __tests__/distributors/zulu-installer.test.ts | 52 +++++++ .../distributors/zulu-linux-installer.test.ts | 52 +++++++ .../zulu-windows-installer.test.ts | 52 +++++++ dist/setup/index.js | 100 +++++++++++-- src/distributions/base-installer.ts | 40 ++++++ src/distributions/graalvm/installer.ts | 25 +++- src/distributions/jetbrains/installer.ts | 8 +- src/distributions/microsoft/installer.ts | 6 +- src/distributions/openjdk/installer.ts | 6 +- src/distributions/oracle/installer.ts | 6 +- src/distributions/zulu/installer.ts | 49 +++++-- src/distributions/zulu/models.ts | 4 + 19 files changed, 785 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index fced83753..bebb93861 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ For more details, see the full release notes on the [releases page](https://git ### Download integrity verification -When a selected distribution publishes an authoritative checksum in its release metadata, `setup-java` automatically verifies each downloaded JDK, JRE, or JMOD archive before extraction and caching. No input is required. Automatic checksum verification is currently available for `temurin`, `semeru`, `adopt`, `corretto`, `dragonwell`, `kona`, and `sapmachine`. +When a selected distribution publishes an authoritative checksum for an archive, `setup-java` automatically verifies each downloaded JDK, JRE, or JMOD archive before extraction and caching. No input is required. Automatic checksum verification is currently available for `temurin`, `semeru`, `adopt`, `corretto`, `dragonwell`, `kona`, `sapmachine`, `graalvm`, `graalvm-community`, `zulu`, `oracle`, `oracle-openjdk`, `microsoft`, and `jetbrains`. Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported only in debug logs. Archives resolved directly from the runner tool cache are not downloaded again and therefore are not reverified. diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index cd0587bd6..c05892a4f 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -18,6 +18,7 @@ import path from 'path'; import * as semver from 'semver'; import fs from 'fs'; import {createHash} from 'crypto'; +import {HttpClient} from '@actions/http-client'; import os from 'os'; @@ -123,6 +124,13 @@ class EmptyJavaBase extends JavaBase { public downloadRelease(javaRelease: JavaDownloadRelease): Promise { return this.downloadAndVerify(javaRelease); } + + public fetchChecksumForTest( + checksumUrl: string, + algorithm: 'sha256' | 'sha512' + ) { + return this.fetchChecksum(checksumUrl, algorithm); + } } describe('findInToolcache', () => { @@ -940,6 +948,119 @@ describe('downloadAndVerify', () => { ); }); +describe('fetchChecksum', () => { + const options: JavaInstallerOptions = { + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }; + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function mockGet(statusCode: number, body: string) { + return jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ + message: {statusCode}, + readBody: async () => body + } as any); + } + + it('parses a bare hex digest', async () => { + const digest = 'a'.repeat(64); + const spy = mockGet(200, digest); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.sha256', + 'sha256' + ); + + expect(spy).toHaveBeenCalledWith( + 'https://vendor.example/jdk.tar.gz.sha256' + ); + expect(checksum).toEqual({ + algorithm: 'sha256', + value: digest, + source: 'https://vendor.example/jdk.tar.gz.sha256' + }); + }); + + it('parses only the first token of a GNU-style checksum file', async () => { + const digest = 'b'.repeat(128); + mockGet(200, `${digest} jbrsdk-21.0.3-linux-x64-b465.3.tar.gz\n`); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.checksum', + 'sha512' + ); + + expect(checksum).toEqual({ + algorithm: 'sha512', + value: digest, + source: 'https://vendor.example/jdk.tar.gz.checksum' + }); + }); + + it('trims surrounding whitespace and newlines', async () => { + const digest = 'c'.repeat(64); + mockGet(200, `\n ${digest} \n`); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.sha256', + 'sha256' + ); + + expect(checksum.value).toBe(digest); + }); + + it('skips verification when the sibling checksum is not published', async () => { + mockGet(404, 'Not Found'); + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.sha256', + 'sha256' + ) + ).resolves.toBeUndefined(); + expect(core.debug).toHaveBeenCalledWith( + 'No authoritative sha256 checksum is available for Empty from https://vendor.example/jdk.tar.gz.sha256; skipping checksum verification.' + ); + }); + + it('surfaces unexpected HTTP failures without query parameters', async () => { + mockGet(500, 'Server Error'); + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.sha256?token=secret', + 'sha256' + ) + ).rejects.toThrow( + 'Failed to fetch the authoritative sha256 checksum for Empty from https://vendor.example/jdk.tar.gz.sha256 (HTTP 500).' + ); + }); + + it('rejects an empty successful checksum response', async () => { + mockGet(200, ' \n'); + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.fetchChecksumForTest( + 'https://vendor.example/jdk.tar.gz.sha256', + 'sha256' + ) + ).rejects.toThrow( + 'Received an empty authoritative sha256 checksum for Empty from https://vendor.example/jdk.tar.gz.sha256.' + ); + }); +}); + describe('normalizeVersion', () => { const DummyJavaBase = JavaBase as any; diff --git a/__tests__/distributors/graalvm-installer.test.ts b/__tests__/distributors/graalvm-installer.test.ts index 0882c6ca4..bb80230de 100644 --- a/__tests__/distributors/graalvm-installer.test.ts +++ b/__tests__/distributors/graalvm-installer.test.ts @@ -129,6 +129,14 @@ describe('GraalVMDistribution', () => { (distribution as any).http = mockHttpClient; (communityDistribution as any).http = mockHttpClient; + // Default checksum sibling response for `${url}.sha256` requests made by + // GraalVM (Oracle) and GraalVM EA. Individual tests override this when + // they need to assert the exact URL/digest contract. + mockHttpClient.get.mockResolvedValue({ + message: {statusCode: 200}, + readBody: jest.fn().mockResolvedValue('a'.repeat(64)) + }); + (util.getDownloadArchiveExtension as jest.Mock).mockReturnValue( 'tar.gz' ); @@ -407,9 +415,16 @@ describe('GraalVMDistribution', () => { expect(result).toEqual({ url: 'https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.5_linux-x64_bin.tar.gz', - version: '17.0.5' + version: '17.0.5', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.5_linux-x64_bin.tar.gz.sha256' + } }); expect(mockHttpClient.head).toHaveBeenCalledWith(result.url); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`); }); it('should construct correct URL for major version (latest)', async () => { @@ -422,7 +437,13 @@ describe('GraalVMDistribution', () => { expect(result).toEqual({ url: 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz', - version: '21' + version: '21', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz.sha256' + } }); }); @@ -465,7 +486,13 @@ describe('GraalVMDistribution', () => { expect(result).toEqual({ url: 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz', - version: '25' + version: '25', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://download.oracle.com/graalvm/25/latest/graalvm-jdk-25_linux-x64_bin.tar.gz.sha256' + } }); }); @@ -637,13 +664,20 @@ describe('GraalVMDistribution', () => { expect(result).toEqual({ url: 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz', - version: '23-ea-20240716' + version: '23-ea-20240716', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz.sha256' + } }); expect(mockHttpClient.getJson).toHaveBeenCalledWith( 'https://api.github.com/repos/graalvm/oracle-graalvm-ea-builds/contents/versions/23-ea.json?ref=main', {Accept: 'application/json'} ); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`); }); it('should throw error when no latest EA version found', async () => { @@ -876,8 +910,15 @@ describe('GraalVMDistribution', () => { expect(fetchEASpy).toHaveBeenCalledWith('23-ea'); expect(result).toEqual({ url: 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz', - version: '23-ea-20240716' + version: '23-ea-20240716', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://example.com/download/graalvm-jdk-23_linux-x64_bin.tar.gz.sha256' + } }); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${result.url}.sha256`); // Verify debug logging expect(core.debug).toHaveBeenCalledWith('Searching for EA build: 23-ea'); @@ -976,7 +1017,13 @@ describe('GraalVMDistribution', () => { expect(result).toEqual({ url: 'https://example.com/download/graalvm-jdk-23_linux-aarch64_bin.tar.gz', - version: '23-ea-20240716' + version: '23-ea-20240716', + checksum: { + algorithm: 'sha256', + value: 'a'.repeat(64), + source: + 'https://example.com/download/graalvm-jdk-23_linux-aarch64_bin.tar.gz.sha256' + } }); }); @@ -1151,6 +1198,80 @@ describe('GraalVMDistribution', () => { url: 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', version: '21.0.2' }); + // The asset had no `digest` field, so no checksum should be attached, + // and the checksum sibling-URL fetch path (used by Oracle GraalVM) + // must not be consulted for GraalVM Community. + expect(result.checksum).toBeUndefined(); + expect(mockHttpClient.get).not.toHaveBeenCalled(); + }); + + it('strips the `sha256:` prefix from a GitHub release asset digest', async () => { + const digest = 'd'.repeat(64); + mockHttpClient.getJson.mockResolvedValue({ + result: [ + { + draft: false, + prerelease: false, + assets: [ + { + name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', + browser_download_url: + 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', + digest: `sha256:${digest}` + } + ] + } + ], + statusCode: 200, + headers: {} + }); + + const result = await ( + communityDistribution as any + ).findPackageForDownload('21.0.2'); + + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: digest, + source: + 'https://api.github.com/repos/graalvm/graalvm-ce-builds/releases?per_page=100' + }); + // The digest came from the release listing itself, so no additional + // HTTP request should be made to resolve the checksum. + expect(mockHttpClient.get).not.toHaveBeenCalled(); + }); + + it('safely skips a missing or malformed release asset digest', async () => { + mockHttpClient.getJson.mockResolvedValue({ + result: [ + { + draft: false, + prerelease: false, + assets: [ + { + name: 'graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', + browser_download_url: + 'https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz', + digest: 'md5:not-a-sha256-digest' + } + ] + } + ], + statusCode: 200, + headers: {} + }); + + const result = await ( + communityDistribution as any + ).findPackageForDownload('21.0.2'); + + expect(result.checksum).toBeUndefined(); + expect(mockHttpClient.get).not.toHaveBeenCalled(); + expect(core.debug).toHaveBeenCalledWith( + expect.stringContaining( + 'No authoritative sha256 digest is available for graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz' + ) + ); }); it('should resolve the latest GraalVM Community release for a major version', async () => { diff --git a/__tests__/distributors/jetbrains-installer.test.ts b/__tests__/distributors/jetbrains-installer.test.ts index 8d5e1a12d..e093dba13 100644 --- a/__tests__/distributors/jetbrains-installer.test.ts +++ b/__tests__/distributors/jetbrains-installer.test.ts @@ -148,6 +148,27 @@ describe('getAvailableVersions', () => { }); describe('findPackageForDownload', () => { + let spyHttpClientGet: any; + + const JETBRAINS_CHECKSUM = 'c'.repeat(128); + + beforeEach(() => { + // Every resolved release fetches `${url}.checksum` (sha512, GNU + // ` ` format); stub it so tests never reach the real + // network, except the dedicated 'version %s can be downloaded' test + // below which intentionally exercises real HTTPS HEAD requests. + spyHttpClientGet = jest + .spyOn(HttpClient.prototype, 'get') + .mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => `${JETBRAINS_CHECKSUM} jbrsdk.tar.gz\n` + } as any); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it.each([ ['17', '17.0.11+1207.24'], ['11.0', '11.0.16+2043.64'], @@ -231,4 +252,45 @@ describe('findPackageForDownload', () => { /No matching version found for SemVer */ ); }); + + it('fetches the authoritative sha512 checksum only for the resolved version', async () => { + const distribution = new JetBrainsDistribution({ + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData as any; + + const result = await distribution['findPackageForDownload']('21'); + + expect(result.checksum).toEqual({ + algorithm: 'sha512', + value: JETBRAINS_CHECKSUM, + source: `${result.url}.checksum` + }); + // Only the single resolved/winning version's checksum is requested, + // not one per candidate considered during version resolution. + expect(spyHttpClientGet).toHaveBeenCalledWith(`${result.url}.checksum`); + expect(spyHttpClientGet).toHaveBeenCalledTimes(1); + }); + + it('parses only the first whitespace-delimited token from the GNU checksum payload', async () => { + spyHttpClientGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => `${JETBRAINS_CHECKSUM} jbrsdk-21.tar.gz\n` + } as any); + + const distribution = new JetBrainsDistribution({ + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData as any; + + const result = await distribution['findPackageForDownload']('21'); + + expect(result.checksum?.value).toBe(JETBRAINS_CHECKSUM); + }); }); diff --git a/__tests__/distributors/microsoft-installer.test.ts b/__tests__/distributors/microsoft-installer.test.ts index f1aca7780..0dc0ea532 100644 --- a/__tests__/distributors/microsoft-installer.test.ts +++ b/__tests__/distributors/microsoft-installer.test.ts @@ -103,9 +103,12 @@ const util = await import('../../src/util.js'); describe('findPackageForDownload', () => { let distribution: InstanceType; let spyGetManifestFromRepo: any; + let spyHttpClientGet: any; let spyDebug: any; let spyCoreError: any; + const MICROSOFT_CHECKSUM = 'b'.repeat(64); + beforeEach(() => { mockOsArch.mockReturnValue('x64'); mockOsPlatform.mockReturnValue(process.platform); @@ -124,6 +127,15 @@ describe('findPackageForDownload', () => { headers: {} }); + // Every resolved release fetches `${download_url}.sha256sum.txt`; stub + // it with a GNU-style ` ` payload so tests never reach + // the real network. + spyHttpClientGet = jest.spyOn(HttpClient.prototype, 'get'); + spyHttpClientGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => `${MICROSOFT_CHECKSUM} microsoft-jdk.tar.gz\n` + }); + spyDebug = core.debug as jest.Mock; spyDebug.mockImplementation(() => {}); @@ -311,6 +323,34 @@ describe('findPackageForDownload', () => { 'https://example.test/jdk.tar.gz.custom.sig' ); }); + + it('fetches the authoritative sha256 checksum from the GNU-style sibling file', async () => { + mockOsPlatform.mockReturnValue(process.platform); + + const result = await distribution['findPackageForDownload']('17.0.7'); + + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: MICROSOFT_CHECKSUM, + source: `${result.url}.sha256sum.txt` + }); + expect(spyHttpClientGet).toHaveBeenCalledWith( + `${result.url}.sha256sum.txt` + ); + expect(spyHttpClientGet).toHaveBeenCalledTimes(1); + }); + + it('parses only the first whitespace-delimited token from the GNU checksum payload', async () => { + spyHttpClientGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => + `${MICROSOFT_CHECKSUM} microsoft-jdk-17.0.7-linux-x64.tar.gz\n` + }); + + const result = await distribution['findPackageForDownload']('17.0.7'); + + expect(result.checksum?.value).toBe(MICROSOFT_CHECKSUM); + }); }); describe('downloadTool', () => { diff --git a/__tests__/distributors/openjdk-installer.test.ts b/__tests__/distributors/openjdk-installer.test.ts index 48720a410..65b7e2c4d 100644 --- a/__tests__/distributors/openjdk-installer.test.ts +++ b/__tests__/distributors/openjdk-installer.test.ts @@ -50,6 +50,14 @@ const archivePage = ` 9.0.4 (build 9.0.4+11) tar.gz `; +const GA_CHECKSUM = 'c'.repeat(64); +const EA_CHECKSUM = 'd'.repeat(64); +const checksumPages: Record = { + 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz.sha256': + GA_CHECKSUM, + 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz.sha256': + EA_CHECKSUM +}; function createDistribution( version = '26', @@ -82,8 +90,16 @@ describe('OpenJdkDistribution', () => { 'https://jdk.java.net/27/': earlyAccessPage, 'https://jdk.java.net/archive/': archivePage }; + if (url in pages) { + return { + message: {statusCode: 200}, + readBody: async () => pages[url] + } as Awaited>; + } + // Any other GET is a `${archiveUrl}.sha256` checksum sibling request. return { - readBody: async () => pages[url] ?? '' + message: {statusCode: 200}, + readBody: async () => checksumPages[url] ?? 'e'.repeat(64) } as Awaited>; }); }); @@ -97,8 +113,15 @@ describe('OpenJdkDistribution', () => { expect(result).toEqual({ version: '26.0.2+10', - url: 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz' + url: 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz', + checksum: { + algorithm: 'sha256', + value: GA_CHECKSUM, + source: + 'https://download.java.net/java/GA/jdk26.0.2/hash/10/GPL/openjdk-26.0.2_linux-x64_bin.tar.gz.sha256' + } }); + expect(getSpy).toHaveBeenCalledWith(`${result.url}.sha256`); }); it('resolves an archived GA release', async () => { @@ -144,9 +167,16 @@ describe('OpenJdkDistribution', () => { expect(result).toEqual({ version: '27.0.0+32', - url: 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz' + url: 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz', + checksum: { + algorithm: 'sha256', + value: EA_CHECKSUM, + source: + 'https://download.java.net/java/early_access/jdk27/32/GPL/openjdk-27-ea+32_linux-x64_bin.tar.gz.sha256' + } }); expect(getSpy).not.toHaveBeenCalledWith('https://jdk.java.net/archive/'); + expect(getSpy).toHaveBeenCalledWith(`${result.url}.sha256`); }); it('reports available versions when no release matches', async () => { diff --git a/__tests__/distributors/oracle-installer.test.ts b/__tests__/distributors/oracle-installer.test.ts index b5cb7989b..7110f6385 100644 --- a/__tests__/distributors/oracle-installer.test.ts +++ b/__tests__/distributors/oracle-installer.test.ts @@ -47,8 +47,11 @@ describe('findPackageForDownload', () => { let distribution: InstanceType; let spyDebug: any; let spyHttpClient: any; + let spyHttpClientGet: any; let spyCoreError: any; + const ORACLE_CHECKSUM = 'f'.repeat(64); + beforeEach(() => { distribution = new OracleDistribution({ version: '', @@ -63,6 +66,14 @@ describe('findPackageForDownload', () => { // Mock core.error to suppress error logs spyCoreError = core.error as jest.Mock; spyCoreError.mockImplementation(() => {}); + + // Every resolved release fetches its `${url}.sha256` sibling checksum; + // stub it so tests never reach the real network. + spyHttpClientGet = jest.spyOn(HttpClient.prototype, 'get'); + spyHttpClientGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => ORACLE_CHECKSUM + }); }); it.each([ @@ -133,6 +144,23 @@ describe('findPackageForDownload', () => { expect(result.url).toBe(url); }); + it('fetches the authoritative sha256 checksum for the resolved archive', async () => { + spyHttpClient = jest.spyOn(HttpClient.prototype, 'head'); + spyHttpClient.mockResolvedValue({message: {statusCode: 200}}); + + const result = await distribution['findPackageForDownload']('21'); + + jest.restoreAllMocks(); + + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: ORACLE_CHECKSUM, + source: `${result.url}.sha256` + }); + expect(spyHttpClientGet).toHaveBeenCalledWith(`${result.url}.sha256`); + expect(spyHttpClientGet).toHaveBeenCalledTimes(1); + }); + it.each([ ['amd64', 'x64'], ['arm64', 'aarch64'] @@ -196,6 +224,10 @@ describe('findPackageForDownload with latest', () => { it('resolves the newest major version from the Adoptium API', async () => { spyHttpClientHead = jest.spyOn(HttpClient.prototype, 'head'); spyHttpClientHead.mockResolvedValue({message: {statusCode: 200}}); + jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => 'f'.repeat(64) + } as any); const distribution = new OracleDistribution({ version: 'latest', diff --git a/__tests__/distributors/zulu-installer.test.ts b/__tests__/distributors/zulu-installer.test.ts index 863a1b795..30125b7d5 100644 --- a/__tests__/distributors/zulu-installer.test.ts +++ b/__tests__/distributors/zulu-installer.test.ts @@ -241,6 +241,26 @@ describe('getArchitectureOptions', () => { }); describe('findPackageForDownload', () => { + let spyPackageDetails: any; + + const ZULU_CHECKSUM = 'a'.repeat(64); + + beforeEach(() => { + // The resolved winning package fetches sha256_hash from the Azul + // package-details endpoint; stub it so tests never reach the real + // network. + spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson'); + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {sha256_hash: ZULU_CHECKSUM} + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it.each([ ['8', '8.0.282+8'], ['11.x', '11.0.10+9'], @@ -279,6 +299,38 @@ describe('findPackageForDownload', () => { expect(result.url).toBe( 'https://cdn.azul.com/zulu/bin/zulu11.35.15-ca-jdk11.0.5-macosx_x64.tar.gz' ); + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: ZULU_CHECKSUM, + source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-10933' + }); + // Only the winning package's UUID triggers a details request. + expect(spyPackageDetails).toHaveBeenCalledWith( + 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-10933' + ); + expect(spyPackageDetails).toHaveBeenCalledTimes(1); + }); + + it('skips checksum verification when sha256_hash is missing or malformed', async () => { + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {sha256_hash: 'not-a-valid-digest'} + }); + + const distribution = new ZuluDistribution({ + version: '', + architecture: 'x86', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData; + const result = await distribution['findPackageForDownload']('11.0.5'); + + expect(result.checksum).toBeUndefined(); + expect(core.debug).toHaveBeenCalledWith( + expect.stringContaining('No authoritative sha256 checksum') + ); }); it('should throw an error', async () => { diff --git a/__tests__/distributors/zulu-linux-installer.test.ts b/__tests__/distributors/zulu-linux-installer.test.ts index 547bf74a3..2b818c7de 100644 --- a/__tests__/distributors/zulu-linux-installer.test.ts +++ b/__tests__/distributors/zulu-linux-installer.test.ts @@ -245,6 +245,26 @@ describe('getArchitectureOptions', () => { }); describe('findPackageForDownload', () => { + let spyPackageDetails: any; + + const ZULU_CHECKSUM = 'a'.repeat(64); + + beforeEach(() => { + // The resolved winning package fetches sha256_hash from the Azul + // package-details endpoint; stub it so tests never reach the real + // network. + spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson'); + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {sha256_hash: ZULU_CHECKSUM} + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it.each([ ['8', '8.0.282+8'], ['11.x', '11.0.10+9'], @@ -283,6 +303,38 @@ describe('findPackageForDownload', () => { expect(result.url).toBe( 'https://cdn.azul.com/zulu/bin/zulu21.32.17-ca-jdk21.0.2-linux_aarch64.tar.gz' ); + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: ZULU_CHECKSUM, + source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12447' + }); + // Only the winning package's UUID triggers a details request. + expect(spyPackageDetails).toHaveBeenCalledWith( + 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12447' + ); + expect(spyPackageDetails).toHaveBeenCalledTimes(1); + }); + + it('skips checksum verification when sha256_hash is missing or malformed', async () => { + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {} + }); + + const distribution = new ZuluDistribution({ + version: '', + architecture: 'arm64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData; + const result = await distribution['findPackageForDownload']('21.0.2'); + + expect(result.checksum).toBeUndefined(); + expect(core.debug).toHaveBeenCalledWith( + expect.stringContaining('No authoritative sha256 checksum') + ); }); it('should throw an error', async () => { diff --git a/__tests__/distributors/zulu-windows-installer.test.ts b/__tests__/distributors/zulu-windows-installer.test.ts index c6749edb0..c90890d5a 100644 --- a/__tests__/distributors/zulu-windows-installer.test.ts +++ b/__tests__/distributors/zulu-windows-installer.test.ts @@ -242,6 +242,26 @@ describe('getArchitectureOptions', () => { }); describe('findPackageForDownload', () => { + let spyPackageDetails: any; + + const ZULU_CHECKSUM = 'a'.repeat(64); + + beforeEach(() => { + // The resolved winning package fetches sha256_hash from the Azul + // package-details endpoint; stub it so tests never reach the real + // network. + spyPackageDetails = jest.spyOn(HttpClient.prototype, 'getJson'); + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {sha256_hash: ZULU_CHECKSUM} + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it.each([ ['8', '8.0.282+8'], ['11.x', '11.0.10+9'], @@ -280,6 +300,38 @@ describe('findPackageForDownload', () => { expect(result.url).toBe( 'https://cdn.azul.com/zulu/bin/zulu17.48.15-ca-jdk17.0.10-windows_aarch64.zip' ); + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: ZULU_CHECKSUM, + source: 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12446' + }); + // Only the winning package's UUID triggers a details request. + expect(spyPackageDetails).toHaveBeenCalledWith( + 'https://api.azul.com/metadata/v1/zulu/packages/test-uuid-12446' + ); + expect(spyPackageDetails).toHaveBeenCalledTimes(1); + }); + + it('skips checksum verification when sha256_hash is missing or malformed', async () => { + spyPackageDetails.mockResolvedValue({ + statusCode: 200, + headers: {}, + result: {sha256_hash: '123'} + }); + + const distribution = new ZuluDistribution({ + version: '', + architecture: 'arm64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData; + const result = await distribution['findPackageForDownload']('17.0.10'); + + expect(result.checksum).toBeUndefined(); + expect(core.debug).toHaveBeenCalledWith( + expect.stringContaining('No authoritative sha256 checksum') + ); }); it('should throw an error', async () => { diff --git a/dist/setup/index.js b/dist/setup/index.js index 54108ba10..e60e7e8fb 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129532,6 +129532,32 @@ class JavaBase { throw error; } } + async fetchChecksum(checksumUrl, algorithm) { + const response = await this.http.get(checksumUrl); + const statusCode = response.message.statusCode; + const source = (() => { + try { + const url = new URL(checksumUrl); + return `${url.origin}${url.pathname}`; + } + catch { + return 'an invalid checksum URL'; + } + })(); + if (statusCode === HttpCodes.NotFound) { + core_debug(`No authoritative ${algorithm} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.`); + return undefined; + } + if (statusCode !== HttpCodes.OK) { + throw new Error(`Failed to fetch the authoritative ${algorithm} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).`); + } + const body = await response.readBody(); + const value = body.trim().split(/\s+/, 1)[0] ?? ''; + if (!value) { + throw new Error(`Received an empty authoritative ${algorithm} checksum for ${this.distribution} from ${source}.`); + } + return { algorithm, value, source: checksumUrl }; + } async setupJava() { if (this.verifySignature && !this.supportsSignatureVerification()) { throw new Error(`Input 'verify-signature' is not supported for distribution '${this.distribution}'.`); @@ -129886,7 +129912,8 @@ class ZuluDistribution extends JavaBase { return { version: convertVersionToSemver(javaVersion), url: item.download_url, - zuluVersion: convertVersionToSemver(item.distro_version) + zuluVersion: convertVersionToSemver(item.distro_version), + packageUuid: item.package_uuid }; }); const satisfiedVersions = availableVersions @@ -129897,18 +129924,33 @@ class ZuluDistribution extends JavaBase { return (-semver_default().compareBuild(a.version, b.version) || -semver_default().compareBuild(a.zuluVersion, b.zuluVersion)); }) - .map(item => { - return { - version: item.version, - url: item.url - }; - }); + .map((item) => ({ + version: item.version, + url: item.url, + packageUuid: item.packageUuid + })); const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null; if (!resolvedFullVersion) { const availableVersionStrings = availableVersions.map(item => item.version); throw this.createVersionNotFoundError(version, availableVersionStrings); } - return resolvedFullVersion; + const packageDetailsUrl = `https://api.azul.com/metadata/v1/zulu/packages/${resolvedFullVersion.packageUuid}`; + const packageDetails = (await this.http.getJson(packageDetailsUrl)).result; + const digest = packageDetails?.sha256_hash?.match(/^[a-f0-9]{64}$/i)?.[0]; + if (!digest) { + core_debug(`No authoritative sha256 checksum is available for Zulu version ${resolvedFullVersion.version} from ${packageDetailsUrl}; skipping checksum verification.`); + } + return { + version: resolvedFullVersion.version, + url: resolvedFullVersion.url, + checksum: digest + ? { + algorithm: 'sha256', + value: digest, + source: packageDetailsUrl + } + : undefined + }; } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); @@ -130760,7 +130802,8 @@ class MicrosoftDistributions extends JavaBase { return { url: file.download_url, signatureUrl, - version: foundRelease.version + version: foundRelease.version, + checksum: await this.fetchChecksum(`${file.download_url}.sha256sum.txt`, 'sha256') }; } supportsSignatureVerification() { @@ -131163,7 +131206,11 @@ class OracleDistribution extends JavaBase { for (const url of possibleUrls) { const response = await this.http.head(url); if (response.message.statusCode === HttpCodes.OK) { - return { url, version: range }; + return { + url, + version: range, + checksum: await this.fetchChecksum(`${url}.sha256`, 'sha256') + }; } if (response.message.statusCode !== HttpCodes.NotFound) { throw new Error(`Http request for Oracle JDK failed with status code: ${response.message.statusCode}`); @@ -131596,7 +131643,11 @@ class GraalVMDistribution extends JavaBase { const fileUrl = this.constructFileUrl(range, major, platform, arch, extension); const response = await this.http.head(fileUrl); this.handleHttpResponse(response, range); - return { url: fileUrl, version: range }; + return { + url: fileUrl, + version: range, + checksum: await this.fetchChecksum(`${fileUrl}.sha256`, 'sha256') + }; } validateVersionRange(range) { if (!range || typeof range !== 'string') { @@ -131677,7 +131728,8 @@ class GraalVMDistribution extends JavaBase { core_debug(`Download URL: ${downloadUrl}`); return { url: downloadUrl, - version: latestVersion.version + version: latestVersion.version, + checksum: await this.fetchChecksum(`${downloadUrl}.sha256`, 'sha256') }; } async fetchEAJson(javaEaVersion) { @@ -131789,9 +131841,20 @@ class GraalVMCommunityDistribution extends GraalVMDistribution { for (const asset of release.assets ?? []) { const version = this.extractAssetVersion(asset.name, assetSuffix); if (version) { + const digest = asset.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1]; + if (!digest) { + core_debug(`No authoritative sha256 digest is available for ${asset.name}; skipping checksum verification for this asset.`); + } versions.set(version, { version, - url: asset.browser_download_url + url: asset.browser_download_url, + checksum: digest + ? { + algorithm: 'sha256', + value: digest, + source: GRAALVM_COMMUNITY_RELEASES_URL + } + : undefined }); } } @@ -131856,7 +131919,10 @@ class JetBrainsDistribution extends JavaBase { const availableVersionStrings = versionsRaw.map(item => `${item.tag_name} (${item.semver}+${item.build})`); throw this.createVersionNotFoundError(range, availableVersionStrings); } - return resolvedFullVersion; + return { + ...resolvedFullVersion, + checksum: await this.fetchChecksum(`${resolvedFullVersion.url}.checksum`, 'sha512') + }; } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); @@ -132162,7 +132228,11 @@ class OpenJdkDistribution extends JavaBase { if (!matchingReleases.length) { throw this.createVersionNotFoundError(range, releases.map(release => release.version), `Platform: ${platform}`); } - return matchingReleases[0]; + const release = matchingReleases[0]; + return { + ...release, + checksum: await this.fetchChecksum(`${release.url}.sha256`, 'sha256') + }; } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index 874b6c4ea..b30da5bde 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -10,6 +10,8 @@ import { isVersionSatisfies } from '../util.js'; import { + ChecksumAlgorithm, + ChecksumMetadata, JavaDownloadRelease, JavaInstallerOptions, JavaInstallerResults @@ -102,6 +104,44 @@ export abstract class JavaBase { } } + protected async fetchChecksum( + checksumUrl: string, + algorithm: ChecksumAlgorithm + ): Promise { + const response = await this.http.get(checksumUrl); + const statusCode = response.message.statusCode; + const source = (() => { + try { + const url = new URL(checksumUrl); + return `${url.origin}${url.pathname}`; + } catch { + return 'an invalid checksum URL'; + } + })(); + + if (statusCode === httpm.HttpCodes.NotFound) { + core.debug( + `No authoritative ${algorithm} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.` + ); + return undefined; + } + + if (statusCode !== httpm.HttpCodes.OK) { + throw new Error( + `Failed to fetch the authoritative ${algorithm} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).` + ); + } + + const body = await response.readBody(); + const value = body.trim().split(/\s+/, 1)[0] ?? ''; + if (!value) { + throw new Error( + `Received an empty authoritative ${algorithm} checksum for ${this.distribution} from ${source}.` + ); + } + return {algorithm, value, source: checksumUrl}; + } + public async setupJava(): Promise { if (this.verifySignature && !this.supportsSignatureVerification()) { throw new Error( diff --git a/src/distributions/graalvm/installer.ts b/src/distributions/graalvm/installer.ts index 055caba41..319a45591 100644 --- a/src/distributions/graalvm/installer.ts +++ b/src/distributions/graalvm/installer.ts @@ -43,6 +43,7 @@ type OsVersions = 'linux' | 'macos' | 'windows'; interface GraalVMCommunityAsset { name: string; browser_download_url: string; + digest?: string; } interface GraalVMCommunityRelease { @@ -145,7 +146,11 @@ export class GraalVMDistribution extends JavaBase { const response = await this.http.head(fileUrl); this.handleHttpResponse(response, range); - return {url: fileUrl, version: range}; + return { + url: fileUrl, + version: range, + checksum: await this.fetchChecksum(`${fileUrl}.sha256`, 'sha256') + }; } protected validateVersionRange(range: string): void { @@ -284,7 +289,8 @@ export class GraalVMDistribution extends JavaBase { return { url: downloadUrl, - version: latestVersion.version + version: latestVersion.version, + checksum: await this.fetchChecksum(`${downloadUrl}.sha256`, 'sha256') }; } @@ -456,9 +462,22 @@ export class GraalVMCommunityDistribution extends GraalVMDistribution { for (const asset of release.assets ?? []) { const version = this.extractAssetVersion(asset.name, assetSuffix); if (version) { + const digest = asset.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1]; + if (!digest) { + core.debug( + `No authoritative sha256 digest is available for ${asset.name}; skipping checksum verification for this asset.` + ); + } versions.set(version, { version, - url: asset.browser_download_url + url: asset.browser_download_url, + checksum: digest + ? { + algorithm: 'sha256', + value: digest, + source: GRAALVM_COMMUNITY_RELEASES_URL + } + : undefined }); } } diff --git a/src/distributions/jetbrains/installer.ts b/src/distributions/jetbrains/installer.ts index d4f7647cd..d6e039127 100644 --- a/src/distributions/jetbrains/installer.ts +++ b/src/distributions/jetbrains/installer.ts @@ -50,7 +50,13 @@ export class JetBrainsDistribution extends JavaBase { throw this.createVersionNotFoundError(range, availableVersionStrings); } - return resolvedFullVersion; + return { + ...resolvedFullVersion, + checksum: await this.fetchChecksum( + `${resolvedFullVersion.url}.checksum`, + 'sha512' + ) + }; } protected async downloadTool( diff --git a/src/distributions/microsoft/installer.ts b/src/distributions/microsoft/installer.ts index f3f3e9f6a..ddafa0f68 100644 --- a/src/distributions/microsoft/installer.ts +++ b/src/distributions/microsoft/installer.ts @@ -114,7 +114,11 @@ export class MicrosoftDistributions extends JavaBase { return { url: file.download_url, signatureUrl, - version: foundRelease.version + version: foundRelease.version, + checksum: await this.fetchChecksum( + `${file.download_url}.sha256sum.txt`, + 'sha256' + ) }; } diff --git a/src/distributions/openjdk/installer.ts b/src/distributions/openjdk/installer.ts index 24a102a5c..6368429a9 100644 --- a/src/distributions/openjdk/installer.ts +++ b/src/distributions/openjdk/installer.ts @@ -50,7 +50,11 @@ export class OpenJdkDistribution extends JavaBase { ); } - return matchingReleases[0]; + const release = matchingReleases[0]; + return { + ...release, + checksum: await this.fetchChecksum(`${release.url}.sha256`, 'sha256') + }; } protected async downloadTool( diff --git a/src/distributions/oracle/installer.ts b/src/distributions/oracle/installer.ts index 51ab8690b..27e21a3de 100644 --- a/src/distributions/oracle/installer.ts +++ b/src/distributions/oracle/installer.ts @@ -112,7 +112,11 @@ export class OracleDistribution extends JavaBase { const response = await this.http.head(url); if (response.message.statusCode === HttpCodes.OK) { - return {url, version: range}; + return { + url, + version: range, + checksum: await this.fetchChecksum(`${url}.sha256`, 'sha256') + }; } if (response.message.statusCode !== HttpCodes.NotFound) { diff --git a/src/distributions/zulu/installer.ts b/src/distributions/zulu/installer.ts index 04b1e15ab..aa0e0e79b 100644 --- a/src/distributions/zulu/installer.ts +++ b/src/distributions/zulu/installer.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import semver from 'semver'; import {JavaBase} from '../base-installer.js'; -import {IZuluVersions} from './models.js'; +import {IZuluPackageDetails, IZuluVersions} from './models.js'; import { extractJdkFile, getDownloadArchiveExtension, @@ -20,6 +20,15 @@ import { JavaInstallerResults } from '../base-models.js'; +// The Azul Metadata API only reports the sha256 checksum on the +// package-details endpoint, keyed by package_uuid, so the resolved candidate +// must retain its UUID after sorting until the single follow-up request is made. +interface ZuluResolvedRelease { + version: string; + url: string; + packageUuid: string; +} + export class ZuluDistribution extends JavaBase { constructor(installerOptions: JavaInstallerOptions) { super('Zulu', installerOptions); @@ -40,7 +49,8 @@ export class ZuluDistribution extends JavaBase { return { version: convertVersionToSemver(javaVersion), url: item.download_url, - zuluVersion: convertVersionToSemver(item.distro_version) + zuluVersion: convertVersionToSemver(item.distro_version), + packageUuid: item.package_uuid }; }); @@ -54,12 +64,11 @@ export class ZuluDistribution extends JavaBase { -semver.compareBuild(a.zuluVersion, b.zuluVersion) ); }) - .map(item => { - return { - version: item.version, - url: item.url - } as JavaDownloadRelease; - }); + .map((item): ZuluResolvedRelease => ({ + version: item.version, + url: item.url, + packageUuid: item.packageUuid + })); const resolvedFullVersion = satisfiedVersions.length > 0 ? satisfiedVersions[0] : null; @@ -70,7 +79,29 @@ export class ZuluDistribution extends JavaBase { throw this.createVersionNotFoundError(version, availableVersionStrings); } - return resolvedFullVersion; + const packageDetailsUrl = `https://api.azul.com/metadata/v1/zulu/packages/${resolvedFullVersion.packageUuid}`; + const packageDetails = ( + await this.http.getJson(packageDetailsUrl) + ).result; + const digest = packageDetails?.sha256_hash?.match(/^[a-f0-9]{64}$/i)?.[0]; + + if (!digest) { + core.debug( + `No authoritative sha256 checksum is available for Zulu version ${resolvedFullVersion.version} from ${packageDetailsUrl}; skipping checksum verification.` + ); + } + + return { + version: resolvedFullVersion.version, + url: resolvedFullVersion.url, + checksum: digest + ? { + algorithm: 'sha256', + value: digest, + source: packageDetailsUrl + } + : undefined + }; } protected async downloadTool( diff --git a/src/distributions/zulu/models.ts b/src/distributions/zulu/models.ts index 36d369e05..c03d12db9 100644 --- a/src/distributions/zulu/models.ts +++ b/src/distributions/zulu/models.ts @@ -10,3 +10,7 @@ export interface IZuluVersions { latest: boolean; availability_type: string; } + +export interface IZuluPackageDetails { + sha256_hash?: string; +} From dfbbf9a6f04614cef1c9dbc6fc693cbf0926b4b2 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 04:22:25 -0400 Subject: [PATCH 7/8] Accept SHA-256 or SHA-512 for JetBrains checksum sibling JetBrains publishes a single, generically-named ".checksum" sibling whose digest algorithm isn't disclosed by the filename. Older JBR 11 builds (e.g. jbrsdk_nomod-11_0_16-*-b2043.64.tar.gz) publish a SHA-256 digest there, while newer builds publish SHA-512. The JetBrains installer previously assumed SHA-512 unconditionally, so verification failed with "Malformed sha512 checksum metadata ... expected a 128-character hexadecimal digest" for those older builds, breaking the jetbrains 11 e2e job on macOS and Windows. fetchChecksum now accepts a list of candidate algorithms and infers the actual algorithm from the returned digest's length, preferring the strongest match. The JetBrains installer passes ['sha512', 'sha256']; all other callers are unaffected since they already pass a single, vendor-disclosed algorithm. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- __tests__/distributors/base-installer.test.ts | 72 ++++++++++++++++++- .../distributors/jetbrains-installer.test.ts | 27 +++++++ dist/setup/index.js | 35 +++++++-- src/checksum.ts | 12 +++- src/distributions/base-installer.ts | 29 ++++++-- src/distributions/jetbrains/installer.ts | 6 +- 6 files changed, 165 insertions(+), 16 deletions(-) diff --git a/__tests__/distributors/base-installer.test.ts b/__tests__/distributors/base-installer.test.ts index c05892a4f..f8aa0d241 100644 --- a/__tests__/distributors/base-installer.test.ts +++ b/__tests__/distributors/base-installer.test.ts @@ -127,7 +127,7 @@ class EmptyJavaBase extends JavaBase { public fetchChecksumForTest( checksumUrl: string, - algorithm: 'sha256' | 'sha512' + algorithm: 'sha256' | 'sha512' | ('sha256' | 'sha512')[] ) { return this.fetchChecksum(checksumUrl, algorithm); } @@ -1059,6 +1059,76 @@ describe('fetchChecksum', () => { 'Received an empty authoritative sha256 checksum for Empty from https://vendor.example/jdk.tar.gz.sha256.' ); }); + + describe('with a list of candidate algorithms', () => { + it('infers sha512 when the digest is 128 hex characters', async () => { + const digest = 'd'.repeat(128); + mockGet(200, `${digest} jbrsdk.tar.gz\n`); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jbrsdk.tar.gz.checksum', + ['sha512', 'sha256'] + ); + + expect(checksum).toEqual({ + algorithm: 'sha512', + value: digest, + source: 'https://vendor.example/jbrsdk.tar.gz.checksum' + }); + }); + + it('infers sha256 when the digest is 64 hex characters, even though sha512 was preferred', async () => { + // Reproduces older JetBrains JBR builds (e.g. JBR 11), which publish a + // SHA-256 digest at the generic `.checksum` sibling instead of SHA-512. + const digest = 'e'.repeat(64); + mockGet(200, `${digest} jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz\n`); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz.checksum', + ['sha512', 'sha256'] + ); + + expect(checksum).toEqual({ + algorithm: 'sha256', + value: digest, + source: + 'https://vendor.example/jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz.checksum' + }); + }); + + it('falls back to the first candidate algorithm when the digest length matches none of them', async () => { + const digest = 'f'.repeat(40); // e.g. sha1, not supported + mockGet(200, `${digest} jbrsdk.tar.gz\n`); + const distribution = new EmptyJavaBase(options); + + const checksum = await distribution.fetchChecksumForTest( + 'https://vendor.example/jbrsdk.tar.gz.checksum', + ['sha512', 'sha256'] + ); + + // No candidate algorithm matches, so the first-listed one is kept; + // downstream verification will reject it as malformed. + expect(checksum.algorithm).toBe('sha512'); + expect(checksum.value).toBe(digest); + }); + + it('reports the checksum as unavailable using a combined algorithm label on 404', async () => { + mockGet(404, 'Not Found'); + const distribution = new EmptyJavaBase(options); + + await expect( + distribution.fetchChecksumForTest( + 'https://vendor.example/jbrsdk.tar.gz.checksum', + ['sha512', 'sha256'] + ) + ).resolves.toBeUndefined(); + expect(core.debug).toHaveBeenCalledWith( + 'No authoritative sha512 or sha256 checksum is available for Empty from https://vendor.example/jbrsdk.tar.gz.checksum; skipping checksum verification.' + ); + }); + }); }); describe('normalizeVersion', () => { diff --git a/__tests__/distributors/jetbrains-installer.test.ts b/__tests__/distributors/jetbrains-installer.test.ts index e093dba13..2a5262196 100644 --- a/__tests__/distributors/jetbrains-installer.test.ts +++ b/__tests__/distributors/jetbrains-installer.test.ts @@ -293,4 +293,31 @@ describe('findPackageForDownload', () => { expect(result.checksum?.value).toBe(JETBRAINS_CHECKSUM); }); + + it('falls back to a sha256 checksum for older JBR builds that only publish one', async () => { + // Older JBR 11 builds (e.g. jbrsdk_nomod-11_0_16-*-b2043.64.tar.gz) publish + // a SHA-256 digest at the generic `.checksum` sibling instead of SHA-512. + const sha256Checksum = 'a'.repeat(64); + spyHttpClientGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => + `${sha256Checksum} jbrsdk_nomod-11_0_16-osx-x64-b2043.64.tar.gz\n` + } as any); + + const distribution = new JetBrainsDistribution({ + version: '21', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + distribution['getAvailableVersions'] = async () => manifestData as any; + + const result = await distribution['findPackageForDownload']('21'); + + expect(result.checksum).toEqual({ + algorithm: 'sha256', + value: sha256Checksum, + source: `${result.url}.checksum` + }); + }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index e60e7e8fb..ec7ce6832 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129429,12 +129429,18 @@ function sanitizedSource(source) { return ' from an invalid checksum source'; } } +// Length, in hex characters, of a digest produced by each supported algorithm. +// Exported so callers (e.g. fetchChecksum) can infer which algorithm a vendor +// actually used when it doesn't disclose it via the checksum URL/filename. +function expectedDigestLength(algorithm) { + return algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; +} function normalizeExpectedDigest(checksum) { const algorithm = checksum.algorithm; const digest = typeof checksum.value === 'string' ? checksum.value.trim().toLowerCase() : ''; - const expectedLength = algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; + const expectedLength = expectedDigestLength(algorithm); if (expectedLength === 0) { throw new Error(`Unsupported checksum algorithm '${String(algorithm)}'${sanitizedSource(checksum.source)}. Supported algorithms are sha256 and sha512.`); } @@ -129533,6 +129539,14 @@ class JavaBase { } } async fetchChecksum(checksumUrl, algorithm) { + // Some vendors (e.g. JetBrains) publish a single, generically-named + // checksum sibling (`.checksum`) whose digest algorithm isn't disclosed + // by the URL and has changed across releases. Accepting a list of + // candidate algorithms lets callers pass every algorithm the vendor is + // known to use; the actual algorithm is then inferred from the length of + // the returned digest. + const algorithms = Array.isArray(algorithm) ? algorithm : [algorithm]; + const algorithmLabel = algorithms.join(' or '); const response = await this.http.get(checksumUrl); const statusCode = response.message.statusCode; const source = (() => { @@ -129545,18 +129559,23 @@ class JavaBase { } })(); if (statusCode === HttpCodes.NotFound) { - core_debug(`No authoritative ${algorithm} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.`); + core_debug(`No authoritative ${algorithmLabel} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.`); return undefined; } if (statusCode !== HttpCodes.OK) { - throw new Error(`Failed to fetch the authoritative ${algorithm} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).`); + throw new Error(`Failed to fetch the authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).`); } const body = await response.readBody(); const value = body.trim().split(/\s+/, 1)[0] ?? ''; if (!value) { - throw new Error(`Received an empty authoritative ${algorithm} checksum for ${this.distribution} from ${source}.`); + throw new Error(`Received an empty authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source}.`); } - return { algorithm, value, source: checksumUrl }; + // Prefer the strongest algorithm whose digest length matches what was + // actually returned; fall back to the first candidate (preserving prior + // behavior/error messages) when the digest doesn't match any of them. + const resolvedAlgorithm = algorithms.find(algo => value.length === expectedDigestLength(algo)) ?? + algorithms[0]; + return { algorithm: resolvedAlgorithm, value, source: checksumUrl }; } async setupJava() { if (this.verifySignature && !this.supportsSignatureVerification()) { @@ -131921,7 +131940,11 @@ class JetBrainsDistribution extends JavaBase { } return { ...resolvedFullVersion, - checksum: await this.fetchChecksum(`${resolvedFullVersion.url}.checksum`, 'sha512') + // JetBrains' `.checksum` sibling doesn't disclose its algorithm via the + // filename, and older JBR builds (e.g. JBR 11) publish a SHA-256 digest + // there while newer builds publish SHA-512. Accept either, preferring + // the stronger SHA-512 when the digest length is ambiguous. + checksum: await this.fetchChecksum(`${resolvedFullVersion.url}.checksum`, ['sha512', 'sha256']) }; } async downloadTool(javaRelease) { diff --git a/src/checksum.ts b/src/checksum.ts index d80efd4d8..cd0b8c843 100644 --- a/src/checksum.ts +++ b/src/checksum.ts @@ -22,14 +22,22 @@ function sanitizedSource(source: string | undefined): string { } } +// Length, in hex characters, of a digest produced by each supported algorithm. +// Exported so callers (e.g. fetchChecksum) can infer which algorithm a vendor +// actually used when it doesn't disclose it via the checksum URL/filename. +export function expectedDigestLength( + algorithm: ChecksumMetadata['algorithm'] +): number { + return algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; +} + function normalizeExpectedDigest(checksum: ChecksumMetadata): string { const algorithm = checksum.algorithm; const digest = typeof checksum.value === 'string' ? checksum.value.trim().toLowerCase() : ''; - const expectedLength = - algorithm === 'sha256' ? 64 : algorithm === 'sha512' ? 128 : 0; + const expectedLength = expectedDigestLength(algorithm); if (expectedLength === 0) { throw new Error( diff --git a/src/distributions/base-installer.ts b/src/distributions/base-installer.ts index b30da5bde..2c8753fb6 100644 --- a/src/distributions/base-installer.ts +++ b/src/distributions/base-installer.ts @@ -19,7 +19,7 @@ import { import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants.js'; import {RetryingHttpClient} from '../retrying-http-client.js'; import os from 'os'; -import {verifyChecksum} from '../checksum.js'; +import {expectedDigestLength, verifyChecksum} from '../checksum.js'; export abstract class JavaBase { protected http: httpm.HttpClient; @@ -106,8 +106,17 @@ export abstract class JavaBase { protected async fetchChecksum( checksumUrl: string, - algorithm: ChecksumAlgorithm + algorithm: ChecksumAlgorithm | ChecksumAlgorithm[] ): Promise { + // Some vendors (e.g. JetBrains) publish a single, generically-named + // checksum sibling (`.checksum`) whose digest algorithm isn't disclosed + // by the URL and has changed across releases. Accepting a list of + // candidate algorithms lets callers pass every algorithm the vendor is + // known to use; the actual algorithm is then inferred from the length of + // the returned digest. + const algorithms = Array.isArray(algorithm) ? algorithm : [algorithm]; + const algorithmLabel = algorithms.join(' or '); + const response = await this.http.get(checksumUrl); const statusCode = response.message.statusCode; const source = (() => { @@ -121,14 +130,14 @@ export abstract class JavaBase { if (statusCode === httpm.HttpCodes.NotFound) { core.debug( - `No authoritative ${algorithm} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.` + `No authoritative ${algorithmLabel} checksum is available for ${this.distribution} from ${source}; skipping checksum verification.` ); return undefined; } if (statusCode !== httpm.HttpCodes.OK) { throw new Error( - `Failed to fetch the authoritative ${algorithm} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).` + `Failed to fetch the authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source} (HTTP ${statusCode}).` ); } @@ -136,10 +145,18 @@ export abstract class JavaBase { const value = body.trim().split(/\s+/, 1)[0] ?? ''; if (!value) { throw new Error( - `Received an empty authoritative ${algorithm} checksum for ${this.distribution} from ${source}.` + `Received an empty authoritative ${algorithmLabel} checksum for ${this.distribution} from ${source}.` ); } - return {algorithm, value, source: checksumUrl}; + + // Prefer the strongest algorithm whose digest length matches what was + // actually returned; fall back to the first candidate (preserving prior + // behavior/error messages) when the digest doesn't match any of them. + const resolvedAlgorithm = + algorithms.find(algo => value.length === expectedDigestLength(algo)) ?? + algorithms[0]; + + return {algorithm: resolvedAlgorithm, value, source: checksumUrl}; } public async setupJava(): Promise { diff --git a/src/distributions/jetbrains/installer.ts b/src/distributions/jetbrains/installer.ts index d6e039127..fdd099173 100644 --- a/src/distributions/jetbrains/installer.ts +++ b/src/distributions/jetbrains/installer.ts @@ -52,9 +52,13 @@ export class JetBrainsDistribution extends JavaBase { return { ...resolvedFullVersion, + // JetBrains' `.checksum` sibling doesn't disclose its algorithm via the + // filename, and older JBR builds (e.g. JBR 11) publish a SHA-256 digest + // there while newer builds publish SHA-512. Accept either, preferring + // the stronger SHA-512 when the digest length is ambiguous. checksum: await this.fetchChecksum( `${resolvedFullVersion.url}.checksum`, - 'sha512' + ['sha512', 'sha256'] ) }; } From 855a234492fbe569ed36d8383e7d738f267c2bda Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Wed, 29 Jul 2026 04:31:06 -0400 Subject: [PATCH 8/8] Use SapMachine archive checksum files Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d --- .../distributors/sapmachine-installer.test.ts | 27 ++++++++++++++++++- dist/setup/index.js | 12 ++++----- src/distributions/sapmachine/installer.ts | 15 ++++++----- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/__tests__/distributors/sapmachine-installer.test.ts b/__tests__/distributors/sapmachine-installer.test.ts index f778e4a47..e41419bf0 100644 --- a/__tests__/distributors/sapmachine-installer.test.ts +++ b/__tests__/distributors/sapmachine-installer.test.ts @@ -52,8 +52,10 @@ const utils = await import('../../src/util.js'); describe('getAvailableVersions', () => { let spyHttpClient: any; + let spyHttpGet: any; let spyUtilGetDownloadArchiveExtension: any; let spyCoreError: any; + const archiveChecksum = 'f'.repeat(64); beforeEach(() => { spyHttpClient = jest.spyOn(HttpClient.prototype, 'getJson'); @@ -62,6 +64,11 @@ describe('getAvailableVersions', () => { headers: {}, result: manifestData }); + spyHttpGet = jest.spyOn(HttpClient.prototype, 'get'); + spyHttpGet.mockResolvedValue({ + message: {statusCode: 200}, + readBody: async () => `${archiveChecksum} archive` + }); spyUtilGetDownloadArchiveExtension = utils.getDownloadArchiveExtension as jest.Mock; @@ -284,11 +291,29 @@ describe('getAvailableVersions', () => { expect(availableVersion.url).toBe(expectedLink); expect(availableVersion.checksum).toEqual({ algorithm: 'sha256', - value: expect.stringMatching(/^[a-f0-9]{64}$/) + value: archiveChecksum, + source: expectedLink.replace(/\.(?:tar\.gz|zip)$/, '.sha256.txt') }); } ); + it('uses the checksum published beside the selected EA archive', async () => { + const distribution = new SapMachineDistribution({ + version: '21-ea', + architecture: 'x64', + packageType: 'jdk', + checkLatest: false + }); + mockPlatform(distribution, 'linux'); + + const release = await distribution['findPackageForDownload']('21'); + + expect(spyHttpGet).toHaveBeenCalledWith( + release.url.replace(/\.(?:tar\.gz|zip)$/, '.sha256.txt') + ); + expect(release.checksum?.value).toBe(archiveChecksum); + }); + it.each([ ['8', 'linux', 'x64'], ['8', 'macos', 'aarch64'], diff --git a/dist/setup/index.js b/dist/setup/index.js index ec7ce6832..09aad7f94 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -131446,11 +131446,7 @@ class SapMachineDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadLink, - checksum: { - algorithm: 'sha256', - value: item.checksum - } + url: item.downloadLink }; }); if (!matchedVersions.length) { @@ -131458,7 +131454,11 @@ class SapMachineDistribution extends JavaBase { throw this.createVersionNotFoundError(version, availableVersionStrings); } const resolvedVersion = matchedVersions[0]; - return resolvedVersion; + const checksumUrl = resolvedVersion.url.replace(/\.(?:tar\.gz|zip)$/, '.sha256.txt'); + return { + ...resolvedVersion, + checksum: await this.fetchChecksum(checksumUrl, 'sha256') + }; } async getAvailableVersions() { const platform = this.getPlatformOption(); diff --git a/src/distributions/sapmachine/installer.ts b/src/distributions/sapmachine/installer.ts index ad82af86f..3d7030fbd 100644 --- a/src/distributions/sapmachine/installer.ts +++ b/src/distributions/sapmachine/installer.ts @@ -44,11 +44,7 @@ export class SapMachineDistribution extends JavaBase { .map(item => { return { version: item.version, - url: item.downloadLink, - checksum: { - algorithm: 'sha256', - value: item.checksum - } + url: item.downloadLink } as JavaDownloadRelease; }); @@ -60,7 +56,14 @@ export class SapMachineDistribution extends JavaBase { } const resolvedVersion = matchedVersions[0]; - return resolvedVersion; + const checksumUrl = resolvedVersion.url.replace( + /\.(?:tar\.gz|zip)$/, + '.sha256.txt' + ); + return { + ...resolvedVersion, + checksum: await this.fetchChecksum(checksumUrl, 'sha256') + }; } private async getAvailableVersions(): Promise {