Skip to content

Commit 0781fc6

Browse files
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>
1 parent 4889c4a commit 0781fc6

27 files changed

Lines changed: 577 additions & 138 deletions

‎README.md‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ steps:
5151
- Added Oracle OpenJDK (`oracle-openjdk`), Red Hat Build of OpenJDK (`redhat`), and Liberica Native Image Kit (`liberica-nik`), and expanded Tencent Kona support through JDK 25.
5252
- Added `java-version: latest` to resolve the newest stable GA release from the distribution's remote metadata.
5353
- Expanded install compatibility with JEP 322 multi-field versions such as `18.0.1.1`, Temurin `jdk+jmods` packages, and native musl artifacts on Alpine for Dragonwell, Corretto, Zulu, and Liberica.
54-
- JDK downloads now automatically verify authoritative checksums, and package signature verification defaults to enabled for Temurin and Microsoft builds.
54+
- JDK downloads now automatically verify authoritative checksums. Package signature verification is supported for Temurin and Microsoft builds, with configurable strict enforcement.
5555
- Added `force-download: true` to bypass the tool cache and perform a reproducible fresh install.
5656
- Dependency caching now supports custom paths with `cache-path` and restore-only operation with `cache-read-only: true`.
5757
- Dependency cache keys now include `.mvn/extensions.xml` and `gradle.properties`, preventing stale restores when Maven extensions or Gradle dependency properties change.
@@ -156,8 +156,8 @@ steps:
156156
| `force-download` | Always download Java and replace any matching version in the tool cache. | `false` |
157157
| `set-default` | Add Java to `PATH` and set `JAVA_HOME`. When `false`, only version-specific `JAVA_HOME_<major>_<arch>` variables are set. | `true` |
158158
| `problem-matcher` | Register Java compiler and uncaught exception problem matchers. | `true` |
159-
| `verify-signature` | Verify downloaded Java package signatures when supported. Defaults to `true` for `temurin` and `microsoft`, and `false` for other distributions. | Automatically enabled for `temurin` and `microsoft` |
160-
| `verify-signature-public-key` | ASCII-armored GPG public key to use for signature verification. Overrides the bundled key. | |
159+
| `verify-signature` | Verify downloaded Java package signatures when supported. Explicitly setting this to `true` makes verification failures fatal. | Distribution-dependent; see [Download integrity and signatures](#download-integrity-and-signatures) |
160+
| `verify-signature-public-key` | One or more ASCII-armored GPG public keys used for signature verification. Concatenate multiple armored key blocks. Custom keys replace the bundled distribution keys. | |
161161
| `token` | Token for fetching GitHub.com-hosted version manifests, useful on GitHub Enterprise Server when unauthenticated requests are rate-limited. | `${{ github.token }}` on GitHub.com; empty string on GHES |
162162
| `cache` | Enable dependency caching for `maven`, `gradle`, or `sbt`. | |
163163
| `cache-jdk` | Cache downloaded JDK installations between jobs. When omitted, JDK caching is enabled only if `cache` is set. Set explicitly to `true` or `false` to override. | Enabled when `cache` is set |
@@ -247,7 +247,34 @@ GitHub-hosted runners primarily pre-cache Eclipse Temurin JDKs. See the installe
247247

248248
Distributions or individual releases without an authoritative checksum continue to install normally, with the omission reported in debug logs. Installations resolved directly from the runner tool cache — including JDKs preinstalled on the runner image and JDKs installed by an earlier step of the same job — are not downloaded again and are not reverified, even when `verify-signature: true` is set. Use `force-download: true` to always download and verify the archive.
249249

250-
Use `verify-signature: true` to verify package signatures for distributions that support it. Currently supported distributions are `temurin` and `microsoft`; setting it for an unsupported distribution fails the workflow.
250+
Package signature verification is supported for `temurin` and `microsoft`. When `verify-signature` is omitted, the action checks the signature and warns if GPG is unavailable or verification fails, but does not enforce the result. Explicitly setting `verify-signature: true` enforces verification and makes these failures fatal. Setting `verify-signature: true` for an unsupported distribution also fails the workflow.
251+
252+
> [!WARNING]
253+
> Requesting explicit signature verification with verify-signature can fail a build after an unexpected but legitimate vendor signing-key rotation, because the action's bundled keys may not yet include the new key. Confirm a new key through the vendor's trusted documentation before using it.
254+
255+
After confirming a legitimate rotation, configure the updated key with `verify-signature-public-key`. The input accepts one or more ASCII-armored public keys; concatenate complete armored key blocks when both old and new vendor keys are needed during a transition. Custom keys replace, rather than extend, the keys bundled with the selected distribution.
256+
257+
```yaml
258+
- uses: actions/setup-java@v6
259+
with:
260+
distribution: temurin
261+
java-version: '25'
262+
verify-signature: true
263+
verify-signature-public-key: |
264+
-----BEGIN PGP PUBLIC KEY BLOCK-----
265+
...vendor key material...
266+
-----END PGP PUBLIC KEY BLOCK-----
267+
```
268+
269+
As a temporary fallback while a legitimate rotation is being investigated, set `verify-signature: false`. This disables package signature verification, although authoritative checksum verification still applies when the vendor publishes a checksum.
270+
271+
```yaml
272+
- uses: actions/setup-java@v6
273+
with:
274+
distribution: temurin
275+
java-version: '25'
276+
verify-signature: false
277+
```
251278

252279
## Caching
253280

‎__tests__/cleanup-java.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ function createRegisteredJdk(version = '21.0.8+9') {
393393
architecture: 'x64',
394394
version,
395395
source: `sha256:${path.basename(root)}`,
396-
verification: 'unverified',
396+
verification: 'disabled',
397397
path: jdkPath
398398
};
399399
registerJdk(jdk);

‎__tests__/distributors/base-installer.test.ts‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
7171
}));
7272

