diff --git a/README.md b/README.md index 563a21e39..619e62e1d 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ steps: | `server-password-env-var` | Environment variable name for Maven repository password or token. | `GITHUB_TOKEN` | | `settings-path` | Directory where `settings.xml` is written. | `~/.m2` | | `overwrite-settings` | Overwrite an existing `settings.xml`. | `true` | -| `gpg-private-key` | GPG private key to import. | | +| `gpg-private-key` | GPG private key to import into an isolated temporary keyring. | | | `gpg-passphrase-env-var` | Environment variable name for the GPG private key passphrase. | `GPG_PASSPHRASE` when a key is set | | `mvn-toolchain-id` | Maven Toolchain ID. When multiple Java versions are installed, the number of IDs must match the number of versions. | `${mvn-toolchain-vendor}_${java-version}` | | `mvn-toolchain-vendor` | Maven Toolchain vendor value. | `${distribution}` | diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index c3303aa55..7457eb603 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -41,10 +41,18 @@ jest.unstable_mockModule('@actions/core', () => ({ toPosixPath: jest.fn((p: string) => p) })); +jest.unstable_mockModule('../src/gpg.js', () => ({ + importKey: jest.fn(), + removeGpgHome: jest.fn(), + toGpgPath: jest.fn() +})); + // Dynamic imports after mocking const core = await import('@actions/core'); +const gpg = await import('../src/gpg.js'); const auth = await import('../src/auth.js'); -const {M2_DIR, MVN_SETTINGS_FILE} = await import('../src/constants.js'); +const {M2_DIR, MVN_SETTINGS_FILE, STATE_GPG_HOME} = + await import('../src/constants.js'); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const m2Dir = path.join(__dirname, M2_DIR); @@ -60,8 +68,17 @@ describe('auth tests', () => { spyOSHomedir.mockReturnValue(__dirname); spyInfo = core.info as jest.Mock; spyInfo.mockImplementation(() => null); + (gpg.toGpgPath as jest.Mock).mockImplementation((p: string) => p); }, 300000); + afterEach(() => { + (core.getInput as jest.Mock).mockReset(); + (core.exportVariable as jest.Mock).mockReset(); + (gpg.importKey as jest.Mock).mockReset(); + (gpg.removeGpgHome as jest.Mock).mockReset(); + (gpg.toGpgPath as jest.Mock).mockReset(); + }); + afterAll(async () => { try { await io.rmRF(m2Dir); @@ -144,6 +161,52 @@ describe('auth tests', () => { ); }, 100000); + it('exports a GPG-compatible path and persists the native GPG home', async () => { + const gpgHome = 'D:\\a\\_temp\\setup-java-gpg-1'; + const exportedGpgHome = '/d/a/_temp/setup-java-gpg-1'; + (gpg.importKey as jest.Mock).mockResolvedValue(gpgHome); + (gpg.toGpgPath as jest.Mock).mockReturnValue(exportedGpgHome); + (core.getInput as jest.Mock).mockImplementation((name: string) => { + const inputs: Record = { + 'server-id': 'packages', + 'server-username-env-var': 'USERNAME', + 'server-password-env-var': 'PASSWORD', + 'settings-path': m2Dir, + 'gpg-private-key': 'KEY ONE\nKEY TWO' + }; + return inputs[name] ?? ''; + }); + + await auth.configureAuthentication(); + + expect(gpg.importKey).toHaveBeenCalledWith('KEY ONE\nKEY TWO'); + expect(core.saveState).toHaveBeenCalledWith(STATE_GPG_HOME, gpgHome); + expect(gpg.toGpgPath).toHaveBeenCalledWith(gpgHome); + expect(core.exportVariable).toHaveBeenCalledWith( + 'GNUPGHOME', + exportedGpgHome + ); + }); + + it('removes the isolated GPG home when environment export fails', async () => { + const gpgHome = path.join(__dirname, 'runner', 'temp', 'setup-java-gpg-2'); + (gpg.importKey as jest.Mock).mockResolvedValue(gpgHome); + (core.exportVariable as jest.Mock).mockImplementation(() => { + throw new Error('environment file unavailable'); + }); + (core.getInput as jest.Mock).mockImplementation((name: string) => { + if (name === 'gpg-private-key') return 'KEY CONTENTS'; + if (name === 'settings-path') return m2Dir; + return ''; + }); + + await expect(auth.configureAuthentication()).rejects.toThrow( + 'environment file unavailable' + ); + + expect(gpg.removeGpgHome).toHaveBeenCalledWith(gpgHome); + }); + it('overwrites existing settings.xml files', async () => { const id = 'packages'; const username = 'USERNAME'; diff --git a/__tests__/cleanup-java.test.ts b/__tests__/cleanup-java.test.ts index 82f7b3671..63b0df8a0 100644 --- a/__tests__/cleanup-java.test.ts +++ b/__tests__/cleanup-java.test.ts @@ -63,6 +63,8 @@ const core = await import('@actions/core'); const cache = await import('@actions/cache'); const {run: cleanup} = await import('../src/cleanup-java.js'); const util = await import('../src/util.js'); +const constants = await import('../src/constants.js'); +const {GPG_HOME_PREFIX} = await import('../src/gpg.js'); const {registerJdk, buildJdkCacheKey} = await import('../src/jdk-cache.js'); const jdkTempRoots: string[] = []; @@ -114,11 +116,65 @@ describe('cleanup', () => { (core.getInput as jest.Mock).mockImplementation((name: string) => { return name === 'cache' ? 'gradle' : ''; }); + await cleanup(); expect(spyCacheSave).toHaveBeenCalled(); expect(spyWarning).not.toHaveBeenCalled(); }); + it('removes the isolated GPG home without touching unrelated key material', async () => { + const tempDir = util.getTempDir(); + fs.mkdirSync(tempDir, {recursive: true}); + const gpgHome = fs.mkdtempSync(path.join(tempDir, GPG_HOME_PREFIX)); + const unrelatedGpgHome = fs.mkdtempSync( + path.join(tempDir, 'user-gpg-home-') + ); + fs.writeFileSync( + path.join(unrelatedGpgHome, 'private.key'), + 'pre-existing' + ); + (core.getInput as jest.Mock).mockReturnValue(''); + (core.getState as jest.Mock).mockImplementation((name: string) => + name === constants.STATE_GPG_HOME ? gpgHome : '' + ); + + await cleanup(); + + expect(fs.existsSync(gpgHome)).toBe(false); + expect( + fs.readFileSync(path.join(unrelatedGpgHome, 'private.key'), 'utf8') + ).toBe('pre-existing'); + fs.rmSync(unrelatedGpgHome, {recursive: true, force: true}); + }); + + it('makes repeated cleanup of the same GPG home idempotent', async () => { + const tempDir = util.getTempDir(); + fs.mkdirSync(tempDir, {recursive: true}); + const gpgHome = fs.mkdtempSync(path.join(tempDir, GPG_HOME_PREFIX)); + (core.getInput as jest.Mock).mockReturnValue(''); + (core.getState as jest.Mock).mockImplementation((name: string) => + name === constants.STATE_GPG_HOME ? gpgHome : '' + ); + + await cleanup(); + await cleanup(); + + expect(fs.existsSync(gpgHome)).toBe(false); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it('skips GPG cleanup when no home was persisted', async () => { + (core.getInput as jest.Mock).mockReturnValue(''); + (core.getState as jest.Mock).mockReturnValue(''); + + await cleanup(); + + expect(spyInfo).not.toHaveBeenCalledWith( + 'Removing private key from isolated GPG home' + ); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + it('does not fail even though the save process throws error', async () => { spyCacheSave.mockImplementation((paths: string[], key: string) => Promise.reject(new Error('Unexpected error')) @@ -148,7 +204,8 @@ describe('cleanup', () => { await cleanup(); expect(spyCacheSave).not.toHaveBeenCalled(); - expect(core.getState).not.toHaveBeenCalled(); + expect(core.getState).toHaveBeenCalledTimes(1); + expect(core.getState).toHaveBeenCalledWith(constants.STATE_GPG_HOME); expect(spyInfo).toHaveBeenCalledWith( 'Cache saving is skipped because cache-read-only is enabled.' ); diff --git a/__tests__/distributors/microsoft-installer.test.ts b/__tests__/distributors/microsoft-installer.test.ts index 0dc0ea532..8e0747ac5 100644 --- a/__tests__/distributors/microsoft-installer.test.ts +++ b/__tests__/distributors/microsoft-installer.test.ts @@ -86,7 +86,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({ jest.unstable_mockModule('../../src/gpg.js', () => ({ importKey: jest.fn(), - deleteKey: jest.fn(), + removeGpgHome: jest.fn(), verifyPackageSignature: jest.fn() })); diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index 793c3778b..830fdb943 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -72,7 +72,7 @@ jest.unstable_mockModule('../../src/util.js', () => ({ jest.unstable_mockModule('../../src/gpg.js', () => ({ importKey: jest.fn(), - deleteKey: jest.fn(), + removeGpgHome: jest.fn(), verifyPackageSignature: jest.fn() })); diff --git a/__tests__/gpg.test.ts b/__tests__/gpg.test.ts index 239643277..db69e3de8 100644 --- a/__tests__/gpg.test.ts +++ b/__tests__/gpg.test.ts @@ -8,6 +8,7 @@ import { afterEach } from '@jest/globals'; import {fileURLToPath} from 'url'; +import * as fs from 'fs'; import * as path from 'path'; import * as io from '@actions/io'; @@ -30,7 +31,10 @@ process.env['RUNNER_TEMP'] = tempDir; describe('gpg tests', () => { beforeEach(async () => { + await io.rmRF(tempDir); await io.mkdirP(tempDir); + jest.clearAllMocks(); + (exec.exec as jest.Mock).mockResolvedValue(0); }); afterAll(async () => { @@ -71,74 +75,193 @@ describe('gpg tests', () => { }); describe('importKey', () => { - it('attempts to import private key and returns null key id on failure', async () => { + it('imports private keys into a unique isolated GPG home', async () => { const privateKey = 'KEY CONTENTS'; - const keyId = await gpg.importKey(privateKey); + let privateKeyFile = ''; + (exec.exec as jest.Mock).mockImplementation( + async (_command: string, _args: string[]) => { + const [createdGpgHome] = fs.readdirSync(tempDir); + privateKeyFile = path.join( + tempDir, + createdGpgHome, + fs + .readdirSync(path.join(tempDir, createdGpgHome)) + .find(file => file.startsWith('private-key-')) ?? '' + ); + expect(fs.readFileSync(privateKeyFile, 'utf8')).toBe(privateKey); + if (process.platform !== 'win32') { + expect(fs.statSync(privateKeyFile).mode & 0o777).toBe(0o600); + } + return 0; + } + ); - expect(keyId).toBeNull(); + const gpgHome = await gpg.importKey(privateKey); + expect(path.dirname(gpgHome)).toBe(tempDir); + expect(path.basename(gpgHome).startsWith(gpg.GPG_HOME_PREFIX)).toBe(true); + expect(fs.existsSync(gpgHome)).toBe(true); + expect(fs.existsSync(privateKeyFile)).toBe(false); + if (process.platform !== 'win32') { + expect(fs.statSync(gpgHome).mode & 0o777).toBe(0o700); + } expect(exec.exec).toHaveBeenCalledWith( 'gpg', - expect.anything(), - expect.anything() + [ + '--homedir', + gpg.toGpgPath(gpgHome), + '--batch', + '--import', + gpg.toGpgPath(privateKeyFile) + ], + {silent: true} + ); + }); + + it('removes the private-key file and isolated home when import fails', async () => { + let gpgHome = ''; + let privateKeyFile = ''; + (exec.exec as jest.Mock).mockImplementation( + async (_command: string, _args: string[]) => { + const [createdGpgHome] = fs.readdirSync(tempDir); + gpgHome = path.join(tempDir, createdGpgHome); + privateKeyFile = path.join( + gpgHome, + fs + .readdirSync(gpgHome) + .find(file => file.startsWith('private-key-')) ?? '' + ); + expect(fs.existsSync(privateKeyFile)).toBe(true); + throw new Error('invalid key'); + } + ); + + await expect(gpg.importKey('INVALID KEY')).rejects.toThrow('invalid key'); + + expect(fs.existsSync(privateKeyFile)).toBe(false); + expect(fs.existsSync(gpgHome)).toBe(false); + }); + + it('imports multi-key input without parsing or deleting fingerprints', async () => { + const privateKeys = 'KEY ONE\nKEY TWO'; + (exec.exec as jest.Mock).mockImplementation( + async (_command: string, _args: string[]) => { + const [createdGpgHome] = fs.readdirSync(tempDir); + const keyFile = fs + .readdirSync(path.join(tempDir, createdGpgHome)) + .find(file => file.startsWith('private-key-')); + expect( + fs.readFileSync( + path.join(tempDir, createdGpgHome, keyFile ?? ''), + 'utf8' + ) + ).toBe(privateKeys); + return 0; + } + ); + + const gpgHome = await gpg.importKey(privateKeys); + + expect(gpgHome).toContain(gpg.GPG_HOME_PREFIX); + expect(exec.exec).toHaveBeenCalledTimes(1); + expect((exec.exec as jest.Mock).mock.calls[0][1]).not.toContain( + '--delete-secret-and-public-key' ); }); + + it('uses a separate GPG home for each invocation', async () => { + const firstGpgHome = await gpg.importKey('FIRST KEY'); + const secondGpgHome = await gpg.importKey('SECOND KEY'); + + expect(firstGpgHome).not.toBe(secondGpgHome); + expect(fs.existsSync(firstGpgHome)).toBe(true); + expect(fs.existsSync(secondGpgHome)).toBe(true); + }); }); - describe('deleteKey', () => { - it('deletes private key', async () => { - const keyId = 'asdfhjkl'; - await gpg.deleteKey(keyId); + describe('removeGpgHome', () => { + it('removes only action-owned GPG homes and is idempotent', async () => { + const gpgHome = await gpg.importKey('KEY CONTENTS'); + const unrelatedGpgHome = path.join(tempDir, 'user-gpg-home'); + fs.mkdirSync(unrelatedGpgHome); - expect(exec.exec).toHaveBeenCalledWith( - 'gpg', - expect.anything(), - expect.anything() + await gpg.removeGpgHome(gpgHome); + await gpg.removeGpgHome(gpgHome); + + expect(exec.exec).toHaveBeenNthCalledWith( + 2, + 'gpgconf', + ['--homedir', gpg.toGpgPath(gpgHome), '--kill', 'gpg-agent'], + {silent: true, ignoreReturnCode: true} ); + expect(exec.exec).toHaveBeenCalledTimes(2); + expect(fs.existsSync(gpgHome)).toBe(false); + expect(fs.existsSync(unrelatedGpgHome)).toBe(true); }); - describe('verifyPackageSignature', () => { - it('imports bundled key and verifies package', async () => { - const publicKeyContent = - '-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest\n-----END PGP PUBLIC KEY BLOCK-----'; - (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', - publicKeyContent - ); - - expect(tc.downloadTool).toHaveBeenCalledWith( - 'https://example.com/jdk.tar.gz.sig' - ); - expect(exec.exec).toHaveBeenNthCalledWith( - 1, - 'gpg', - [ - '--homedir', - expect.any(String), - '--batch', - '--import', - expect.stringContaining('public-key.asc') - ], - expect.objectContaining({silent: true}) - ); - expect(exec.exec).toHaveBeenNthCalledWith( - 2, - 'gpg', - [ - '--homedir', - expect.any(String), - '--batch', - '--verify', - '/tmp/jdk.tar.gz.sig', - '/tmp/jdk.tar.gz' - ], - expect.objectContaining({silent: true}) - ); - }); + it('removes the GPG home when gpgconf is unavailable', async () => { + const gpgHome = await gpg.importKey('KEY CONTENTS'); + (exec.exec as jest.Mock).mockRejectedValueOnce( + new Error('gpgconf not found') + ); + + await gpg.removeGpgHome(gpgHome); + + expect(fs.existsSync(gpgHome)).toBe(false); + }); + + it('refuses to remove a GPG home it does not own', async () => { + const unrelatedGpgHome = path.join(tempDir, 'user-gpg-home'); + fs.mkdirSync(unrelatedGpgHome, {recursive: true}); + + await expect(gpg.removeGpgHome(unrelatedGpgHome)).rejects.toThrow( + 'Refusing to remove unexpected GPG home' + ); + expect(fs.existsSync(unrelatedGpgHome)).toBe(true); + }); + }); + + describe('verifyPackageSignature', () => { + it('imports bundled key and verifies package', async () => { + const publicKeyContent = + '-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest\n-----END PGP PUBLIC KEY BLOCK-----'; + (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', + publicKeyContent + ); + + expect(tc.downloadTool).toHaveBeenCalledWith( + 'https://example.com/jdk.tar.gz.sig' + ); + expect(exec.exec).toHaveBeenNthCalledWith( + 1, + 'gpg', + [ + '--homedir', + expect.any(String), + '--batch', + '--import', + expect.stringContaining('public-key.asc') + ], + expect.objectContaining({silent: true}) + ); + expect(exec.exec).toHaveBeenNthCalledWith( + 2, + 'gpg', + [ + '--homedir', + expect.any(String), + '--batch', + '--verify', + '/tmp/jdk.tar.gz.sig', + '/tmp/jdk.tar.gz' + ], + expect.objectContaining({silent: true}) + ); }); }); }); diff --git a/action.yml b/action.yml index d27c23aca..60c1a00e9 100644 --- a/action.yml +++ b/action.yml @@ -72,7 +72,7 @@ inputs: required: false default: true gpg-private-key: - description: 'GPG private key to import. Default is empty string.' + description: 'GPG private key to import into an isolated temporary keyring. Default is empty string.' required: false default: '' gpg-passphrase-env-var: diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 734dc227c..2befb2c65 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -30769,13 +30769,12 @@ module.exports = { /* harmony export */ __nccwpck_require__.d(__webpack_exports__, { /* harmony export */ Ch: () => (/* binding */ INPUT_CACHE_READ_ONLY), +/* harmony export */ Fi: () => (/* binding */ STATE_GPG_HOME), /* harmony export */ GL: () => (/* binding */ INPUT_CACHE_JDK), /* harmony export */ gk: () => (/* binding */ INPUT_CACHE), -/* harmony export */ wG: () => (/* binding */ INPUT_JOB_STATUS), -/* harmony export */ wm: () => (/* binding */ STATE_GPG_PRIVATE_KEY_FINGERPRINT), -/* harmony export */ wz: () => (/* binding */ INPUT_GPG_PRIVATE_KEY) +/* harmony export */ wG: () => (/* binding */ INPUT_JOB_STATUS) /* harmony export */ }); -/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */ +/* unused harmony exports MACOS_JAVA_CONTENT_POSTFIX, INPUT_JAVA_VERSION, INPUT_JAVA_VERSION_FILE, INPUT_ARCHITECTURE, INPUT_JAVA_PACKAGE, INPUT_DISTRIBUTION, INPUT_JDK_FILE, INPUT_JDK_FILE_DEPRECATED, INPUT_CHECK_LATEST, INPUT_FORCE_DOWNLOAD, INPUT_SET_DEFAULT, INPUT_PROBLEM_MATCHER, INPUT_VERIFY_SIGNATURE, INPUT_VERIFY_SIGNATURE_PUBLIC_KEY, INPUT_SERVER_ID, INPUT_SERVER_USERNAME_ENV_VAR, INPUT_SERVER_PASSWORD_ENV_VAR, INPUT_SERVER_USERNAME_DEPRECATED, INPUT_SERVER_PASSWORD_DEPRECATED, INPUT_SETTINGS_PATH, INPUT_OVERWRITE_SETTINGS, INPUT_GPG_PRIVATE_KEY, INPUT_GPG_PASSPHRASE_ENV_VAR, INPUT_GPG_PASSPHRASE_DEPRECATED, INPUT_DEFAULT_SERVER_USERNAME, INPUT_DEFAULT_SERVER_PASSWORD, INPUT_DEFAULT_GPG_PRIVATE_KEY, INPUT_DEFAULT_GPG_PASSPHRASE, MAVEN_GPG_PASSPHRASE_DEFAULT_ENV, GPG_PASSPHRASE_PROFILE_ID, INPUT_CACHE_DEPENDENCY_PATH, INPUT_CACHE_PATH, M2_DIR, MVN_SETTINGS_FILE, MVN_TOOLCHAINS_FILE, INPUT_MVN_TOOLCHAIN_ID, INPUT_MVN_TOOLCHAIN_VENDOR, INPUT_SHOW_DOWNLOAD_PROGRESS, MAVEN_ARGS_ENV, MAVEN_NO_TRANSFER_PROGRESS_FLAG, MAVEN_NO_TRANSFER_PROGRESS_LONG_FLAG, DISTRIBUTIONS_ONLY_MAJOR_VERSION */ const MACOS_JAVA_CONTENT_POSTFIX = 'Contents/Home'; const INPUT_JAVA_VERSION = 'java-version'; const INPUT_JAVA_VERSION_FILE = 'java-version-file'; @@ -30816,7 +30815,7 @@ const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path'; const INPUT_CACHE_PATH = 'cache-path'; const INPUT_CACHE_READ_ONLY = 'cache-read-only'; const INPUT_JOB_STATUS = 'job-status'; -const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint'; +const STATE_GPG_HOME = 'gpg-home'; const M2_DIR = '.m2'; const MVN_SETTINGS_FILE = 'settings.xml'; const MVN_TOOLCHAINS_FILE = 'toolchains.xml'; @@ -34222,10 +34221,11 @@ const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCa /* harmony export */ Ck: () => (/* binding */ mkdir), /* harmony export */ H8: () => (/* binding */ IS_WINDOWS), /* harmony export */ Qh: () => (/* binding */ isRooted), +/* harmony export */ rm: () => (/* binding */ rm), /* harmony export */ t2: () => (/* binding */ exists), /* harmony export */ vr: () => (/* binding */ tryGetExecutablePath) /* harmony export */ }); -/* unused harmony exports chmod, copyFile, lstat, open, readdir, rename, rm, rmdir, stat, symlink, unlink, readlink, UV_FS_O_EXLOCK, READONLY, isDirectory, getCmdPath */ +/* unused harmony exports chmod, copyFile, lstat, open, readdir, rename, rmdir, stat, symlink, unlink, readlink, UV_FS_O_EXLOCK, READONLY, isDirectory, getCmdPath */ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(9896); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(6928); var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { @@ -34415,9 +34415,10 @@ function getCmdPath() { /* harmony export */ __nccwpck_require__.d(__webpack_exports__, { /* harmony export */ K7: () => (/* binding */ which), -/* harmony export */ U$: () => (/* binding */ mkdirP) +/* harmony export */ U$: () => (/* binding */ mkdirP), +/* harmony export */ Yz: () => (/* binding */ rmRF) /* harmony export */ }); -/* unused harmony exports cp, mv, rmRF, findInPath */ +/* unused harmony exports cp, mv, findInPath */ /* harmony import */ var assert__WEBPACK_IMPORTED_MODULE_0__ = __nccwpck_require__(2613); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __nccwpck_require__(6928); /* harmony import */ var _io_util_js__WEBPACK_IMPORTED_MODULE_2__ = __nccwpck_require__(90); @@ -34510,7 +34511,7 @@ function mv(source_1, dest_1) { */ function rmRF(inputPath) { return __awaiter(this, void 0, void 0, function* () { - if (ioUtil.IS_WINDOWS) { + if (_io_util_js__WEBPACK_IMPORTED_MODULE_2__/* .IS_WINDOWS */ .H8) { // Check for invalid characters // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file if (/[*"<>|]/.test(inputPath)) { @@ -34519,7 +34520,7 @@ function rmRF(inputPath) { } try { // note if path does not exist, error is silent - yield ioUtil.rm(inputPath, { + yield _io_util_js__WEBPACK_IMPORTED_MODULE_2__.rm(inputPath, { force: true, maxRetries: 3, recursive: true, @@ -35733,6 +35734,8 @@ var cleanup_java_core = __nccwpck_require__(3838); var external_fs_ = __nccwpck_require__(9896); // EXTERNAL MODULE: external "path" var external_path_ = __nccwpck_require__(6928); +// EXTERNAL MODULE: external "crypto" +var external_crypto_ = __nccwpck_require__(6982); // EXTERNAL MODULE: ./node_modules/@actions/io/lib/io.js var lib_io = __nccwpck_require__(8701); // EXTERNAL MODULE: ./node_modules/@actions/exec/lib/exec.js + 2 modules @@ -35748,8 +35751,9 @@ var src_util = __nccwpck_require__(4527); -const PRIVATE_KEY_FILE = external_path_.join(src_util/* getTempDir */.G4(), 'private-key.asc'); -const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/; + +const GPG_HOME_PREFIX = 'setup-java-gpg-'; +const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-'; // Convert a Windows path (D:\a\_temp\...) to a POSIX path (/d/a/_temp/...). // The Git-bundled GPG on Windows (MSYS2-based) uses POSIX path conventions // internally. Passing Windows paths with backslashes can cause fatal GPG errors @@ -35761,41 +35765,67 @@ function toGpgPath(p) { .replace(/\\/g, '/') .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } +function createGpgHome(prefix) { + const gpgHome = fs.mkdtempSync(path.join(util.getTempDir(), prefix)); + if (process.platform !== 'win32') { + fs.chmodSync(gpgHome, 0o700); + } + return gpgHome; +} async function importKey(privateKey) { - fs.writeFileSync(PRIVATE_KEY_FILE, privateKey, { - encoding: 'utf-8', - flag: 'w' - }); - let output = ''; - const options = { - silent: true, - listeners: { - stdout: (data) => { - output += data.toString(); - } + const gpgHome = createGpgHome(GPG_HOME_PREFIX); + const privateKeyFile = path.join(gpgHome, `private-key-${randomUUID()}.asc`); + try { + fs.writeFileSync(privateKeyFile, privateKey, { + encoding: 'utf-8', + flag: 'wx', + mode: 0o600 + }); + try { + await exec.exec('gpg', [ + '--homedir', + toGpgPath(gpgHome), + '--batch', + '--import', + toGpgPath(privateKeyFile) + ], { silent: true }); } - }; - await exec.exec('gpg', [ - '--batch', - '--import-options', - 'import-show', - '--import', - PRIVATE_KEY_FILE - ], options); - await io.rmRF(PRIVATE_KEY_FILE); - const match = output.match(PRIVATE_KEY_FINGERPRINT_REGEX); - return match && match[0]; -} -async function deleteKey(keyFingerprint) { - await lib_exec/* exec */.m('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); + finally { + fs.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} +async function removeGpgHome(gpgHome) { + if (!gpgHome) { + return; + } + const resolvedGpgHome = external_path_.resolve(gpgHome); + const resolvedTempDir = external_path_.resolve(src_util/* getTempDir */.G4()); + if (external_path_.dirname(resolvedGpgHome) !== resolvedTempDir || + !external_path_.basename(resolvedGpgHome).startsWith(GPG_HOME_PREFIX)) { + throw new Error(`Refusing to remove unexpected GPG home: ${gpgHome}`); + } + if (!external_fs_.existsSync(resolvedGpgHome)) { + return; + } + try { + await lib_exec/* exec */.m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); + } + catch { + // gpgconf may be unavailable, but directory removal must still be attempted. + } + await lib_io/* rmRF */.Yz(resolvedGpgHome); } async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { const signaturePath = await tc.downloadTool(signatureUrl); let gpgHome; try { - gpgHome = fs.mkdtempSync(path.join(util.getTempDir(), 'verify-signature-gpg-home-')); + gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { @@ -35842,16 +35872,17 @@ var external_url_ = __nccwpck_require__(7016); -async function removePrivateKeyFromKeychain() { - if (cleanup_java_core/* getInput */.V4(constants/* INPUT_GPG_PRIVATE_KEY */.wz, { required: false })) { - cleanup_java_core/* info */.pq('Removing private key from keychain'); - try { - const keyFingerprint = cleanup_java_core/* getState */.Gu(constants/* STATE_GPG_PRIVATE_KEY_FINGERPRINT */.wm); - await deleteKey(keyFingerprint); - } - catch (error) { - cleanup_java_core/* setFailed */.C1(`Failed to remove private key due to: ${error.message}`); - } +async function cleanup_java_removeGpgHome() { + const gpgHome = cleanup_java_core/* getState */.Gu(constants/* STATE_GPG_HOME */.Fi); + if (!gpgHome) { + return; + } + cleanup_java_core/* info */.pq('Removing private key from isolated GPG home'); + try { + await removeGpgHome(gpgHome); + } + catch (error) { + cleanup_java_core/* setFailed */.C1(`Failed to remove isolated GPG home due to: ${error.message}`); } } /** @@ -35899,7 +35930,7 @@ async function ignoreError(promise) { }); } async function run() { - await removePrivateKeyFromKeychain(); + await cleanup_java_removeGpgHome(); await ignoreError(saveCaches()); } if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) { diff --git a/dist/setup/220.index.js b/dist/setup/220.index.js index 64353cbc4..e9ba3c91f 100644 --- a/dist/setup/220.index.js +++ b/dist/setup/220.index.js @@ -170,25 +170,30 @@ class MicrosoftDistributions extends base_installer/* JavaBase */.O { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Fh: () => (/* binding */ importKey), -/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature) +/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature), +/* harmony export */ mS: () => (/* binding */ removeGpgHome), +/* harmony export */ nY: () => (/* binding */ toGpgPath) /* harmony export */ }); -/* unused harmony exports PRIVATE_KEY_FILE, toGpgPath, deleteKey */ +/* unused harmony export GPG_HOME_PREFIX */ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9896); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8701); -/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5260); -/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9805); -/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4527); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6982); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8701); +/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5260); +/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9805); +/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(4527); -const PRIVATE_KEY_FILE = path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'private-key.asc'); -const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/; + +const GPG_HOME_PREFIX = 'setup-java-gpg-'; +const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-'; // Convert a Windows path (D:\a\_temp\...) to a POSIX path (/d/a/_temp/...). // The Git-bundled GPG on Windows (MSYS2-based) uses POSIX path conventions // internally. Passing Windows paths with backslashes can cause fatal GPG errors @@ -200,45 +205,71 @@ function toGpgPath(p) { .replace(/\\/g, '/') .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } +function createGpgHome(prefix) { + const gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4(), prefix)); + if (process.platform !== 'win32') { + fs__WEBPACK_IMPORTED_MODULE_0__.chmodSync(gpgHome, 0o700); + } + return gpgHome; +} async function importKey(privateKey) { - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(PRIVATE_KEY_FILE, privateKey, { - encoding: 'utf-8', - flag: 'w' - }); - let output = ''; - const options = { - silent: true, - listeners: { - stdout: (data) => { - output += data.toString(); - } + const gpgHome = createGpgHome(GPG_HOME_PREFIX); + const privateKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, `private-key-${(0,crypto__WEBPACK_IMPORTED_MODULE_2__.randomUUID)()}.asc`); + try { + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(privateKeyFile, privateKey, { + encoding: 'utf-8', + flag: 'wx', + mode: 0o600 + }); + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ + '--homedir', + toGpgPath(gpgHome), + '--batch', + '--import', + toGpgPath(privateKeyFile) + ], { silent: true }); } - }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ - '--batch', - '--import-options', - 'import-show', - '--import', - PRIVATE_KEY_FILE - ], options); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(PRIVATE_KEY_FILE); - const match = output.match(PRIVATE_KEY_FINGERPRINT_REGEX); - return match && match[0]; + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } } -async function deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function removeGpgHome(gpgHome) { + if (!gpgHome) { + return; + } + const resolvedGpgHome = path__WEBPACK_IMPORTED_MODULE_1__.resolve(gpgHome); + const resolvedTempDir = path__WEBPACK_IMPORTED_MODULE_1__.resolve(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4()); + if (path__WEBPACK_IMPORTED_MODULE_1__.dirname(resolvedGpgHome) !== resolvedTempDir || + !path__WEBPACK_IMPORTED_MODULE_1__.basename(resolvedGpgHome).startsWith(GPG_HOME_PREFIX)) { + throw new Error(`Refusing to remove unexpected GPG home: ${gpgHome}`); + } + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { + return; + } + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); + } + catch { + // gpgconf may be unavailable, but directory removal must still be attempted. + } + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome); } async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { - const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__/* .downloadTool */ .bq(signatureUrl); + const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); let gpgHome; try { - gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'verify-signature-gpg-home-')); + gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); } catch { // ignore cleanup failures @@ -249,14 +280,14 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten const publicKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, 'public-key.asc'); fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(publicKeyFile, publicKeyContent, { encoding: 'utf-8' }); const options = { silent: true }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', '--import', toGpgPath(publicKeyFile) ], options); - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', @@ -266,8 +297,8 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten ], options); } finally { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(gpgHome); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); } } diff --git a/dist/setup/463.index.js b/dist/setup/463.index.js index cd19ab248..c0578ee6f 100644 --- a/dist/setup/463.index.js +++ b/dist/setup/463.index.js @@ -268,25 +268,30 @@ class TemurinDistribution extends base_installer/* JavaBase */.O { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Fh: () => (/* binding */ importKey), -/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature) +/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature), +/* harmony export */ mS: () => (/* binding */ removeGpgHome), +/* harmony export */ nY: () => (/* binding */ toGpgPath) /* harmony export */ }); -/* unused harmony exports PRIVATE_KEY_FILE, toGpgPath, deleteKey */ +/* unused harmony export GPG_HOME_PREFIX */ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9896); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8701); -/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5260); -/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9805); -/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4527); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6982); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8701); +/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5260); +/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9805); +/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(4527); -const PRIVATE_KEY_FILE = path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'private-key.asc'); -const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/; + +const GPG_HOME_PREFIX = 'setup-java-gpg-'; +const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-'; // Convert a Windows path (D:\a\_temp\...) to a POSIX path (/d/a/_temp/...). // The Git-bundled GPG on Windows (MSYS2-based) uses POSIX path conventions // internally. Passing Windows paths with backslashes can cause fatal GPG errors @@ -298,45 +303,71 @@ function toGpgPath(p) { .replace(/\\/g, '/') .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } +function createGpgHome(prefix) { + const gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4(), prefix)); + if (process.platform !== 'win32') { + fs__WEBPACK_IMPORTED_MODULE_0__.chmodSync(gpgHome, 0o700); + } + return gpgHome; +} async function importKey(privateKey) { - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(PRIVATE_KEY_FILE, privateKey, { - encoding: 'utf-8', - flag: 'w' - }); - let output = ''; - const options = { - silent: true, - listeners: { - stdout: (data) => { - output += data.toString(); - } + const gpgHome = createGpgHome(GPG_HOME_PREFIX); + const privateKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, `private-key-${(0,crypto__WEBPACK_IMPORTED_MODULE_2__.randomUUID)()}.asc`); + try { + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(privateKeyFile, privateKey, { + encoding: 'utf-8', + flag: 'wx', + mode: 0o600 + }); + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ + '--homedir', + toGpgPath(gpgHome), + '--batch', + '--import', + toGpgPath(privateKeyFile) + ], { silent: true }); } - }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ - '--batch', - '--import-options', - 'import-show', - '--import', - PRIVATE_KEY_FILE - ], options); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(PRIVATE_KEY_FILE); - const match = output.match(PRIVATE_KEY_FINGERPRINT_REGEX); - return match && match[0]; + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } } -async function deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function removeGpgHome(gpgHome) { + if (!gpgHome) { + return; + } + const resolvedGpgHome = path__WEBPACK_IMPORTED_MODULE_1__.resolve(gpgHome); + const resolvedTempDir = path__WEBPACK_IMPORTED_MODULE_1__.resolve(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4()); + if (path__WEBPACK_IMPORTED_MODULE_1__.dirname(resolvedGpgHome) !== resolvedTempDir || + !path__WEBPACK_IMPORTED_MODULE_1__.basename(resolvedGpgHome).startsWith(GPG_HOME_PREFIX)) { + throw new Error(`Refusing to remove unexpected GPG home: ${gpgHome}`); + } + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { + return; + } + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); + } + catch { + // gpgconf may be unavailable, but directory removal must still be attempted. + } + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome); } async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { - const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__/* .downloadTool */ .bq(signatureUrl); + const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); let gpgHome; try { - gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'verify-signature-gpg-home-')); + gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); } catch { // ignore cleanup failures @@ -347,14 +378,14 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten const publicKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, 'public-key.asc'); fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(publicKeyFile, publicKeyContent, { encoding: 'utf-8' }); const options = { silent: true }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', '--import', toGpgPath(publicKeyFile) ], options); - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', @@ -364,8 +395,8 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten ], options); } finally { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(gpgHome); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); } } diff --git a/dist/setup/81.index.js b/dist/setup/81.index.js index 85410c772..4d98b9c2a 100644 --- a/dist/setup/81.index.js +++ b/dist/setup/81.index.js @@ -49,8 +49,15 @@ async function configureAuthentication() { await createAuthenticationSettings(id, usernameEnvVar, passwordEnvVar, settingsDirectory, overwriteSettings, gpgPassphraseEnvVar); if (gpgPrivateKey) { _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .info */ .pq('Importing private gpg key'); - const keyFingerprint = (await _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .importKey */ .Fh(gpgPrivateKey)) || ''; - _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .saveState */ .LZ(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .STATE_GPG_PRIVATE_KEY_FINGERPRINT */ .wm, keyFingerprint); + const gpgHome = await _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .importKey */ .Fh(gpgPrivateKey); + try { + _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .saveState */ .LZ(_constants_js__WEBPACK_IMPORTED_MODULE_7__/* .STATE_GPG_HOME */ .Fi, gpgHome); + _actions_core__WEBPACK_IMPORTED_MODULE_1__/* .exportVariable */ .dN('GNUPGHOME', _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .toGpgPath */ .nY(gpgHome)); + } + catch (error) { + await _gpg_js__WEBPACK_IMPORTED_MODULE_5__/* .removeGpgHome */ .mS(gpgHome); + throw error; + } } } function getInputWithDeprecatedAlias(inputName, deprecatedInputName, defaultValue) { @@ -124,25 +131,30 @@ async function write(directory, settings, overwriteSettings) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Fh: () => (/* binding */ importKey), -/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature) +/* harmony export */ Yi: () => (/* binding */ verifyPackageSignature), +/* harmony export */ mS: () => (/* binding */ removeGpgHome), +/* harmony export */ nY: () => (/* binding */ toGpgPath) /* harmony export */ }); -/* unused harmony exports PRIVATE_KEY_FILE, toGpgPath, deleteKey */ +/* unused harmony export GPG_HOME_PREFIX */ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9896); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8701); -/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5260); -/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9805); -/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4527); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6982); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _actions_io__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8701); +/* harmony import */ var _actions_exec__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5260); +/* harmony import */ var _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9805); +/* harmony import */ var _util_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(4527); + -const PRIVATE_KEY_FILE = path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'private-key.asc'); -const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/; +const GPG_HOME_PREFIX = 'setup-java-gpg-'; +const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-'; // Convert a Windows path (D:\a\_temp\...) to a POSIX path (/d/a/_temp/...). // The Git-bundled GPG on Windows (MSYS2-based) uses POSIX path conventions // internally. Passing Windows paths with backslashes can cause fatal GPG errors @@ -154,45 +166,71 @@ function toGpgPath(p) { .replace(/\\/g, '/') .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } +function createGpgHome(prefix) { + const gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4(), prefix)); + if (process.platform !== 'win32') { + fs__WEBPACK_IMPORTED_MODULE_0__.chmodSync(gpgHome, 0o700); + } + return gpgHome; +} async function importKey(privateKey) { - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(PRIVATE_KEY_FILE, privateKey, { - encoding: 'utf-8', - flag: 'w' - }); - let output = ''; - const options = { - silent: true, - listeners: { - stdout: (data) => { - output += data.toString(); - } + const gpgHome = createGpgHome(GPG_HOME_PREFIX); + const privateKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, `private-key-${(0,crypto__WEBPACK_IMPORTED_MODULE_2__.randomUUID)()}.asc`); + try { + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(privateKeyFile, privateKey, { + encoding: 'utf-8', + flag: 'wx', + mode: 0o600 + }); + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ + '--homedir', + toGpgPath(gpgHome), + '--batch', + '--import', + toGpgPath(privateKeyFile) + ], { silent: true }); } - }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ - '--batch', - '--import-options', - 'import-show', - '--import', - PRIVATE_KEY_FILE - ], options); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(PRIVATE_KEY_FILE); - const match = output.match(PRIVATE_KEY_FINGERPRINT_REGEX); - return match && match[0]; + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } } -async function deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function removeGpgHome(gpgHome) { + if (!gpgHome) { + return; + } + const resolvedGpgHome = path__WEBPACK_IMPORTED_MODULE_1__.resolve(gpgHome); + const resolvedTempDir = path__WEBPACK_IMPORTED_MODULE_1__.resolve(_util_js__WEBPACK_IMPORTED_MODULE_6__/* .getTempDir */ .G4()); + if (path__WEBPACK_IMPORTED_MODULE_1__.dirname(resolvedGpgHome) !== resolvedTempDir || + !path__WEBPACK_IMPORTED_MODULE_1__.basename(resolvedGpgHome).startsWith(GPG_HOME_PREFIX)) { + throw new Error(`Refusing to remove unexpected GPG home: ${gpgHome}`); + } + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(resolvedGpgHome)) { + return; + } + try { + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpgconf', ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], { silent: true, ignoreReturnCode: true }); + } + catch { + // gpgconf may be unavailable, but directory removal must still be attempted. + } + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(resolvedGpgHome); } async function verifyPackageSignature(archivePath, signatureUrl, publicKeyContent) { - const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_4__/* .downloadTool */ .bq(signatureUrl); + const signaturePath = await _actions_tool_cache__WEBPACK_IMPORTED_MODULE_5__/* .downloadTool */ .bq(signatureUrl); let gpgHome; try { - gpgHome = fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(path__WEBPACK_IMPORTED_MODULE_1__.join(_util_js__WEBPACK_IMPORTED_MODULE_5__/* .getTempDir */ .G4(), 'verify-signature-gpg-home-')); + gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); } catch { // ignore cleanup failures @@ -203,14 +241,14 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten const publicKeyFile = path__WEBPACK_IMPORTED_MODULE_1__.join(gpgHome, 'public-key.asc'); fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(publicKeyFile, publicKeyContent, { encoding: 'utf-8' }); const options = { silent: true }; - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', '--import', toGpgPath(publicKeyFile) ], options); - await _actions_exec__WEBPACK_IMPORTED_MODULE_3__/* .exec */ .m('gpg', [ + await _actions_exec__WEBPACK_IMPORTED_MODULE_4__/* .exec */ .m('gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', @@ -220,8 +258,8 @@ async function verifyPackageSignature(archivePath, signatureUrl, publicKeyConten ], options); } finally { - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(signaturePath); - await _actions_io__WEBPACK_IMPORTED_MODULE_2__/* .rmRF */ .Yz(gpgHome); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(signaturePath); + await _actions_io__WEBPACK_IMPORTED_MODULE_3__/* .rmRF */ .Yz(gpgHome); } } diff --git a/dist/setup/index.js b/dist/setup/index.js index 4b7ec051c..992717c79 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -30770,6 +30770,7 @@ module.exports = { /* harmony export */ __nccwpck_require__.d(__webpack_exports__, { /* harmony export */ At: () => (/* binding */ INPUT_CACHE_DEPENDENCY_PATH), /* harmony export */ E8: () => (/* binding */ INPUT_SET_DEFAULT), +/* harmony export */ Fi: () => (/* binding */ STATE_GPG_HOME), /* harmony export */ GL: () => (/* binding */ INPUT_CACHE_JDK), /* harmony export */ I9: () => (/* binding */ INPUT_FORCE_DOWNLOAD), /* harmony export */ K$: () => (/* binding */ GPG_PASSPHRASE_PROFILE_ID), @@ -30810,7 +30811,6 @@ module.exports = { /* harmony export */ vO: () => (/* binding */ MVN_SETTINGS_FILE), /* harmony export */ wX: () => (/* binding */ INPUT_SHOW_DOWNLOAD_PROGRESS), /* harmony export */ wc: () => (/* binding */ INPUT_JDK_FILE_DEPRECATED), -/* harmony export */ wm: () => (/* binding */ STATE_GPG_PRIVATE_KEY_FINGERPRINT), /* harmony export */ wz: () => (/* binding */ INPUT_GPG_PRIVATE_KEY), /* harmony export */ xp: () => (/* binding */ INPUT_DEFAULT_SERVER_PASSWORD) /* harmony export */ }); @@ -30855,7 +30855,7 @@ const INPUT_CACHE_DEPENDENCY_PATH = 'cache-dependency-path'; const INPUT_CACHE_PATH = 'cache-path'; const INPUT_CACHE_READ_ONLY = 'cache-read-only'; const INPUT_JOB_STATUS = 'job-status'; -const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint'; +const STATE_GPG_HOME = 'gpg-home'; const M2_DIR = '.m2'; const MVN_SETTINGS_FILE = 'settings.xml'; const MVN_TOOLCHAINS_FILE = 'toolchains.xml'; diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index ffe700c79..4a6ec8975 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -873,7 +873,7 @@ See the help docs on [Publishing a Package](https://help.github.com/en/github/ma #### Legacy / alternative: let setup-java import the key -If you prefer signing with the `gpg` executable (for example because you are using `maven-gpg-plugin` older than 3.2.0), you can let setup-java import the key instead by providing the `gpg-private-key` and `gpg-passphrase-env-var` inputs. The private key is written to a file in the runner's temp directory, imported into the GPG keychain, and the file is promptly removed before proceeding with the rest of the setup process. A cleanup step removes the imported private key from the GPG keychain after the job completes regardless of the job status. This ensures that the private key is no longer accessible on self-hosted runners and cannot "leak" between jobs (hosted runners are always clean instances). +If you prefer signing with the `gpg` executable (for example because you are using `maven-gpg-plugin` older than 3.2.0), you can let setup-java import the key instead by providing the `gpg-private-key` and `gpg-passphrase-env-var` inputs. setup-java creates a uniquely named, permission-restricted GPG home in the runner's temp directory, imports the key only into that isolated keyring, and exports `GNUPGHOME` for subsequent Maven and GPG commands. The temporary key file is permission-restricted and removed whether the import succeeds or fails. A cleanup step removes the complete action-owned GPG home after the job regardless of job status, without modifying the runner user's default keyring. Each setup-java invocation owns a separate keyring, including on persistent self-hosted runners. setup-java imports the key independently of the plugin version, but the generated passphrase profile described below uses `gpg.passphraseEnvName`, which requires `maven-gpg-plugin` 3.2.0 or newer. Since `gpg-passphrase-env-var` defaults to `GPG_PASSPHRASE`, setup-java writes that profile unless you override the input to `MAVEN_GPG_PASSPHRASE`. diff --git a/src/auth.ts b/src/auth.ts index 277342c55..49bf9ae34 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -52,8 +52,14 @@ export async function configureAuthentication() { if (gpgPrivateKey) { core.info('Importing private gpg key'); - const keyFingerprint = (await gpg.importKey(gpgPrivateKey)) || ''; - core.saveState(constants.STATE_GPG_PRIVATE_KEY_FINGERPRINT, keyFingerprint); + const gpgHome = await gpg.importKey(gpgPrivateKey); + try { + core.saveState(constants.STATE_GPG_HOME, gpgHome); + core.exportVariable('GNUPGHOME', gpg.toGpgPath(gpgHome)); + } catch (error) { + await gpg.removeGpgHome(gpgHome); + throw error; + } } } diff --git a/src/cleanup-java.ts b/src/cleanup-java.ts index b9fae6b2f..cf333d00c 100644 --- a/src/cleanup-java.ts +++ b/src/cleanup-java.ts @@ -8,19 +8,19 @@ import { } from './util.js'; import {fileURLToPath} from 'url'; -async function removePrivateKeyFromKeychain() { - if (core.getInput(constants.INPUT_GPG_PRIVATE_KEY, {required: false})) { - core.info('Removing private key from keychain'); - try { - const keyFingerprint = core.getState( - constants.STATE_GPG_PRIVATE_KEY_FINGERPRINT - ); - await gpg.deleteKey(keyFingerprint); - } catch (error) { - core.setFailed( - `Failed to remove private key due to: ${(error as Error).message}` - ); - } +async function removeGpgHome() { + const gpgHome = core.getState(constants.STATE_GPG_HOME); + if (!gpgHome) { + return; + } + + core.info('Removing private key from isolated GPG home'); + try { + await gpg.removeGpgHome(gpgHome); + } catch (error) { + core.setFailed( + `Failed to remove isolated GPG home due to: ${(error as Error).message}` + ); } } @@ -73,7 +73,7 @@ async function ignoreError(promise: Promise) { } export async function run() { - await removePrivateKeyFromKeychain(); + await removeGpgHome(); await ignoreError(saveCaches()); } diff --git a/src/constants.ts b/src/constants.ts index 6e19a5873..83a2aaf00 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,7 +43,7 @@ export const INPUT_CACHE_PATH = 'cache-path'; export const INPUT_CACHE_READ_ONLY = 'cache-read-only'; export const INPUT_JOB_STATUS = 'job-status'; -export const STATE_GPG_PRIVATE_KEY_FINGERPRINT = 'gpg-private-key-fingerprint'; +export const STATE_GPG_HOME = 'gpg-home'; export const M2_DIR = '.m2'; export const MVN_SETTINGS_FILE = 'settings.xml'; diff --git a/src/gpg.ts b/src/gpg.ts index 6eb78721f..46e0a6ce5 100644 --- a/src/gpg.ts +++ b/src/gpg.ts @@ -1,14 +1,14 @@ import * as fs from 'fs'; import * as path from 'path'; +import {randomUUID} from 'crypto'; import * as io from '@actions/io'; import * as exec from '@actions/exec'; import * as tc from '@actions/tool-cache'; import * as util from './util.js'; import {ExecOptions} from '@actions/exec'; -export const PRIVATE_KEY_FILE = path.join(util.getTempDir(), 'private-key.asc'); - -const PRIVATE_KEY_FINGERPRINT_REGEX = /\w{40}/; +export const GPG_HOME_PREFIX = 'setup-java-gpg-'; +const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-'; // Convert a Windows path (D:\a\_temp\...) to a POSIX path (/d/a/_temp/...). // The Git-bundled GPG on Windows (MSYS2-based) uses POSIX path conventions @@ -21,49 +21,77 @@ export function toGpgPath(p: string): string { .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } -export async function importKey(privateKey: string) { - fs.writeFileSync(PRIVATE_KEY_FILE, privateKey, { - encoding: 'utf-8', - flag: 'w' - }); +function createGpgHome(prefix: string): string { + const gpgHome = fs.mkdtempSync(path.join(util.getTempDir(), prefix)); + if (process.platform !== 'win32') { + fs.chmodSync(gpgHome, 0o700); + } + return gpgHome; +} + +export async function importKey(privateKey: string): Promise { + const gpgHome = createGpgHome(GPG_HOME_PREFIX); + const privateKeyFile = path.join(gpgHome, `private-key-${randomUUID()}.asc`); - let output = ''; + try { + fs.writeFileSync(privateKeyFile, privateKey, { + encoding: 'utf-8', + flag: 'wx', + mode: 0o600 + }); - const options: ExecOptions = { - silent: true, - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } + try { + await exec.exec( + 'gpg', + [ + '--homedir', + toGpgPath(gpgHome), + '--batch', + '--import', + toGpgPath(privateKeyFile) + ], + {silent: true} + ); + } finally { + fs.rmSync(privateKeyFile, {force: true}); } - }; - await exec.exec( - 'gpg', - [ - '--batch', - '--import-options', - 'import-show', - '--import', - PRIVATE_KEY_FILE - ], - options - ); + return gpgHome; + } catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} - await io.rmRF(PRIVATE_KEY_FILE); +export async function removeGpgHome(gpgHome: string): Promise { + if (!gpgHome) { + return; + } - const match = output.match(PRIVATE_KEY_FINGERPRINT_REGEX); - return match && match[0]; -} + const resolvedGpgHome = path.resolve(gpgHome); + const resolvedTempDir = path.resolve(util.getTempDir()); + if ( + path.dirname(resolvedGpgHome) !== resolvedTempDir || + !path.basename(resolvedGpgHome).startsWith(GPG_HOME_PREFIX) + ) { + throw new Error(`Refusing to remove unexpected GPG home: ${gpgHome}`); + } -export async function deleteKey(keyFingerprint: string) { - await exec.exec( - 'gpg', - ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], - { - silent: true - } - ); + if (!fs.existsSync(resolvedGpgHome)) { + return; + } + + try { + await exec.exec( + 'gpgconf', + ['--homedir', toGpgPath(resolvedGpgHome), '--kill', 'gpg-agent'], + {silent: true, ignoreReturnCode: true} + ); + } catch { + // gpgconf may be unavailable, but directory removal must still be attempted. + } + + await io.rmRF(resolvedGpgHome); } export async function verifyPackageSignature( @@ -74,9 +102,7 @@ export async function verifyPackageSignature( const signaturePath = await tc.downloadTool(signatureUrl); let gpgHome: string; try { - gpgHome = fs.mkdtempSync( - path.join(util.getTempDir(), 'verify-signature-gpg-home-') - ); + gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { await io.rmRF(signaturePath);