mirror of
https://github.com/actions/setup-java.git
synced 2026-09-04 01:49:32 +00:00
Fix alpine failures by switching default back to only warn on verification failures. To prevent build failures due to missing GPG or rotated vendor keys. (#1262)
Also allow multiple GPG keys to be provided. Co-authored-by: John <1615532+johnoliver@users.noreply.github.com>
This commit is contained in:
@@ -393,7 +393,7 @@ function createRegisteredJdk(version = '21.0.8+9') {
|
||||
architecture: 'x64',
|
||||
version,
|
||||
source: `sha256:${path.basename(root)}`,
|
||||
verification: 'unverified',
|
||||
verification: 'disabled',
|
||||
path: jdkPath
|
||||
};
|
||||
registerJdk(jdk);
|
||||
|
||||
@@ -71,8 +71,11 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
|
||||
getJdkVerificationIdentity: jest.fn((verified: boolean, key?: string) =>
|
||||
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
|
||||
getJdkVerificationIdentity: jest.fn(
|
||||
(verified: boolean, enforced: boolean, key?: string) =>
|
||||
verified
|
||||
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
|
||||
: 'disabled'
|
||||
),
|
||||
registerJdk: jest.fn(),
|
||||
restoreJdk: jest.fn()
|
||||
@@ -395,8 +398,10 @@ describe('setupJava', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockImplementation(
|
||||
(verified: boolean, key?: string) =>
|
||||
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
|
||||
(verified: boolean, enforced: boolean, key?: string) =>
|
||||
verified
|
||||
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
|
||||
: 'disabled'
|
||||
);
|
||||
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
|
||||
spyGetToolcachePath.mockImplementation(
|
||||
@@ -826,7 +831,7 @@ describe('setupJava', () => {
|
||||
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
version: actualJavaVersion,
|
||||
verification: 'unverified'
|
||||
verification: 'disabled'
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -886,7 +891,7 @@ describe('setupJava', () => {
|
||||
architecture: 'x86',
|
||||
version: actualJavaVersion,
|
||||
source: `some/random_url/java/${actualJavaVersion}`,
|
||||
verification: 'unverified',
|
||||
verification: 'disabled',
|
||||
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
|
||||
});
|
||||
expect(downloadTool).not.toHaveBeenCalled();
|
||||
@@ -919,7 +924,7 @@ describe('setupJava', () => {
|
||||
architecture: 'x86',
|
||||
version: actualJavaVersion,
|
||||
source: `some/random_url/java/${actualJavaVersion}`,
|
||||
verification: 'unverified',
|
||||
verification: 'disabled',
|
||||
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
|
||||
};
|
||||
expect(jdkCache.restoreJdk).toHaveBeenCalledWith(expectedIdentity);
|
||||
|
||||
@@ -58,7 +58,7 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
|
||||
getJdkVerificationIdentity: jest.fn(() => 'unverified'),
|
||||
getJdkVerificationIdentity: jest.fn(() => 'disabled'),
|
||||
registerJdk: jest.fn(),
|
||||
restoreJdk: jest.fn()
|
||||
}));
|
||||
@@ -106,7 +106,7 @@ describe('setupJava', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockReturnValue(
|
||||
'unverified'
|
||||
'disabled'
|
||||
);
|
||||
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
|
||||
spyGetToolcachePath.mockImplementation(
|
||||
@@ -283,7 +283,7 @@ describe('setupJava', () => {
|
||||
expect.objectContaining({
|
||||
distribution: 'jdkfile',
|
||||
version: actualJavaVersion,
|
||||
verification: 'unverified'
|
||||
verification: 'disabled'
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -444,6 +444,64 @@ describe('downloadTool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('warns with key rotation recovery guidance when default verification fails', async () => {
|
||||
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
|
||||
const signedDistribution = new MicrosoftDistributions({
|
||||
version: '17',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
});
|
||||
|
||||
await signedDistribution['downloadTool']({
|
||||
version: '17.0.14+7',
|
||||
url: 'https://example.com/jdk.tar.gz',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
});
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
|
||||
)
|
||||
);
|
||||
expect(spyExtractJdkFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails with recovery guidance when verification is explicitly enabled', async () => {
|
||||
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
|
||||
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',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
})
|
||||
).rejects.toThrow(
|
||||
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
|
||||
);
|
||||
expect(spyExtractJdkFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('warns when the signature is missing during default verification', async () => {
|
||||
await distribution['downloadTool']({
|
||||
version: '17.0.14+7',
|
||||
url: 'https://example.com/jdk.tar.gz'
|
||||
});
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
"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();
|
||||
expect(spyExtractJdkFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails when signature is missing and verification is enabled', async () => {
|
||||
const signedDistribution = new MicrosoftDistributions({
|
||||
version: '17',
|
||||
|
||||
@@ -73,6 +73,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
|
||||
jest.unstable_mockModule('../../src/gpg.js', () => ({
|
||||
importKey: jest.fn(),
|
||||
removeGpgHome: jest.fn(),
|
||||
isGpgAvailable: jest.fn(),
|
||||
verifyPackageSignature: jest.fn()
|
||||
}));
|
||||
|
||||
@@ -437,6 +438,7 @@ describe('downloadTool', () => {
|
||||
beforeEach(() => {
|
||||
spyDownloadTool = tc.downloadTool as jest.Mock;
|
||||
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
|
||||
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(true);
|
||||
spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
|
||||
spyVerifySignature.mockResolvedValue(undefined);
|
||||
spyExtractJdkFile = util.extractJdkFile as jest.Mock;
|
||||
@@ -502,6 +504,133 @@ describe('downloadTool', () => {
|
||||
expect(spyVerifySignature).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips implicit signature verification when gpg is unavailable', async () => {
|
||||
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
|
||||
const distribution = new TemurinDistribution(
|
||||
{
|
||||
version: '17',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
},
|
||||
TemurinImplementation.Hotspot
|
||||
);
|
||||
|
||||
await expect(
|
||||
distribution['downloadTool']({
|
||||
version: '17.0.14+7',
|
||||
url: 'https://example.com/jdk.tar.gz',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
})
|
||||
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
|
||||
|
||||
expect(spyVerifySignature).not.toHaveBeenCalled();
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
"Input 'verify-signature' is enabled, but gpg is not available."
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when signature verification is explicitly enabled without gpg', async () => {
|
||||
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
|
||||
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',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
})
|
||||
).rejects.toThrow(
|
||||
"Input 'verify-signature' is enabled, but gpg is not available."
|
||||
);
|
||||
|
||||
expect(spyVerifySignature).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('warns when implicit signature verification fails', async () => {
|
||||
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
|
||||
const distribution = new TemurinDistribution(
|
||||
{
|
||||
version: '17',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
},
|
||||
TemurinImplementation.Hotspot
|
||||
);
|
||||
|
||||
await expect(
|
||||
distribution['downloadTool']({
|
||||
version: '17.0.14+7',
|
||||
url: 'https://example.com/jdk.tar.gz',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
})
|
||||
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'https://github.com/actions/setup-java#download-integrity-and-signatures'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when explicitly requested signature verification fails', async () => {
|
||||
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
|
||||
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',
|
||||
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
|
||||
})
|
||||
).rejects.toThrow(
|
||||
/Failed to verify signature for Temurin version 17\.0\.14\+7.*bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
|
||||
);
|
||||
});
|
||||
|
||||
it('warns when a signature is missing and verification is implicit', async () => {
|
||||
const distribution = new TemurinDistribution(
|
||||
{
|
||||
version: '17',
|
||||
architecture: 'x64',
|
||||
packageType: 'jdk',
|
||||
checkLatest: false
|
||||
},
|
||||
TemurinImplementation.Hotspot
|
||||
);
|
||||
|
||||
await expect(
|
||||
distribution['downloadTool']({
|
||||
version: '17.0.14+7',
|
||||
url: 'https://example.com/jdk.tar.gz'
|
||||
})
|
||||
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
"Input 'verify-signature' is enabled, but no signature URL was found for Temurin version 17.0.14+7."
|
||||
);
|
||||
expect(spyVerifySignature).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('downloads and adds matching JMODs to the JDK', async () => {
|
||||
spyDownloadTool
|
||||
.mockResolvedValueOnce('/tmp/jdk.tar.gz')
|
||||
|
||||
+28
-1
@@ -245,7 +245,7 @@ describe('gpg tests', () => {
|
||||
expect.any(String),
|
||||
'--batch',
|
||||
'--import',
|
||||
expect.stringContaining('public-key.asc')
|
||||
expect.stringContaining('public-key-0.asc')
|
||||
],
|
||||
expect.objectContaining({silent: true})
|
||||
);
|
||||
@@ -263,5 +263,32 @@ describe('gpg tests', () => {
|
||||
expect.objectContaining({silent: true})
|
||||
);
|
||||
});
|
||||
|
||||
it('imports multiple bundled keys before verifying the package', async () => {
|
||||
(tc.downloadTool as jest.Mock<any>).mockResolvedValue(
|
||||
'/tmp/jdk.tar.gz.sig'
|
||||
);
|
||||
|
||||
await gpg.verifyPackageSignature(
|
||||
'/tmp/jdk.tar.gz',
|
||||
'https://example.com/jdk.tar.gz.sig',
|
||||
['public-key-a', 'public-key-b']
|
||||
);
|
||||
|
||||
expect(exec.exec).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'gpg',
|
||||
[
|
||||
'--homedir',
|
||||
expect.any(String),
|
||||
'--batch',
|
||||
'--import',
|
||||
expect.stringContaining('public-key-0.asc'),
|
||||
expect.stringContaining('public-key-1.asc')
|
||||
],
|
||||
expect.objectContaining({silent: true})
|
||||
);
|
||||
expect(exec.exec).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+45
-18
@@ -43,7 +43,7 @@ const jdk = {
|
||||
architecture: 'x64',
|
||||
version: '21.0.8+9',
|
||||
source: 'sha256:abc123',
|
||||
verification: 'unverified',
|
||||
verification: 'disabled',
|
||||
path: '/toolcache/Java_temurin_jdk/21.0.8-9'
|
||||
};
|
||||
|
||||
@@ -117,32 +117,59 @@ describe('JDK cache', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('separates unverified, bundled-key, and custom-key caches', () => {
|
||||
const unverified = getJdkVerificationIdentity(false);
|
||||
const bundled = getJdkVerificationIdentity(true);
|
||||
it('separates verification policies and keys', () => {
|
||||
const disabled = getJdkVerificationIdentity(false, false);
|
||||
const checkAndWarnBundled = getJdkVerificationIdentity(true, false);
|
||||
const enforcedBundled = getJdkVerificationIdentity(true, true);
|
||||
const customA = getJdkVerificationIdentity(
|
||||
true,
|
||||
true,
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nkey-a\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n'
|
||||
);
|
||||
const customANormalized = getJdkVerificationIdentity(
|
||||
true,
|
||||
true,
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey-a\n-----END PGP PUBLIC KEY BLOCK-----'
|
||||
);
|
||||
const customB = getJdkVerificationIdentity(true, 'different-key');
|
||||
|
||||
expect(new Set([unverified, bundled, customA, customB])).toHaveProperty(
|
||||
'size',
|
||||
4
|
||||
const customB = getJdkVerificationIdentity(true, true, 'different-key');
|
||||
const checkAndWarnCustomA = getJdkVerificationIdentity(
|
||||
true,
|
||||
false,
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey-a\n-----END PGP PUBLIC KEY BLOCK-----'
|
||||
);
|
||||
const customList = getJdkVerificationIdentity(true, true, [
|
||||
'key-a',
|
||||
'key-b'
|
||||
]);
|
||||
const customListWithDifferentBoundary = getJdkVerificationIdentity(
|
||||
true,
|
||||
true,
|
||||
['key-ak', 'ey-b']
|
||||
);
|
||||
|
||||
expect(
|
||||
new Set([
|
||||
disabled,
|
||||
checkAndWarnBundled,
|
||||
enforcedBundled,
|
||||
customA,
|
||||
customB
|
||||
])
|
||||
).toHaveProperty('size', 5);
|
||||
expect(disabled).toBe('disabled');
|
||||
expect(checkAndWarnBundled).toBe('check-and-warn:bundled');
|
||||
expect(enforcedBundled).toBe('enforced:bundled');
|
||||
expect(checkAndWarnCustomA).not.toBe(customA);
|
||||
expect(customA).toBe(customANormalized);
|
||||
expect(customA).not.toContain('key-a');
|
||||
expect(customList).not.toBe(customListWithDifferentBoundary);
|
||||
expect(
|
||||
new Set(
|
||||
[unverified, bundled, customA, customB].map(verification =>
|
||||
buildJdkCacheKey({...jdk, verification})
|
||||
[disabled, checkAndWarnBundled, enforcedBundled, customA, customB].map(
|
||||
verification => buildJdkCacheKey({...jdk, verification})
|
||||
)
|
||||
)
|
||||
).toHaveProperty('size', 4);
|
||||
).toHaveProperty('size', 5);
|
||||
});
|
||||
|
||||
it('restores and records an exact JDK cache hit', async () => {
|
||||
@@ -210,12 +237,12 @@ describe('JDK cache', () => {
|
||||
|
||||
it('saves only the key matching the installation that occupies the path', async () => {
|
||||
const jdkPath = createInstallation();
|
||||
const verified = {...jdk, path: jdkPath, verification: 'verified:bundled'};
|
||||
const unverified = {...jdk, path: jdkPath};
|
||||
const enforced = {...jdk, path: jdkPath, verification: 'enforced:bundled'};
|
||||
const disabled = {...jdk, path: jdkPath};
|
||||
|
||||
registerJdk(verified);
|
||||
registerJdk(enforced);
|
||||
writeInstallation(jdkPath, 'force-downloaded-without-verification');
|
||||
registerJdk(unverified);
|
||||
registerJdk(disabled);
|
||||
(core.getState as jest.Mock).mockReturnValue(lastState());
|
||||
(cache.saveCache as jest.Mock).mockResolvedValue(1);
|
||||
|
||||
@@ -223,11 +250,11 @@ describe('JDK cache', () => {
|
||||
|
||||
expect(cache.saveCache).not.toHaveBeenCalledWith(
|
||||
[jdkPath],
|
||||
buildJdkCacheKey(verified)
|
||||
buildJdkCacheKey(enforced)
|
||||
);
|
||||
expect(cache.saveCache).toHaveBeenCalledWith(
|
||||
[jdkPath],
|
||||
buildJdkCacheKey(unverified)
|
||||
buildJdkCacheKey(disabled)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user