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 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 // (exit code 2), so all paths passed to GPG must be in POSIX format on Windows. export function toGpgPath(p: string): string { if (process.platform !== 'win32') return p; return p .replace(/\\/g, '/') .replace(/^([A-Za-z]):\//, (_, drive) => `/${drive.toLowerCase()}/`); } 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`); 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( archivePath: string, signatureUrl: string, publicKeyContent: string ) { const signaturePath = await tc.downloadTool(signatureUrl); let gpgHome: string; try { gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX); } catch (error) { try { await io.rmRF(signaturePath); } catch { // ignore cleanup failures } throw new Error( `Failed to create temporary GPG home directory for signature verification: ${ (error as Error).message }`, {cause: error} ); } try { const publicKeyFile = path.join(gpgHome, 'public-key.asc'); fs.writeFileSync(publicKeyFile, publicKeyContent, {encoding: 'utf-8'}); const options: ExecOptions = {silent: true}; await exec.exec( 'gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', '--import', toGpgPath(publicKeyFile) ], options ); await exec.exec( 'gpg', [ '--homedir', toGpgPath(gpgHome), '--batch', '--verify', toGpgPath(signaturePath), toGpgPath(archivePath) ], options ); } finally { await io.rmRF(signaturePath); await io.rmRF(gpgHome); } }