Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/e2e-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,29 @@ jobs:
run: bash __tests__/verify-java.sh "${{ matrix.version }}" "${{ steps.setup-java.outputs.path }}"
shell: bash

setup-java-temurin-signature-verification:
name: temurin ${{ matrix.version }} signature verification - ${{ matrix.os }}
needs: setup-java-major-minor-versions
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
version: ['21', '17']
steps:
- name: Checkout
uses: actions/checkout@v6
- name: setup-java with signature verification
uses: ./
id: setup-java
with:
java-version: ${{ matrix.version }}
distribution: temurin
verify-signature: true
- name: Verify Java
run: bash __tests__/verify-java.sh "${{ matrix.version }}" "${{ steps.setup-java.outputs.path }}"
shell: bash

setup-java-ea-versions-sapmachine:
name: sapmachine ${{ matrix.version }} (jdk-x64) - ${{ matrix.os }}
needs: setup-java-major-minor-versions
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ For information about the latest releases, recent updates, and newly supported d

- `check-latest`: Setting this option makes the action to check for the latest available version for the version spec.

- `verify-signature`: Verifies downloaded Java package signatures when supported by the selected distribution. Currently supported for `temurin`. If set to `true` for unsupported distributions, the action fails.

- `cache`: Quick [setup caching](#caching-packages-dependencies) for the dependencies managed through one of the predefined package managers. It can be one of "maven", "gradle" or "sbt".

- `cache-dependency-path`: The path to a dependency file: pom.xml, build.gradle, build.sbt, etc. This option can be used with the `cache` option. If this option is omitted, the action searches for the dependency file in the entire repository. This option supports wildcards and a list of file names for caching multiple dependencies.
Expand Down
297 changes: 198 additions & 99 deletions __tests__/data/temurin.json

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions __tests__/distributors/base-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,24 @@ describe('setupJava', () => {
}
);

it('should fail when verify-signature is enabled for unsupported distributions', async () => {
mockJavaBase = new EmptyJavaBase({
version: '11',
architecture: 'x86',
packageType: 'jdk',
checkLatest: false,
verifySignature: true
});

await expect(mockJavaBase.setupJava()).rejects.toThrow(
"Input 'verify-signature' is not supported for distribution 'Empty'."
);
expect(spyTcFindAllVersions).not.toHaveBeenCalled();
expect(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).not.toHaveBeenCalled();
expect(spyCoreSetOutput).not.toHaveBeenCalled();
});

it.each([
[
{
Expand Down
86 changes: 85 additions & 1 deletion __tests__/distributors/temurin-installer.test.ts
Original file line number Diff line number Diff line change
@@ -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_SIGNATURE_KEY_FINGERPRINT
} 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';
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -281,3 +287,81 @@ 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_SIGNATURE_KEY_FINGERPRINT
);
});

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();
});
});
41 changes: 41 additions & 0 deletions __tests__/gpg.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import * as io from '@actions/io';
import * as exec from '@actions/exec';
import * as tc from '@actions/tool-cache';
import * as gpg from '../src/gpg';

jest.mock('@actions/exec', () => {
Expand All @@ -9,6 +10,12 @@ jest.mock('@actions/exec', () => {
};
});

jest.mock('@actions/tool-cache', () => {
return {
downloadTool: jest.fn()
};
});

const tempDir = path.join(__dirname, 'runner', 'temp');
process.env['RUNNER_TEMP'] = tempDir;

Expand Down Expand Up @@ -51,5 +58,39 @@ describe('gpg tests', () => {
expect.anything()
);
});

