Files
setup-java/src/gpg.ts
T
Copilotbrunoborgescopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
a78ec39f12 Stop GPG agent before removing signature-verification home (#1273)
* Initial plan

* Stop GPG agent before verification home cleanup

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
2026-09-28 21:26:40 -04:00

171 lines
4.6 KiB
TypeScript

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 type {ExecOptions} from '@actions/exec';
import type {SignatureVerificationKey} from './distributions/base-models.js';
export const GPG_HOME_PREFIX = 'setup-java-gpg-';
const VERIFY_GPG_HOME_PREFIX = 'verify-signature-gpg-home-';
export async function isGpgAvailable(): Promise<boolean> {
return Boolean(await io.which('gpg', false));
}
// 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,
tempDir: string = util.getTempDir()
): string {
const gpgHome = fs.mkdtempSync(path.join(tempDir, prefix));
if (process.platform !== 'win32') {
fs.chmodSync(gpgHome, 0o700);
}
return gpgHome;
}
export async function importKey(privateKey: string): Promise<string> {
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<void> {
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;
}
await stopGpgAgent(resolvedGpgHome);
await io.rmRF(resolvedGpgHome);
}
async function stopGpgAgent(gpgHome: string): Promise<void> {
try {
await exec.exec(
'gpgconf',
['--homedir', toGpgPath(gpgHome), '--kill', 'gpg-agent'],
{silent: true, ignoreReturnCode: true}
);
} catch {
// gpgconf may be unavailable, but directory removal must still be attempted.
}
}
export async function verifyPackageSignature(
archivePath: string,
signatureUrl: string,
publicKeyContent: SignatureVerificationKey
) {
const signaturePath = await tc.downloadTool(signatureUrl);
let gpgHome: string;
try {
// Both RUNNER_TEMP and TMPDIR can exceed macOS's 104-byte agent socket limit.
const tempDir = process.platform === 'darwin' ? '/tmp' : util.getTempDir();
gpgHome = createGpgHome(VERIFY_GPG_HOME_PREFIX, tempDir);
} 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 publicKeys = Array.isArray(publicKeyContent)
? publicKeyContent
: [publicKeyContent];
const publicKeyFiles = publicKeys.map((publicKey, index) => {
const publicKeyFile = path.join(gpgHome, `public-key-${index}.asc`);
fs.writeFileSync(publicKeyFile, publicKey, {encoding: 'utf-8'});
return toGpgPath(publicKeyFile);
});
const options: ExecOptions = {silent: true};
await exec.exec(
'gpg',
[
'--homedir',
toGpgPath(gpgHome),
'--batch',
'--import',
...publicKeyFiles
],
options
);
await exec.exec(
'gpg',
[
'--homedir',
toGpgPath(gpgHome),
'--batch',
'--verify',
toGpgPath(signaturePath),
toGpgPath(archivePath)
],
options
);
} finally {
await stopGpgAgent(gpgHome);
await io.rmRF(signaturePath);
await io.rmRF(gpgHome);
}
}