feat: Add verify-signature plumbing and Temurin+Microsoft verification support (#1060)

* Add verify-signature plumbing and Temurin verification support

* Rebuild dist after signature verification changes

* Refine signature verification errors and regenerate dist

* refactor: make gpg.ts generic, move Adoptium-specific constant to temurin distribution

* fix: mock renameWinArchive in temurin tests and add signature e2e job

* refactor: bundle Adoptium public key, replace keyserver lookup with local import

* feat: add verify-signature-public-key input to allow custom GPG key override

* refactor: extract Adoptium public key to adoptium-key.ts; tighten gpg.ts cleanup scope

* Add verify-signature plumbing and Temurin verification support

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Add Microsoft signature verification support

* Regenerate dist bundles for Microsoft signature checks

* Harden Microsoft signature URL handling

* Add setup-java-microsoft-signature-verification e2e job

* chore: regenerate dist files

* Fix e2e-versions: remove duplicate job, update signature jobs to checkout@v7 with env vars

* Fix Prettier formatting in test files

* fix: mock renameWinArchive in microsoft-installer tests to fix Windows CI failure

* fix: use --homedir flag instead of GNUPGHOME env var for Windows GPG compatibility

The Git-bundled GPG on Windows (MSYS2-based) does not automatically convert
Windows-style paths in environment variables like GNUPGHOME. This caused GPG
to fail with exit code 2 when verifying Microsoft JDK signatures on Windows,
because the GNUPGHOME path (D:\a\_temp\...) was not recognized as a valid
POSIX path.

Fix: pass --homedir as an explicit command-line argument to both gpg --import
and gpg --verify. MSYS2 does correctly convert Windows paths in command-line
arguments, so this approach works reliably on Windows, Linux, and macOS.

* fix: convert Windows paths to POSIX format for MSYS2 GPG on Windows

The Git-bundled GPG on Windows (C:\Program Files\Git\usr\bin\gpg.exe) is
an MSYS2-based binary that uses POSIX path conventions internally. When
Windows-style paths with backslashes and drive letters (D:\a\_temp\...)
are passed as arguments, GPG may fail to resolve them correctly, resulting
in a fatal error (exit code 2).

Fix: add a toGpgPath() helper that converts Windows paths to MSYS2 POSIX
format (/d/a/_temp/...) before passing them to any gpg command. On Linux
and macOS the helper is a no-op.

Applied to all four paths used in verifyPackageSignature:
- gpgHome (--homedir argument)
- publicKeyFile (--import argument)
- signaturePath (--verify signature argument)
- archivePath (--verify data argument)

* Fix gpg test formatting

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Bruno Borges <brborges@microsoft.com>
This commit is contained in:
John
2026-06-29 13:19:49 +01:00
committed by GitHub
parent e9339ddc84
commit b150355f04
20 changed files with 1117 additions and 112 deletions
@@ -1,8 +1,15 @@
import {MicrosoftDistributions} from '../../src/distributions/microsoft/installer';
import {
MicrosoftDistributions,
MICROSOFT_PUBLIC_KEY
} from '../../src/distributions/microsoft/installer';
import os from 'os';
import data from '../data/microsoft.json';
import * as httpm from '@actions/http-client';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as gpg from '../../src/gpg';
import * as util from '../../src/util';
import fs from 'fs';
describe('findPackageForDownload', () => {
let distribution: MicrosoftDistributions;
@@ -102,6 +109,7 @@ describe('findPackageForDownload', () => {
.replace('{{OS_TYPE}}', os)
.replace('{{ARCHIVE_TYPE}}', archive);
expect(result.url).toBe(url);
expect(result.signatureUrl).toBe(`${url}.sig`);
});
it.each([
@@ -187,4 +195,153 @@ describe('findPackageForDownload', () => {
/No matching version found for SemVer */
);
});
it('uses manifest-provided signature URL when available', async () => {
spyGetManifestFromRepo.mockReturnValue({
result: [
{
version: '17.0.10',
stable: true,
release_url: 'https://example.test',
files: [
{
filename: 'microsoft-jdk-17.0.10-linux-x64.tar.gz',
arch: 'x64',
platform: 'linux',
download_url: 'https://example.test/jdk.tar.gz',
signature_url: 'https://example.test/jdk.tar.gz.custom.sig'
}
]
}
],
statusCode: 200,
headers: {}
});
jest.spyOn(os, 'platform').mockReturnValue('linux');
const result = await distribution['findPackageForDownload']('17.0.10');
expect(result.signatureUrl).toBe(
'https://example.test/jdk.tar.gz.custom.sig'
);
});
});
describe('downloadTool', () => {
let spyDownloadTool: jest.SpyInstance;
let spyExtractJdkFile: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyVerifySignature: jest.SpyInstance;
let distribution: MicrosoftDistributions;
beforeEach(() => {
jest
.spyOn(os, 'platform')
.mockReturnValue(process.platform as ReturnType<typeof os.platform>);
distribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false
});
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockImplementation(async () => {
return '/tmp/jdk.tar.gz';
});
spyExtractJdkFile = jest.spyOn(util, 'extractJdkFile');
spyExtractJdkFile.mockImplementation(async () => {
return '/tmp/unpacked';
});
jest.spyOn(fs, 'readdirSync').mockReturnValue(['jdk'] as any);
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockImplementation(async () => {
return '/tmp/cached';
});
jest
.spyOn(util, 'renameWinArchive')
.mockImplementation((archivePath: string) => `${archivePath}.zip`);
spyVerifySignature = jest.spyOn(gpg, 'verifyPackageSignature');
spyVerifySignature.mockImplementation(async () => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('verifies signature when enabled', async () => {
const signedDistribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
});
await signedDistribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
});
expect(spyVerifySignature).toHaveBeenCalledWith(
'/tmp/jdk.tar.gz',
'https://example.com/jdk.tar.gz.sig',
MICROSOFT_PUBLIC_KEY
);
});
it('uses custom public key when verifySignaturePublicKey is provided', async () => {
const customKey =
'-----BEGIN PGP PUBLIC KEY BLOCK-----\ncustom\n-----END PGP PUBLIC KEY BLOCK-----';
const signedDistribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true,
verifySignaturePublicKey: customKey
});
await signedDistribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz',
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
});
expect(spyVerifySignature).toHaveBeenCalledWith(
'/tmp/jdk.tar.gz',
'https://example.com/jdk.tar.gz.sig',
customKey
);
});
it('fails when signature is missing and verification is enabled', async () => {
const signedDistribution = new MicrosoftDistributions({
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
});
await expect(
signedDistribution['downloadTool']({
version: '17.0.14+7',
url: 'https://example.com/jdk.tar.gz'
})
).rejects.toThrow(
"Input 'verify-signature' is enabled, but no signature URL was found for Microsoft Build of OpenJDK version 17.0.14+7."
);
expect(spyVerifySignature).not.toHaveBeenCalled();
});
it('supports signature verification', () => {
expect(distribution['supportsSignatureVerification']()).toBe(true);
});
});