SQSCANGHA-140 Fix GPG path handling for Windows

Fix GPG signature verification on Windows by converting Windows-style
paths to Unix-style paths that GPG expects. GPG on Windows (from Git
for Windows) is a Unix tool that requires Unix-style paths.

Issue: GPG was receiving Windows paths like C:\a\_temp\gpg-home and
attempting to use them, resulting in malformed paths like
/c/a/sonarqube-scan-action/C:\a\_temp\gpg-home and errors:
- "keyblock resource: No such file or directory"
- "can't connect to the dirmngr: No such file or directory"
- "keyserver receive failed: No dirmngr"

Solution:
- Add convertToUnixPath() function to convert Windows paths to Unix-style
  (e.g., C:\a\_temp\gpg -> /c/a/_temp/gpg)
- Apply conversion in tryImportKey() and runGpgVerify() before calling GPG
- No-op on non-Windows platforms
- Add comprehensive tests for path conversion

This ensures GPG signature verification works on all platforms (Linux,
macOS, and Windows).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claire Villard 2026-04-27 19:06:35 +02:00
parent 79d962c4f8
commit 7a11667fa2
4 changed files with 161 additions and 19 deletions

49
dist/index.js vendored
View file

@ -12,6 +12,9 @@ import { ok } from 'assert';
import 'string_decoder';
import * as events from 'events';
import { setTimeout as setTimeout$1 } from 'timers';
import * as fs$1 from 'node:fs';
import * as os$1 from 'node:os';
import * as path$1 from 'node:path';
import 'http';
import 'https';
import 'net';
@ -3948,15 +3951,37 @@ function getGpgCommand() {
return "gpg";
}
/**
* Converts a Windows path to Unix-style path for GPG
* GPG on Windows (from Git for Windows) expects Unix-style paths
* @param {string} windowsPath - Windows path (e.g., C:\a\_temp\gpg-home)
* @returns {string} Unix-style path (e.g., /c/a/_temp/gpg-home)
*/
function convertToUnixPath(windowsPath) {
if (process.platform !== "win32") {
return windowsPath;
}
// Convert backslashes to forward slashes
let unixPath = windowsPath.replace(/\\/g, "/");
// Convert drive letter (e.g., C: -> /c)
unixPath = unixPath.replace(/^([A-Za-z]):/, (match, drive) => {
return `/${drive.toLowerCase()}`;
});
return unixPath;
}
/**
* Creates a temporary GPG home directory
* @returns {string} Path to the temporary GPG home directory
*/
function setupGpgHome() {
const tempDir = process.env.RUNNER_TEMP || os.tmpdir();
const gpgHome = path.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
const tempDir = process.env.RUNNER_TEMP || os$1.tmpdir();
const gpgHome = path$1.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
fs$1.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
return gpgHome;
}
@ -3971,12 +3996,14 @@ function setupGpgHome() {
*/
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand();
// Convert Windows paths to Unix-style for GPG compatibility
const gpgHomePath = convertToUnixPath(gpgHome);
await execExports.exec(
gpgCommand,
[
"--homedir",
gpgHome,
gpgHomePath,
"--batch",
"--keyserver",
keyserver,
@ -4041,9 +4068,17 @@ async function runGpgVerify(zipPath, signaturePath, gpgHome) {
try {
info("Verifying GPG signature...");
// Convert Windows paths to Unix-style for GPG compatibility
await execExports.exec(
gpgCommand,
["--homedir", gpgHome, "--batch", "--verify", signaturePath, zipPath],
[
"--homedir",
convertToUnixPath(gpgHome),
"--batch",
"--verify",
convertToUnixPath(signaturePath),
convertToUnixPath(zipPath),
],
{
silent: false,
}
@ -4061,8 +4096,8 @@ async function runGpgVerify(zipPath, signaturePath, gpgHome) {
*/
function cleanupGpgHome(gpgHome) {
try {
if (fs.existsSync(gpgHome)) {
fs.rmSync(gpgHome, { recursive: true, force: true });
if (fs$1.existsSync(gpgHome)) {
fs$1.rmSync(gpgHome, { recursive: true, force: true });
debug(`Cleaned up temporary GPG home: ${gpgHome}`);
}
} catch (error) {

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -18,16 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { describe, it, afterEach, beforeEach } from "node:test";
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
getGpgCommand,
setupGpgHome,
cleanupGpgHome,
importSonarSourceKey,
convertToUnixPath,
} from "../gpg-verification.js";
describe("gpg-verification", () => {
@ -54,6 +55,80 @@ describe("gpg-verification", () => {
});
});
describe("convertToUnixPath", () => {
it("should convert Windows path with drive letter to Unix path", () => {
// Mock Windows platform
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "win32",
writable: true,
configurable: true,
});
try {
assert.equal(
convertToUnixPath("C:\\a\\_temp\\gpg-home"),
"/c/a/_temp/gpg-home"
);
assert.equal(
convertToUnixPath("D:\\Users\\test\\file.txt"),
"/d/Users/test/file.txt"
);
} finally {
// Restore original platform
Object.defineProperty(process, "platform", {
value: originalPlatform,
writable: true,
configurable: true,
});
}
});
it("should handle mixed slashes on Windows", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "win32",
writable: true,
configurable: true,
});
try {
assert.equal(
convertToUnixPath("C:\\a/_temp\\gpg-home"),
"/c/a/_temp/gpg-home"
);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
writable: true,
configurable: true,
});
}
});
it("should return path unchanged on non-Windows platforms", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "linux",
writable: true,
configurable: true,
});
try {
assert.equal(
convertToUnixPath("/tmp/gpg-home"),
"/tmp/gpg-home"
);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
writable: true,
configurable: true,
});
}
});
});
describe("setupGpgHome", () => {
it("should create a temporary GPG home directory", () => {
const gpgHome = setupGpgHome();
@ -65,8 +140,8 @@ describe("gpg-verification", () => {
// Check directory permissions (on Unix systems)
if (process.platform !== "win32") {
const stats = fs.statSync(gpgHome);
const mode = stats.mode & parseInt("777", 8);
assert.equal(mode, parseInt("700", 8));
const mode = stats.mode & Number.parseInt("777", 8);
assert.equal(mode, Number.parseInt("700", 8));
}
});

View file

@ -20,9 +20,9 @@
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
// SonarSource public key fingerprint for verifying scanner signatures
const SONARSOURCE_KEY_FINGERPRINT = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
@ -99,6 +99,28 @@ export function getGpgCommand() {
return "gpg";
}
/**
* Converts a Windows path to Unix-style path for GPG
* GPG on Windows (from Git for Windows) expects Unix-style paths
* @param {string} windowsPath - Windows path (e.g., C:\a\_temp\gpg-home)
* @returns {string} Unix-style path (e.g., /c/a/_temp/gpg-home)
*/
export function convertToUnixPath(windowsPath) {
if (process.platform !== "win32") {
return windowsPath;
}
// Convert backslashes to forward slashes
let unixPath = windowsPath.replace(/\\/g, "/");
// Convert drive letter (e.g., C: -> /c)
unixPath = unixPath.replace(/^([A-Za-z]):/, (match, drive) => {
return `/${drive.toLowerCase()}`;
});
return unixPath;
}
/**
* Creates a temporary GPG home directory
* @returns {string} Path to the temporary GPG home directory
@ -122,12 +144,14 @@ export function setupGpgHome() {
*/
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand();
// Convert Windows paths to Unix-style for GPG compatibility
const gpgHomePath = convertToUnixPath(gpgHome);
await exec.exec(
gpgCommand,
[
"--homedir",
gpgHome,
gpgHomePath,
"--batch",
"--keyserver",
keyserver,
@ -192,9 +216,17 @@ export async function runGpgVerify(zipPath, signaturePath, gpgHome) {
try {
core.info("Verifying GPG signature...");
// Convert Windows paths to Unix-style for GPG compatibility
await exec.exec(
gpgCommand,
["--homedir", gpgHome, "--batch", "--verify", signaturePath, zipPath],
[
"--homedir",
convertToUnixPath(gpgHome),
"--batch",
"--verify",
convertToUnixPath(signaturePath),
convertToUnixPath(zipPath),
],
{
silent: false,
}