Verify JDK downloads with vendor checksums (#1167)

* Verify JDK downloads with vendor checksums

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* Handle missing vendor checksum values

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* Preserve checksum error during cleanup failure

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* Validate checksum metadata value types

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* Clarify checksum documentation

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* Expand vendor checksum verification

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

* 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

* Use SapMachine archive checksum files

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a800a031-600e-4d28-b23e-be309555d38d
This commit is contained in:
Bruno Borges
2026-07-29 04:43:56 -04:00
committed by GitHub
parent 19c23b379e
commit 27f2c62824
39 changed files with 1629 additions and 103 deletions
+98
View File
@@ -10,6 +10,8 @@ import {
isVersionSatisfies
} from '../util.js';
import {
ChecksumAlgorithm,
ChecksumMetadata,
JavaDownloadRelease,
JavaInstallerOptions,
JavaInstallerResults
@@ -17,6 +19,7 @@ import {
import {MACOS_JAVA_CONTENT_POSTFIX} from '../constants.js';
import {RetryingHttpClient} from '../retrying-http-client.js';
import os from 'os';
import {expectedDigestLength, verifyChecksum} from '../checksum.js';
export abstract class JavaBase {
protected http: httpm.HttpClient;
@@ -61,6 +64,101 @@ export abstract class JavaBase {
range: string
): Promise<JavaDownloadRelease>;
protected async downloadAndVerify(
javaRelease: JavaDownloadRelease
): Promise<string> {
const archivePath = await tc.downloadTool(javaRelease.url);
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, checksum, {
distribution: this.distribution,
version: javaRelease.version
});
core.debug(
`Verified ${checksum.algorithm} checksum for ${this.distribution} version ${javaRelease.version}.`
);
return archivePath;
} catch (error) {
let cleanupError: unknown;
let cleanupFailed = false;
try {
await fs.promises.rm(archivePath, {force: true});
} 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: error}
);
}
throw error;
}
}
protected async fetchChecksum(
checksumUrl: string,
algorithm: ChecksumAlgorithm | ChecksumAlgorithm[]
): Promise<ChecksumMetadata | undefined> {
// 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 = (() => {
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 ${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 ${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 ${algorithmLabel} checksum for ${this.distribution} from ${source}.`
);
}
// 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<JavaInstallerResults> {
if (this.verifySignature && !this.supportsSignatureVerification()) {
throw new Error(