describe('verifyPackageSignature', () => {
it('downloads signature and verifies package', async () => {
const testFingerprint = '3B04D753C9050D9A5D343F39843C48A565F8F04B';
(tc.downloadTool as jest.Mock).mockResolvedValue('/tmp/jdk.tar.gz.sig');
await gpg.verifyPackageSignature(
'/tmp/jdk.tar.gz',
'https://example.com/jdk.tar.gz.sig',
testFingerprint
);

expect(tc.downloadTool).toHaveBeenCalledWith(
'https://example.com/jdk.tar.gz.sig'
);
expect(exec.exec).toHaveBeenNthCalledWith(
1,
'gpg',
[
'--batch',
'--keyserver',
'keyserver.ubuntu.com',
'--recv-keys',
testFingerprint
],
expect.objectContaining({silent: true})
);
expect(exec.exec).toHaveBeenNthCalledWith(
2,
'gpg',
['--batch', '--verify', '/tmp/jdk.tar.gz.sig', '/tmp/jdk.tar.gz'],
expect.objectContaining({silent: true})
);
});
});
});
});
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ inputs:
description: 'Set this option if you want the action to check for the latest available version that satisfies the version spec'
required: false
default: false
verify-signature:
description: 'Verify downloaded Java package signatures when supported by the selected distribution'
required: false
default: false
server-id:
description: 'ID of the distributionManagement repository in the pom.xml
file. Default is `github`'
Expand Down
35 changes: 33 additions & 2 deletions dist/cleanup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51996,7 +51996,7 @@ else {
"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DISTRIBUTIONS_ONLY_MAJOR_VERSION = exports.INPUT_MVN_TOOLCHAIN_VENDOR = exports.INPUT_MVN_TOOLCHAIN_ID = exports.MVN_TOOLCHAINS_FILE = exports.MVN_SETTINGS_FILE = exports.M2_DIR = exports.STATE_GPG_PRIVATE_KEY_FINGERPRINT = exports.INPUT_JOB_STATUS = exports.INPUT_CACHE_DEPENDENCY_PATH = exports.INPUT_CACHE = exports.INPUT_DEFAULT_GPG_PASSPHRASE = exports.INPUT_DEFAULT_GPG_PRIVATE_KEY = exports.INPUT_GPG_PASSPHRASE = exports.INPUT_GPG_PRIVATE_KEY = exports.INPUT_OVERWRITE_SETTINGS = exports.INPUT_SETTINGS_PATH = exports.INPUT_SERVER_PASSWORD = exports.INPUT_SERVER_USERNAME = exports.INPUT_SERVER_ID = exports.INPUT_CHECK_LATEST = exports.INPUT_JDK_FILE = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_ARCHITECTURE = exports.INPUT_JAVA_VERSION_FILE = exports.INPUT_JAVA_VERSION = exports.MACOS_JAVA_CONTENT_POSTFIX = void 0;
exports.DISTRIBUTIONS_ONLY_MAJOR_VERSION = exports.INPUT_MVN_TOOLCHAIN_VENDOR = exports.INPUT_MVN_TOOLCHAIN_ID = exports.MVN_TOOLCHAINS_FILE = exports.MVN_SETTINGS_FILE = exports.M2_DIR = exports.STATE_GPG_PRIVATE_KEY_FINGERPRINT = exports.INPUT_JOB_STATUS = exports.INPUT_CACHE_DEPENDENCY_PATH = exports.INPUT_CACHE = exports.INPUT_DEFAULT_GPG_PASSPHRASE = exports.INPUT_DEFAULT_GPG_PRIVATE_KEY = exports.INPUT_GPG_PASSPHRASE = exports.INPUT_GPG_PRIVATE_KEY = exports.INPUT_OVERWRITE_SETTINGS = exports.INPUT_SETTINGS_PATH = exports.INPUT_SERVER_PASSWORD = exports.INPUT_SERVER_USERNAME = exports.INPUT_SERVER_ID = exports.INPUT_VERIFY_SIGNATURE = exports.INPUT_CHECK_LATEST = exports.INPUT_JDK_FILE = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_ARCHITECTURE = exports.INPUT_JAVA_VERSION_FILE = exports.INPUT_JAVA_VERSION = exports.MACOS_JAVA_CONTENT_POSTFIX = void 0;
exports.MACOS_JAVA_CONTENT_POSTFIX = 'Contents/Home';
exports.INPUT_JAVA_VERSION = 'java-version';
exports.INPUT_JAVA_VERSION_FILE = 'java-version-file';
Expand All @@ -52005,6 +52005,7 @@ exports.INPUT_JAVA_PACKAGE = 'java-package';
exports.INPUT_DISTRIBUTION = 'distribution';
exports.INPUT_JDK_FILE = 'jdkFile';
exports.INPUT_CHECK_LATEST = 'check-latest';
exports.INPUT_VERIFY_SIGNATURE = 'verify-signature';
exports.INPUT_SERVER_ID = 'server-id';
exports.INPUT_SERVER_USERNAME = 'server-username';
exports.INPUT_SERVER_PASSWORD = 'server-password';
Expand Down Expand Up @@ -52066,11 +52067,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.deleteKey = exports.importKey = exports.PRIVATE_KEY_FILE = void 0;
exports.verifyPackageSignature = exports.deleteKey = exports.importKey = exports.PRIVATE_KEY_FILE = void 0;
const fs = __importStar(__nccwpck_require__(79896));
const path = __importStar(__nccwpck_require__(16928));
const io = __importStar(__nccwpck_require__(94994));
const exec = __importStar(__nccwpck_require__(95236));
const tc = __importStar(__nccwpck_require__(33472));
const util = __importStar(__nccwpck_require__(54527));
exports.PRIVATE_KEY_FILE = path.join(util.getTempDir(), 'private-key.asc');
const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/;
Expand Down Expand Up @@ -52110,6 +52112,35 @@ function deleteKey(keyFingerprint) {
});
}
exports.deleteKey = deleteKey;
function verifyPackageSignature(archivePath, signatureUrl, keyFingerprint) {
return __awaiter(this, void 0, void 0, function* () {
const signaturePath = yield tc.downloadTool(signatureUrl);
let gpgHome;
try {
gpgHome = fs.mkdtempSync(path.join(util.getTempDir(), 'verify-signature-gpg-home-'));
}
catch (error) {
throw new Error(`Failed to create temporary GPG home directory for signature verification: ${error.message}`);
}
const env = Object.assign(Object.assign({}, process.env), { GNUPGHOME: gpgHome });
try {
const options = { silent: true, env };
yield exec.exec('gpg', [
'--batch',
'--keyserver',
'keyserver.ubuntu.com',
'--recv-keys',
keyFingerprint
], options);
yield exec.exec('gpg', ['--batch', '--verify', signaturePath, archivePath], options);
}
finally {
yield io.rmRF(signaturePath);
yield io.rmRF(gpgHome);
}
});
}
exports.verifyPackageSignature = verifyPackageSignature;


/***/ }),
Expand Down
Loading
Loading