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,10 +1,15 @@
import {HttpClient} from '@actions/http-client';
import * as tc from '@actions/tool-cache';
import fs from 'fs';
import os from 'os';
import {
TemurinDistribution,
TemurinImplementation
TemurinImplementation,
ADOPTIUM_PUBLIC_KEY
} from '../../src/distributions/temurin/installer';
import {JavaInstallerOptions} from '../../src/distributions/base-models';
import * as util from '../../src/util';
import * as gpg from '../../src/gpg';
import manifestData from '../data/temurin.json';
import * as core from '@actions/core';
@@ -231,6 +236,7 @@ describe('findPackageForDownload', () => {
distribution['getAvailableVersions'] = async () => manifestData as any;
const resolvedVersion = await distribution['findPackageForDownload'](input);
expect(resolvedVersion.version).toBe(expected);
expect(resolvedVersion.signatureUrl).toBeDefined();
});
it('version is found but binaries list is empty', async () => {
@@ -281,3 +287,109 @@ describe('findPackageForDownload', () => {
);
});
});
describe('downloadTool', () => {
let spyDownloadTool: jest.SpyInstance;
let spyVerifySignature: jest.SpyInstance;
let spyExtractJdkFile: jest.SpyInstance;
let spyCacheDir: jest.SpyInstance;
let spyReadDirSync: jest.SpyInstance;
let spyRenameWinArchive: jest.SpyInstance;
beforeEach(() => {
spyDownloadTool = jest.spyOn(tc, 'downloadTool');
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
spyVerifySignature = jest.spyOn(gpg, 'verifyPackageSignature');
spyVerifySignature.mockResolvedValue(undefined);
spyExtractJdkFile = jest.spyOn(util, 'extractJdkFile');
spyExtractJdkFile.mockResolvedValue('/tmp/extracted');
spyCacheDir = jest.spyOn(tc, 'cacheDir');
spyCacheDir.mockResolvedValue('/tmp/toolcache');
spyReadDirSync = jest.spyOn(fs, 'readdirSync');
spyReadDirSync.mockReturnValue(['jdk-17'] as any);
spyRenameWinArchive = jest.spyOn(util, 'renameWinArchive');
spyRenameWinArchive.mockReturnValue('/tmp/jdk.tar.gz.zip');
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('verifies signature when enabled', async () => {
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
},
TemurinImplementation.Hotspot
);
await distribution['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',
ADOPTIUM_PUBLIC_KEY
);
});
it('fails when signature is missing and verification is enabled', async () => {
const distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
},
TemurinImplementation.Hotspot
);
await expect(
distribution['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"
);
expect(spyVerifySignature).not.toHaveBeenCalled();
});
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 distribution = new TemurinDistribution(
{
version: '17',
architecture: 'x64',
packageType: 'jdk',
checkLatest: false,
verifySignature: true,
verifySignaturePublicKey: customKey
},
TemurinImplementation.Hotspot
);
await distribution['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
);
});
});