NO-JIRA Add proxy support for GPG keyserver access

This commit is contained in:
Marius Boden
2026-05-06 16:10:46 +00:00
committed by Julien HENRY
parent c444753899
commit 9ddabeeb80
6 changed files with 269 additions and 4 deletions
+2
View File
@@ -213,6 +213,8 @@ By default, the action verifies the OpenPGP signature of the SonarScanner CLI bi
> [!NOTE]
> Signature verification requires `gpg` and `dirmngr` to be installed on the runner. GitHub-hosted runners include both, but some self-hosted runners or containers may not.
>
> If your runner accesses the internet through a proxy, the action automatically picks up the `HTTPS_PROXY` or `https_proxy` environment variable when fetching the public key from the keyserver. `HTTP_PROXY` is intentionally not used as a fallback, since keyservers are accessed over TLS (`hkps://`).
>
> **Version history:**
> - Introduced in **v7.2** with a default value of `true` to avoid breaking existing workflows on runners without `dirmngr`.
> - Changed to `false` by default in **v8** (breaking change). If your runner does not have `gpg` or `dirmngr` installed, set this option to `true` explicitly.
+18
View File
@@ -3977,6 +3977,18 @@ function setupGpgHome() {
return gpgHome;
}
/**
* Detects HTTPS proxy from environment variables.
* Checks both upper and lower case variants (HTTPS_PROXY, https_proxy).
* Only HTTPS proxy is used since keyservers use hkps:// (TLS).
* HTTP_PROXY is intentionally not used as a fallback to avoid routing
* HTTPS traffic through a proxy not intended for TLS connections.
* @returns {string|undefined} Proxy URL or undefined if not set
*/
function getProxyFromEnv() {
return process.env.HTTPS_PROXY || process.env.https_proxy;
}
/**
* Attempts to import a public key from a specific keyserver
* @param {string} gpgHome - Path to GPG home directory
@@ -3988,6 +4000,11 @@ function setupGpgHome() {
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand();
const gpgHomePath = convertToUnixPath(gpgHome);
const proxyUrl = getProxyFromEnv();
if (proxyUrl) {
info(`Using proxy for keyserver access: ${proxyUrl}`);
}
await execExports.exec(
gpgCommand,
@@ -3997,6 +4014,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
"--batch",
"--keyserver",
keyserver,
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
"--recv-keys",
keyFingerprint,
],
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -20,8 +20,8 @@
import assert from "node:assert/strict";
import * as fs from "node:fs";
import {afterEach, describe, it, mock} from "node:test";
import {setupGpgHome,} from "../gpg-verification.js";
import { afterEach, describe, it, mock } from "node:test";
import { getProxyFromEnv, setupGpgHome, } from "../gpg-verification.js";
/**
* Helper function to create a temporary GPG home directory for testing.
@@ -483,4 +483,231 @@ describe("gpg-verification with mocked exec", () => {
);
});
});
describe("getProxyFromEnv", () => {
const proxyVars = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
function clearProxyEnv() {
for (const v of proxyVars) {
delete process.env[v];
}
}
afterEach(() => {
clearProxyEnv();
});
it("should return undefined when no proxy is set", () => {
clearProxyEnv();
assert.equal(getProxyFromEnv(), undefined);
});
it("should prefer HTTPS_PROXY over https_proxy", () => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://proxy-https:8080";
process.env.https_proxy = "http://proxy-lower:8080";
assert.equal(getProxyFromEnv(), "http://proxy-https:8080");
});
it("should use https_proxy (lowercase)", () => {
clearProxyEnv();
process.env.https_proxy = "http://proxy-lower:8080";
assert.equal(getProxyFromEnv(), "http://proxy-lower:8080");
});
it("should not fall back to HTTP_PROXY", () => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://proxy-http:3128";
assert.equal(getProxyFromEnv(), undefined);
});
it("should not fall back to http_proxy (lowercase)", () => {
clearProxyEnv();
process.env.http_proxy = "http://proxy-lower-http:3128";
assert.equal(getProxyFromEnv(), undefined);
});
it("should ignore HTTP_PROXY when only HTTP proxy is configured", () => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://http-only-proxy:3128";
process.env.http_proxy = "http://http-only-proxy:3128";
assert.equal(getProxyFromEnv(), undefined);
});
});
describe("tryImportKey with proxy", () => {
const proxyVars = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
function clearProxyEnv() {
for (const v of proxyVars) {
delete process.env[v];
}
}
afterEach(() => {
clearProxyEnv();
});
it("should not pass --keyserver-options when no proxy env is set", async (t) => {
clearProxyEnv();
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=no-proxy");
const gpgHome = createTrackedGpgHome(tempDirs);
const keyserver = "hkps://keyserver.ubuntu.com";
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
assert.equal(execCalls.length, 1);
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options");
});
it("should use HTTPS_PROXY when set", async (t) => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://corporate-proxy:8080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-https-upper");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1, "Should include --keyserver-options");
assert.equal(args[optIdx + 1], "http-proxy=http://corporate-proxy:8080");
});
it("should use https_proxy (lowercase) when set", async (t) => {
clearProxyEnv();
process.env.https_proxy = "http://lowercase-proxy:3128";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-https-lower");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1);
assert.equal(args[optIdx + 1], "http-proxy=http://lowercase-proxy:3128");
});
it("should not use proxy when only HTTP_PROXY is set", async (t) => {
clearProxyEnv();
process.env.HTTP_PROXY = "http://http-only-proxy:9090";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-http-upper");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options when only HTTP_PROXY is set");
});
it("should not use proxy when only http_proxy (lowercase) is set", async (t) => {
clearProxyEnv();
process.env.http_proxy = "http://last-resort-proxy:1080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-http-lower");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
assert.ok(!args.includes("--keyserver-options"), "Should NOT include --keyserver-options when only http_proxy is set");
});
it("should prefer HTTPS_PROXY over https_proxy and ignore HTTP variants", async (t) => {
clearProxyEnv();
process.env.HTTPS_PROXY = "http://preferred:8080";
process.env.https_proxy = "http://not-this-one:8080";
process.env.HTTP_PROXY = "http://also-not:3128";
process.env.http_proxy = "http://nope:1080";
const execCalls = [];
const execFn = mock.fn(async (command, args) => {
execCalls.push({ command, args });
return 0;
});
t.mock.module("@actions/exec", {
namedExports: {
exec: execFn,
},
});
const { importSonarSourceKey } = await import("../gpg-verification.js?test=proxy-precedence");
const gpgHome = createTrackedGpgHome(tempDirs);
await importSonarSourceKey(gpgHome, "ABCD1234", "hkps://keyserver.ubuntu.com");
const args = execCalls[0].args;
const optIdx = args.indexOf("--keyserver-options");
assert.ok(optIdx !== -1);
assert.equal(args[optIdx + 1], "http-proxy=http://preferred:8080");
});
});
});
+18
View File
@@ -125,6 +125,18 @@ export function setupGpgHome() {
return gpgHome;
}
/**
* Detects HTTPS proxy from environment variables.
* Checks both upper and lower case variants (HTTPS_PROXY, https_proxy).
* Only HTTPS proxy is used since keyservers use hkps:// (TLS).
* HTTP_PROXY is intentionally not used as a fallback to avoid routing
* HTTPS traffic through a proxy not intended for TLS connections.
* @returns {string|undefined} Proxy URL or undefined if not set
*/
export function getProxyFromEnv() {
return process.env.HTTPS_PROXY || process.env.https_proxy;
}
/**
* Attempts to import a public key from a specific keyserver
* @param {string} gpgHome - Path to GPG home directory
@@ -136,6 +148,11 @@ export function setupGpgHome() {
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
const gpgCommand = getGpgCommand();
const gpgHomePath = convertToUnixPath(gpgHome);
const proxyUrl = getProxyFromEnv();
if (proxyUrl) {
core.info(`Using proxy for keyserver access: ${proxyUrl}`);
}
await exec.exec(
gpgCommand,
@@ -145,6 +162,7 @@ async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
"--batch",
"--keyserver",
keyserver,
...(proxyUrl ? ["--keyserver-options", `http-proxy=${proxyUrl}`] : []),
"--recv-keys",
keyFingerprint,
],