diff --git a/README.md b/README.md index 563a21e3..619e62e1 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 c3303aa5..7457eb60 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 82f7b367..63b0df8a 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 0dc0ea53..8e0747ac 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 793c3778..830fdb94 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 23964327..db69e3de 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 - ); + 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') + ); - 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}) - ); - }); + 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 d27c23ac..60c1a00e 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 734dc227..2befb2c6 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()}/`); } -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(); - } - } - }; - 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]; +function createGpgHome(prefix) { + const gpgHome = fs.mkdtempSync(path.join(util.getTempDir(), prefix)); + if (process.platform !== 'win32') { + fs.chmodSync(gpgHome, 0o700); + } + return gpgHome; } -async function deleteKey(keyFingerprint) { - await lib_exec/* exec */.m('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function importKey(privateKey) { + 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 }); + } + 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 64353cbc..e9ba3c91 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()}/`); } -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(); - } - } - }; - 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]; +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 deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function importKey(privateKey) { + 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 }); + } + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} +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 cd19ab24..c0578ee6 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()}/`); } -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(); - } - } - }; - 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]; +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 deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function importKey(privateKey) { + 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 }); + } + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} +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 85410c77..4d98b9c2 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()}/`); } -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(); - } - } - }; - 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]; +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 deleteKey(keyFingerprint) { - await exec.exec('gpg', ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], { - silent: true - }); +async function importKey(privateKey) { + 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 }); + } + finally { + fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(privateKeyFile, { force: true }); + } + return gpgHome; + } + catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} +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 4b7ec051..992717c7 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 ffe700c7..4a6ec897 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 277342c5..49bf9ae3 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 b9fae6b2..cf333d00 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 6e19a587..83a2aaf0 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 6eb78721..46e0a6ce 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' - }); - - let output = ''; - - const options: ExecOptions = { - silent: true, - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - }; - - 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]; +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 deleteKey(keyFingerprint: string) { - await exec.exec( - 'gpg', - ['--batch', '--yes', '--delete-secret-and-public-key', keyFingerprint], - { - silent: true +export async function importKey(privateKey: string): Promise { + 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} + ); + } finally { + fs.rmSync(privateKeyFile, {force: true}); } - ); + + return gpgHome; + } catch (error) { + await removeGpgHome(gpgHome); + throw error; + } +} + +export async function removeGpgHome(gpgHome: string): Promise { + if (!gpgHome) { + return; + } + + 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}`); + } + + 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);