7373
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
74-
getJdkVerificationIdentity: jest.fn((verified: boolean, key?: string) =>
75-
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
74+
getJdkVerificationIdentity: jest.fn(
75+
(verified: boolean, enforced: boolean, key?: string) =>
76+
verified
77+
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
78+
: 'disabled'
7679
),
7780
registerJdk: jest.fn(),
7881
restoreJdk: jest.fn()
@@ -395,8 +398,10 @@ describe('setupJava', () => {
395398

396399
beforeEach(() => {
397400
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockImplementation(
398-
(verified: boolean, key?: string) =>
399-
verified ? (key ? 'verified:custom' : 'verified:bundled') : 'unverified'
401+
(verified: boolean, enforced: boolean, key?: string) =>
402+
verified
403+
? `${enforced ? 'enforced' : 'check-and-warn'}:${key ? 'custom' : 'bundled'}`
404+
: 'disabled'
400405
);
401406
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
402407
spyGetToolcachePath.mockImplementation(
@@ -826,7 +831,7 @@ describe('setupJava', () => {
826831
expect(jdkCache.registerJdk).toHaveBeenCalledWith(
827832
expect.objectContaining({
828833
version: actualJavaVersion,
829-
verification: 'unverified'
834+
verification: 'disabled'
830835
})
831836
);
832837
});
@@ -886,7 +891,7 @@ describe('setupJava', () => {
886891
architecture: 'x86',
887892
version: actualJavaVersion,
888893
source: `some/random_url/java/${actualJavaVersion}`,
889-
verification: 'unverified',
894+
verification: 'disabled',
890895
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
891896
});
892897
expect(downloadTool).not.toHaveBeenCalled();
@@ -919,7 +924,7 @@ describe('setupJava', () => {
919924
architecture: 'x86',
920925
version: actualJavaVersion,
921926
source: `some/random_url/java/${actualJavaVersion}`,
922-
verification: 'unverified',
927+
verification: 'disabled',
923928
path: path.join(toolCachePath, 'Java_Empty_jdk', actualJavaVersion)
924929
};
925930
expect(jdkCache.restoreJdk).toHaveBeenCalledWith(expectedIdentity);

‎__tests__/distributors/local-installer.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jest.unstable_mockModule('@actions/tool-cache', () => ({
5858
}));
5959

6060
jest.unstable_mockModule('../../src/jdk-cache.js', () => ({
61-
getJdkVerificationIdentity: jest.fn(() => 'unverified'),
61+
getJdkVerificationIdentity: jest.fn(() => 'disabled'),
6262
registerJdk: jest.fn(),
6363
restoreJdk: jest.fn()
6464
}));
@@ -106,7 +106,7 @@ describe('setupJava', () => {
106106

107107
beforeEach(() => {
108108
(jdkCache.getJdkVerificationIdentity as jest.Mock).mockReturnValue(
109-
'unverified'
109+
'disabled'
110110
);
111111
spyGetToolcachePath = util.getToolcachePath as jest.Mock;
112112
spyGetToolcachePath.mockImplementation(
@@ -283,7 +283,7 @@ describe('setupJava', () => {
283283
expect.objectContaining({
284284
distribution: 'jdkfile',
285285
version: actualJavaVersion,
286-
verification: 'unverified'
286+
verification: 'disabled'
287287
})
288288
);
289289
} finally {

‎__tests__/distributors/microsoft-installer.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,64 @@ describe('downloadTool', () => {
444444
);
445445
});
446446

447+
it('warns with key rotation recovery guidance when default verification fails', async () => {
448+
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
449+
const signedDistribution = new MicrosoftDistributions({
450+
version: '17',
451+
architecture: 'x64',
452+
packageType: 'jdk',
453+
checkLatest: false
454+
});
455+
456+
await signedDistribution['downloadTool']({
457+
version: '17.0.14+7',
458+
url: 'https://example.com/jdk.tar.gz',
459+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
460+
});
461+
462+
expect(core.warning).toHaveBeenCalledWith(
463+
expect.stringMatching(
464+
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
465+
)
466+
);
467+
expect(spyExtractJdkFile).toHaveBeenCalled();
468+
});
469+
470+
it('fails with recovery guidance when verification is explicitly enabled', async () => {
471+
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
472+
const signedDistribution = new MicrosoftDistributions({
473+
version: '17',
474+
architecture: 'x64',
475+
packageType: 'jdk',
476+
checkLatest: false,
477+
verifySignature: true
478+
});
479+
480+
await expect(
481+
signedDistribution['downloadTool']({
482+
version: '17.0.14+7',
483+
url: 'https://example.com/jdk.tar.gz',
484+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
485+
})
486+
).rejects.toThrow(
487+
/bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
488+
);
489+
expect(spyExtractJdkFile).not.toHaveBeenCalled();
490+
});
491+
492+
it('warns when the signature is missing during default verification', async () => {
493+
await distribution['downloadTool']({
494+
version: '17.0.14+7',
495+
url: 'https://example.com/jdk.tar.gz'
496+
});
497+
498+
expect(core.warning).toHaveBeenCalledWith(
499+
"Input 'verify-signature' is enabled, but no signature URL was found for Microsoft Build of OpenJDK version 17.0.14+7."
500+
);
501+
expect(spyVerifySignature).not.toHaveBeenCalled();
502+
expect(spyExtractJdkFile).toHaveBeenCalled();
503+
});
504+
447505
it('fails when signature is missing and verification is enabled', async () => {
448506
const signedDistribution = new MicrosoftDistributions({
449507
version: '17',

‎__tests__/distributors/temurin-installer.test.ts‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({
7373
jest.unstable_mockModule('../../src/gpg.js', () => ({
7474
importKey: jest.fn(),
7575
removeGpgHome: jest.fn(),
76+
isGpgAvailable: jest.fn(),
7677
verifyPackageSignature: jest.fn()
7778
}));
7879

@@ -437,6 +438,7 @@ describe('downloadTool', () => {
437438
beforeEach(() => {
438439
spyDownloadTool = tc.downloadTool as jest.Mock;
439440
spyDownloadTool.mockResolvedValue('/tmp/jdk.tar.gz');
441+
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(true);
440442
spyVerifySignature = gpg.verifyPackageSignature as jest.Mock;
441443
spyVerifySignature.mockResolvedValue(undefined);
442444
spyExtractJdkFile = util.extractJdkFile as jest.Mock;
@@ -502,6 +504,133 @@ describe('downloadTool', () => {
502504
expect(spyVerifySignature).not.toHaveBeenCalled();
503505
});
504506

507+
it('skips implicit signature verification when gpg is unavailable', async () => {
508+
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
509+
const distribution = new TemurinDistribution(
510+
{
511+
version: '17',
512+
architecture: 'x64',
513+
packageType: 'jdk',
514+
checkLatest: false
515+
},
516+
TemurinImplementation.Hotspot
517+
);
518+
519+
await expect(
520+
distribution['downloadTool']({
521+
version: '17.0.14+7',
522+
url: 'https://example.com/jdk.tar.gz',
523+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
524+
})
525+
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
526+
527+
expect(spyVerifySignature).not.toHaveBeenCalled();
528+
expect(core.warning).toHaveBeenCalledWith(
529+
"Input 'verify-signature' is enabled, but gpg is not available."
530+
);
531+
});
532+
533+
it('fails when signature verification is explicitly enabled without gpg', async () => {
534+
(gpg.isGpgAvailable as jest.Mock).mockResolvedValue(false);
535+
const distribution = new TemurinDistribution(
536+
{
537+
version: '17',
538+
architecture: 'x64',
539+
packageType: 'jdk',
540+
checkLatest: false,
541+
verifySignature: true
542+
},
543+
TemurinImplementation.Hotspot
544+
);
545+
546+
await expect(
547+
distribution['downloadTool']({
548+
version: '17.0.14+7',
549+
url: 'https://example.com/jdk.tar.gz',
550+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
551+
})
552+
).rejects.toThrow(
553+
"Input 'verify-signature' is enabled, but gpg is not available."
554+
);
555+
556+
expect(spyVerifySignature).not.toHaveBeenCalled();
557+
});
558+
559+
it('warns when implicit signature verification fails', async () => {
560+
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
561+
const distribution = new TemurinDistribution(
562+
{
563+
version: '17',
564+
architecture: 'x64',
565+
packageType: 'jdk',
566+
checkLatest: false
567+
},
568+
TemurinImplementation.Hotspot
569+
);
570+
571+
await expect(
572+
distribution['downloadTool']({
573+
version: '17.0.14+7',
574+
url: 'https://example.com/jdk.tar.gz',
575+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
576+
})
577+
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
578+
579+
expect(core.warning).toHaveBeenCalledWith(
580+
expect.stringContaining(
581+
'https://github.com/actions/setup-java#download-integrity-and-signatures'
582+
)
583+
);
584+
});
585+
586+
it('fails when explicitly requested signature verification fails', async () => {
587+
spyVerifySignature.mockRejectedValue(new Error('bad signature'));
588+
const distribution = new TemurinDistribution(
589+
{
590+
version: '17',
591+
architecture: 'x64',
592+
packageType: 'jdk',
593+
checkLatest: false,
594+
verifySignature: true
595+
},
596+
TemurinImplementation.Hotspot
597+
);
598+
599+
await expect(
600+
distribution['downloadTool']({
601+
version: '17.0.14+7',
602+
url: 'https://example.com/jdk.tar.gz',
603+
signatureUrl: 'https://example.com/jdk.tar.gz.sig'
604+
})
605+
).rejects.toThrow(
606+
/Failed to verify signature for Temurin version 17\.0\.14\+7.*bad signature.*https:\/\/github\.com\/actions\/setup-java#download-integrity-and-signatures/
607+
);
608+
});
609+
610+
it('warns when a signature is missing and verification is implicit', async () => {
611+
const distribution = new TemurinDistribution(
612+
{
613+
version: '17',
614+
architecture: 'x64',
615+
packageType: 'jdk',
616+
checkLatest: false
617+
},
618+
TemurinImplementation.Hotspot
619+
);
620+
621+
await expect(
622+
distribution['downloadTool']({
623+
version: '17.0.14+7',
624+
url: 'https://example.com/jdk.tar.gz'
625+
})
626+
).resolves.toEqual({version: '17.0.14+7', path: '/tmp/toolcache'});
627+
628+
expect(core.warning).toHaveBeenCalledWith(
629+
"Input 'verify-signature' is enabled, but no signature URL was found for Temurin version 17.0.14+7."
630+
);
631+
expect(spyVerifySignature).not.toHaveBeenCalled();
632+
});
633+
505634
it('downloads and adds matching JMODs to the JDK', async () => {
506635
spyDownloadTool
507636
.mockResolvedValueOnce('/tmp/jdk.tar.gz')

0 commit comments

Comments
 (0)