mirror of
https://github.com/LamGC/quickly-conf-sshd.git
synced 2026-10-13 07:43:57 +00:00
test: exercise real WKD and HKPS discovery in isolated CI
This commit is contained in:
@@ -44,3 +44,27 @@ jobs:
|
|||||||
path: /tmp/quickly-browser-artifacts
|
path: /tmp/quickly-browser-artifacts
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
live-discovery:
|
||||||
|
name: Live public GPG discovery (external network)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 6
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- name: Ensure actual GnuPG network dependencies
|
||||||
|
run: |
|
||||||
|
if ! command -v gpg >/dev/null || ! command -v dirmngr >/dev/null; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y gnupg dirmngr
|
||||||
|
fi
|
||||||
|
gpg --version
|
||||||
|
dirmngr --version
|
||||||
|
- name: Discover the fixed public test certificate through WKD and HKPS
|
||||||
|
# This fixture is ONLY a public integration-test identity. Fork owners
|
||||||
|
# must not replace it with their deployment identity or production keys.
|
||||||
|
# No installer, system trust changes, signing, or SSH configuration runs.
|
||||||
|
run: node scripts/test-live-discovery.mjs
|
||||||
|
|||||||
@@ -264,6 +264,22 @@ npx wrangler deploy --dry-run --config cf-worker/wrangler.toml
|
|||||||
|
|
||||||
测试只使用临时 HOME、命令替身、公开测试 SSH 钥匙及临时无身份/无访问权限的 GPG 测试钥匙。真实浏览器测试仅访问 localhost。`build:test` 产物标记为测试,生产验证拒绝它;上线必须重新生产构建。
|
测试只使用临时 HOME、命令替身、公开测试 SSH 钥匙及临时无身份/无访问权限的 GPG 测试钥匙。真实浏览器测试仅访问 localhost。`build:test` 产物标记为测试,生产验证拒绝它;上线必须重新生产构建。
|
||||||
|
|
||||||
|
### 真实 WKD / HKPS 集成测试
|
||||||
|
|
||||||
|
CI 的 `Live public GPG discovery (external network)` 是独立任务,与 `npm test` 中不访问外部密钥服务的核心测试分开。它使用真实 GnuPG/dirmngr,在全新的临时钥匙环中通过 WKD、keys.openpgp.org 和 keyserver.ubuntu.com 发现公共证书,再用实际客户端的证书检查函数验证完整主指纹和指定签名子钥;不执行安装器、不改系统信任、不安装 SSH 服务。
|
||||||
|
|
||||||
|
`tests/fixtures/live-discovery.json` 是**仅用于测试的固定公开身份**。Fork 或部署本项目时不应把它改成自己的邮箱/指纹;生产配置只修改 `deployment.json` 与 `keys/`。测试身份不作为服务器 SSH 授权密钥,也不会写入生成的安装默认值。只有维护集成测试本身(例如测试证书到期)时才需要更换此夹具。
|
||||||
|
|
||||||
|
每个来源有独立的结果、有限超时与重试;网络/服务不可用和证书验证失败会明确报告并使该任务失败,不会静默跳过后标为通过。CI 摘要只保留来源、固定指纹与结果,不上传证书或其他 UID 信息。没有该身份签署的 SSH 公钥文档,因此此测试不声称完成了用户真实业务文档验签;签名验签和错误场景仍由隔离的临时测试钥匙覆盖。
|
||||||
|
|
||||||
|
可显式运行同一外网测试:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/test-live-discovery.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
运行需要允许 dirmngr 创建本地 socket 并访问这些 HTTPS/HKPS 服务。受限环境中仅 HTTP 下载成功不代表 GPG 发现流程通过。
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0,见 [LICENSE](LICENSE)。
|
Apache-2.0,见 [LICENSE](LICENSE)。
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
// Explicit external integration test. npm test imports this module but never
|
||||||
|
// invokes its network entry point. The public identity is a test-only fixture.
|
||||||
|
import {spawnSync} from 'node:child_process';
|
||||||
|
import {appendFileSync, chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs';
|
||||||
|
import {tmpdir} from 'node:os';
|
||||||
|
import {dirname, join, resolve} from 'node:path';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
|
||||||
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const MAX_CERTIFICATE = 1024 * 1024;
|
||||||
|
const ATTEMPT_MS = 15_000;
|
||||||
|
// Deadline for starting/continuing network lookups. Each local verifier and
|
||||||
|
// private-agent cleanup also has its own timeout; CI supplies the hard job cap.
|
||||||
|
const NETWORK_BUDGET_MS = 120_000;
|
||||||
|
const MAX_ATTEMPTS = 2;
|
||||||
|
const fingerprint = value => typeof value === 'string' && /^[A-F0-9]{40}$/.test(value);
|
||||||
|
|
||||||
|
export function validateFixture(input) {
|
||||||
|
if (!input || typeof input !== 'object' || !input.testOnlyNotice?.includes('integration-test identity only') ||
|
||||||
|
typeof input.email !== 'string' || input.email.length > 254 || !/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$/.test(input.email) ||
|
||||||
|
!fingerprint(input.primaryFingerprint) || !fingerprint(input.signingSubkeyFingerprint) ||
|
||||||
|
input.primaryFingerprint === input.signingSubkeyFingerprint || !Array.isArray(input.keyservers) ||
|
||||||
|
input.keyservers.length < 1 || input.keyservers.length > 4 ||
|
||||||
|
input.keyservers.some(value => typeof value !== 'string' || !/^hkps:\/\/[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?$/.test(value)) ||
|
||||||
|
new Set(input.keyservers).size !== input.keyservers.length) throw Error('Invalid public integration-test fixture.');
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function classifyFailure(result) {
|
||||||
|
if (result.error?.code === 'ETIMEDOUT' || result.signal === 'SIGKILL') return 'timeout';
|
||||||
|
if (result.error?.code === 'ENOENT') return 'missing-tool';
|
||||||
|
if (result.error) return 'process-error';
|
||||||
|
const diagnostic = String(result.stderr ?? '').toLowerCase();
|
||||||
|
if (/no dirmngr|can.?t create socket|failed to start dirmngr|operation not permitted/.test(diagnostic)) return 'local-agent-unavailable';
|
||||||
|
if (/timed? out|timeout/.test(diagnostic)) return 'timeout';
|
||||||
|
if (/certificate.*(failed|invalid)|tls.*(failed|error)|not trusted/.test(diagnostic)) return 'transport-security-error';
|
||||||
|
if (/network is unreachable|no route to host|could not resolve|name or service not known|network error|connection refused/.test(diagnostic)) return 'network-unavailable';
|
||||||
|
if (/no data|not found|no public key/.test(diagnostic)) return 'key-not-found';
|
||||||
|
return 'gpg-error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function execute(command, args, {home, timeout = ATTEMPT_MS} = {}) {
|
||||||
|
return spawnSync(command, args, {
|
||||||
|
encoding: 'utf8', timeout, killSignal: 'SIGKILL', maxBuffer: MAX_CERTIFICATE + 64 * 1024,
|
||||||
|
env: {...process.env, LC_ALL: 'C', ...(home ? {HOME: home, GNUPGHOME: home} : {})},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract just the production verifier functions. Never source or run the
|
||||||
|
// installer, its argument parser, dependencies installer, or SSH configuration.
|
||||||
|
export function verifierHarness(clientSource) {
|
||||||
|
const names = ['new_gpg_home', 'gpg_local', 'import_pinned_key'];
|
||||||
|
const functions = names.map(name => {
|
||||||
|
const matches = [...clientSource.matchAll(new RegExp(`^${name}\\(\\) \\{\\n[\\s\\S]*?^\\}\\n`, 'gm'))];
|
||||||
|
if (matches.length !== 1) throw Error(`Cannot extract client verifier function: ${name}.`);
|
||||||
|
return matches[0][0];
|
||||||
|
}).join('\n');
|
||||||
|
return `#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
umask 077
|
||||||
|
LC_ALL=C
|
||||||
|
export LC_ALL
|
||||||
|
state=$1
|
||||||
|
certificate=$2
|
||||||
|
pinned_fingerprint=$3
|
||||||
|
pin_mode=$4
|
||||||
|
gpg_home=
|
||||||
|
temps=
|
||||||
|
fail() { printf 'client-certificate-rejected\\n' >&2; exit 1; }
|
||||||
|
need() { command -v "$1" >/dev/null 2>&1 || fail; }
|
||||||
|
new_temp() { TEMP=$(mktemp "$1/verify.XXXXXX"); }
|
||||||
|
${functions}
|
||||||
|
import_pinned_key "$certificate"
|
||||||
|
printf '%s\\n' "$certificate_primary"
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateCertificate(certificate, config, {
|
||||||
|
clientSource = readFileSync(join(repositoryRoot, 'conf-sshd.sh'), 'utf8'), runner = execute,
|
||||||
|
} = {}) {
|
||||||
|
if (typeof certificate !== 'string' || !certificate.length || Buffer.byteLength(certificate) > MAX_CERTIFICATE) return {ok: false, status: 'invalid-certificate-size'};
|
||||||
|
const home = mkdtempSync(join(tmpdir(), 'conf-sshd-certificate-check-'));
|
||||||
|
chmodSync(home, 0o700);
|
||||||
|
try {
|
||||||
|
const keyFile = join(home, 'public.asc'), harness = join(home, 'verify.sh');
|
||||||
|
writeFileSync(keyFile, certificate, {mode: 0o600});
|
||||||
|
writeFileSync(harness, verifierHarness(clientSource), {mode: 0o600});
|
||||||
|
for (const mode of ['primary', 'subkey']) {
|
||||||
|
// One clean home per pin mode, even though the function itself isolates its ring.
|
||||||
|
const state = mkdtempSync(join(home, `${mode}-`));
|
||||||
|
const pin = mode === 'primary' ? config.primaryFingerprint : config.signingSubkeyFingerprint;
|
||||||
|
const result = runner('/bin/sh', [harness, state, keyFile, pin, mode], {home: state, timeout: ATTEMPT_MS});
|
||||||
|
if (result.status !== 0 || result.error || result.stdout?.trim() !== config.primaryFingerprint) {
|
||||||
|
return {ok: false, status: result.error ? classifyFailure(result) : 'certificate-pin-or-capability-rejected', mode};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {ok: true, status: 'validated-primary-and-subkey'};
|
||||||
|
} finally {
|
||||||
|
// The extracted verifier uses --no-autostart and a public-only keyring.
|
||||||
|
rmSync(home, {recursive: true, force: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function discoveryProbes(config) {
|
||||||
|
return [
|
||||||
|
{source: 'wkd', queryMode: 'email', args: ['--auto-key-locate', 'clear,wkd', '--locate-external-keys', '--', config.email]},
|
||||||
|
...config.keyservers.map(server => ({
|
||||||
|
source: server, queryMode: 'primary',
|
||||||
|
args: ['--auto-key-locate', 'clear', '--keyserver', server, '--recv-keys', config.primaryFingerprint],
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runDiscovery(config, {runner = execute, validator = validateCertificate, now = Date.now} = {}) {
|
||||||
|
validateFixture(config);
|
||||||
|
const start = now(), deadline = start + NETWORK_BUDGET_MS, results = [];
|
||||||
|
for (const probe of discoveryProbes(config)) {
|
||||||
|
let status = 'budget-exhausted', attempts = 0;
|
||||||
|
for (; attempts < MAX_ATTEMPTS;) {
|
||||||
|
const remaining = deadline - now();
|
||||||
|
if (remaining <= 0) { status = 'budget-exhausted'; break; }
|
||||||
|
attempts++;
|
||||||
|
const home = mkdtempSync(join(tmpdir(), 'conf-sshd-live-discovery-'));
|
||||||
|
chmodSync(home, 0o700);
|
||||||
|
try {
|
||||||
|
const base = ['--batch', '--no-options', '--homedir', home, '--no-tty', '--no-auto-key-retrieve'];
|
||||||
|
const fetched = runner('gpg', [...base, '--status-fd', '1', ...probe.args], {home, timeout: Math.min(ATTEMPT_MS, remaining)});
|
||||||
|
if (fetched.status !== 0 || fetched.error) {
|
||||||
|
status = classifyFailure(fetched);
|
||||||
|
} else {
|
||||||
|
const exportRemaining = deadline - now();
|
||||||
|
if (exportRemaining <= 0) { status = 'budget-exhausted'; break; }
|
||||||
|
const exported = runner('gpg', [...base, '--no-autostart', '--auto-key-locate', 'clear', '--armor', '--export'], {home, timeout: Math.min(ATTEMPT_MS, exportRemaining)});
|
||||||
|
if (exported.status !== 0 || exported.error) status = `export-${classifyFailure(exported)}`;
|
||||||
|
else {
|
||||||
|
const validation = validator(exported.stdout, config);
|
||||||
|
status = validation.ok ? 'passed' : validation.status;
|
||||||
|
// A fetched but invalid certificate is a security failure, not a reason
|
||||||
|
// to hide the result by retrying until the server returns another key.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Only services associated with this newly created private home are stopped.
|
||||||
|
runner('gpgconf', ['--homedir', home, '--kill', 'all'], {home, timeout: 5000});
|
||||||
|
rmSync(home, {recursive: true, force: true});
|
||||||
|
}
|
||||||
|
if (!['timeout', 'network-unavailable', 'key-not-found', 'gpg-error'].includes(status)) break;
|
||||||
|
}
|
||||||
|
results.push({source: probe.source, queryMode: probe.queryMode, status, attempts,
|
||||||
|
primaryFingerprint: config.primaryFingerprint, signingSubkeyFingerprint: config.signingSubkeyFingerprint});
|
||||||
|
}
|
||||||
|
return {schema: 'conf-sshd-live-discovery-v1', status: results.every(result => result.status === 'passed') ? 'passed' : 'failed', results};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summaryMarkdown(report) {
|
||||||
|
return ['## Public GPG discovery integration test', '',
|
||||||
|
`Overall: ${report.status}. Every source must pass; infrastructure failures are not skips.`, '',
|
||||||
|
...report.results.map(result => `- ${result.source} (${result.queryMode}): ${result.status}; attempts ${result.attempts}; primary ${result.primaryFingerprint ?? 'unavailable'}; signing subkey ${result.signingSubkeyFingerprint ?? 'unavailable'}`), '',
|
||||||
|
'This tests public certificate discovery and the client pin/capability checks. No user-signed key-list payload is tested, and no SSH or persistent user trust settings are changed.', ''].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
let report;
|
||||||
|
try {
|
||||||
|
const config = validateFixture(JSON.parse(readFileSync(join(repositoryRoot, 'tests/fixtures/live-discovery.json'), 'utf8')));
|
||||||
|
const missing = ['gpg', 'dirmngr', 'gpgconf'].find(command => execute(command, ['--version'], {timeout: 5000}).status !== 0);
|
||||||
|
report = missing ? {schema: 'conf-sshd-live-discovery-v1', status: 'failed', results: [{source: 'local-tools', queryMode: 'none', status: 'missing-tool', attempts: 0,
|
||||||
|
primaryFingerprint: config.primaryFingerprint, signingSubkeyFingerprint: config.signingSubkeyFingerprint}]} : runDiscovery(config);
|
||||||
|
} catch {
|
||||||
|
report = {schema: 'conf-sshd-live-discovery-v1', status: 'failed', results: [{source: 'local-harness', queryMode: 'none', status: 'fixture-or-harness-error', attempts: 0}]};
|
||||||
|
}
|
||||||
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryMarkdown(report));
|
||||||
|
return report.status === 'passed' ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main();
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"testOnlyNotice": "Public integration-test identity only. Not production authorized keys or deployment trust. Do not change this fixture for a fork deployment.",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"primaryFingerprint": "7EF5FB1E65C1A6C6D02DFCDFCE57746D7E5EE6EF",
|
||||||
|
"signingSubkeyFingerprint": "B1B89449E5877614EA672F5F6C5AE2A913941E1D",
|
||||||
|
"keyservers": ["hkps://keys.openpgp.org", "hkps://keyserver.ubuntu.com"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Hermetic tests only. The network CLI is deliberately not invoked here.
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {existsSync, readFileSync} from 'node:fs';
|
||||||
|
import {spawnSync} from 'node:child_process';
|
||||||
|
import {openPgpFixture} from './helpers/openpgp-fixture.mjs';
|
||||||
|
import {classifyFailure, discoveryProbes, runDiscovery, summaryMarkdown, validateCertificate, validateFixture, verifierHarness} from '../scripts/test-live-discovery.mjs';
|
||||||
|
|
||||||
|
const fixture = JSON.parse(readFileSync(new URL('./fixtures/live-discovery.json', import.meta.url), 'utf8'));
|
||||||
|
const clientSource = readFileSync(new URL('../conf-sshd.sh', import.meta.url), 'utf8');
|
||||||
|
const gpgAvailable = spawnSync('gpg', ['--version']).status === 0;
|
||||||
|
|
||||||
|
test('fixed public discovery identity is explicitly test-only and full-pinned', () => {
|
||||||
|
assert.equal(validateFixture(fixture), fixture);
|
||||||
|
assert.equal(fixture.email, '[email protected]');
|
||||||
|
assert.equal(fixture.primaryFingerprint, '7EF5FB1E65C1A6C6D02DFCDFCE57746D7E5EE6EF');
|
||||||
|
assert.equal(fixture.signingSubkeyFingerprint, 'B1B89449E5877614EA672F5F6C5AE2A913941E1D');
|
||||||
|
for (const change of [{primaryFingerprint: 'CE57746D7E5EE6EF'}, {testOnlyNotice: ''}, {email: 'bad\naddress'}, {keyservers: ['http://bad.example']}, {signingSubkeyFingerprint: fixture.primaryFingerprint}]) {
|
||||||
|
assert.throws(() => validateFixture({...fixture, ...change}), /Invalid/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('discovery probes cover WKD and each HKPS independently using the full primary pin', () => {
|
||||||
|
const probes = discoveryProbes(fixture);
|
||||||
|
assert.equal(probes.length, 3);
|
||||||
|
assert.deepEqual(probes[0].args.slice(-3), ['--locate-external-keys', '--', fixture.email]);
|
||||||
|
for (const probe of probes.slice(1)) {
|
||||||
|
assert.deepEqual(probe.args.slice(-2), ['--recv-keys', fixture.primaryFingerprint]);
|
||||||
|
assert.equal(probe.queryMode, 'primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('network and local failures are classified without exposing diagnostic text', () => {
|
||||||
|
for (const [result, expected] of [
|
||||||
|
[{error: {code: 'ETIMEDOUT'}}, 'timeout'], [{signal: 'SIGKILL'}, 'timeout'],
|
||||||
|
[{error: {code: 'ENOENT'}}, 'missing-tool'], [{error: {code: 'EACCES'}}, 'process-error'],
|
||||||
|
[{stderr: 'gpg: Connection timed out'}, 'timeout'], [{stderr: 'Network is unreachable'}, 'network-unavailable'],
|
||||||
|
[{stderr: 'gpg: error: No data'}, 'key-not-found'], [{stderr: 'TLS certificate verification failed'}, 'transport-security-error'],
|
||||||
|
[{stderr: 'No dirmngr'}, 'local-agent-unavailable'], [{stderr: "can't create socket"}, 'local-agent-unavailable'],
|
||||||
|
[{stderr: 'failed to start dirmngr'}, 'local-agent-unavailable'], [{stderr: 'Operation not permitted'}, 'local-agent-unavailable'],
|
||||||
|
[{stderr: 'Private unrelated UID value'}, 'gpg-error'],
|
||||||
|
]) assert.equal(classifyFailure(result), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unavailable network is a failing report, retries are bounded, and every attempt uses a fresh home', () => {
|
||||||
|
const homes = [], calls = [];
|
||||||
|
const report = runDiscovery(fixture, {
|
||||||
|
runner(command, args, options) {
|
||||||
|
calls.push({command, args, options});
|
||||||
|
if (command === 'gpgconf') return {status: 0};
|
||||||
|
homes.push(options.home);
|
||||||
|
assert.ok(options.timeout <= 15_000);
|
||||||
|
assert.ok(args.includes('--no-options'));
|
||||||
|
assert.ok(args.includes('--no-auto-key-retrieve'));
|
||||||
|
return {status: 2, stderr: 'Network is unreachable. Unrelated UID must never appear.'};
|
||||||
|
},
|
||||||
|
validator() { assert.fail('No certificate should be validated after a failed lookup.'); },
|
||||||
|
});
|
||||||
|
assert.equal(report.status, 'failed');
|
||||||
|
assert.equal(report.results.length, 3);
|
||||||
|
assert.ok(report.results.every(result => result.status === 'network-unavailable' && result.attempts === 2));
|
||||||
|
assert.equal(new Set(homes).size, 6);
|
||||||
|
assert.ok(homes.every(home => !existsSync(home)));
|
||||||
|
assert.equal(calls.filter(call => call.command === 'gpgconf').length, 6);
|
||||||
|
assert.doesNotMatch(JSON.stringify(report) + summaryMarkdown(report), /Unrelated UID/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a missing tool and exhausted budget fail rather than becoming skipped passes', () => {
|
||||||
|
const missing = runDiscovery(fixture, {runner: () => ({status: null, error: {code: 'ENOENT'}})});
|
||||||
|
assert.equal(missing.status, 'failed');
|
||||||
|
assert.ok(missing.results.every(result => result.status === 'missing-tool' && result.attempts === 1));
|
||||||
|
const localAgent = runDiscovery(fixture, {runner: () => ({status: 2, stderr: 'gpg: failed to start dirmngr'})});
|
||||||
|
assert.equal(localAgent.status, 'failed');
|
||||||
|
assert.ok(localAgent.results.every(result => result.status === 'local-agent-unavailable' && result.attempts === 1));
|
||||||
|
let ticks = 0;
|
||||||
|
const budget = runDiscovery(fixture, {now: () => ticks++ * 120_001, runner: () => assert.fail('Budget-exhausted test must not invoke tools.')});
|
||||||
|
assert.equal(budget.status, 'failed');
|
||||||
|
assert.ok(budget.results.every(result => result.status === 'budget-exhausted' && result.attempts === 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetched invalid certificates cannot be retried into apparent success', () => {
|
||||||
|
let validations = 0;
|
||||||
|
const report = runDiscovery(fixture, {
|
||||||
|
runner(command, args) {return {status: 0, stdout: args.includes('--export') ? 'untrusted certificate bytes' : ''};},
|
||||||
|
validator() { validations++; return {ok: false, status: 'certificate-pin-or-capability-rejected'}; },
|
||||||
|
});
|
||||||
|
assert.equal(report.status, 'failed');
|
||||||
|
assert.equal(validations, 3);
|
||||||
|
assert.ok(report.results.every(result => result.attempts === 1 && result.status === 'certificate-pin-or-capability-rejected'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('successful discovery validates each exported certificate and reports no raw bytes', () => {
|
||||||
|
const certificates = [];
|
||||||
|
const report = runDiscovery(fixture, {
|
||||||
|
runner(command, args) {return {status: 0, stdout: args.includes('--export') ? 'public certificate fixture bytes' : ''};},
|
||||||
|
validator(certificate, config) {certificates.push(certificate);assert.equal(config, fixture);return {ok: true};},
|
||||||
|
});
|
||||||
|
assert.equal(report.status, 'passed');
|
||||||
|
assert.equal(certificates.length, 3);
|
||||||
|
assert.ok(report.results.every(result => result.status === 'passed'));
|
||||||
|
assert.doesNotMatch(JSON.stringify(report), /public certificate fixture bytes/);
|
||||||
|
assert.match(summaryMarkdown(report), /No user-signed key-list payload is tested/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifier harness extracts production certificate functions but never executes installer dispatch', () => {
|
||||||
|
const harness = verifierHarness(clientSource);
|
||||||
|
for (const name of ['new_gpg_home', 'gpg_local', 'import_pinned_key']) assert.ok(harness.includes(`${name}() {`));
|
||||||
|
for (const forbidden of ['install_dependency()', 'prepare_sshd_config()', 'case $mode in', 'load_local_config()']) assert.ok(!harness.includes(forbidden));
|
||||||
|
assert.throws(() => verifierHarness('not an installer'), /Cannot extract/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('actual client verifier accepts real local primary and signing-subkey pins and rejects unsafe certificates', {skip: !gpgAvailable}, () => {
|
||||||
|
const key = openPgpFixture({subkeys: 2});
|
||||||
|
const config = {...fixture, primaryFingerprint: key.primaryFingerprint, signingSubkeyFingerprint: key.signerFingerprint};
|
||||||
|
assert.equal(validateCertificate(key.publicKey, config).ok, true);
|
||||||
|
assert.equal(validateCertificate(key.publicKey, {...config, signingSubkeyFingerprint: key.signerFingerprints[1]}).ok, true);
|
||||||
|
assert.equal(validateCertificate(key.publicKey, {...config, primaryFingerprint: 'A'.repeat(40)}).ok, false);
|
||||||
|
assert.equal(validateCertificate(key.publicKey, {...config, signingSubkeyFingerprint: 'B'.repeat(40)}).ok, false);
|
||||||
|
for (const options of [{expired: true}, {revoked: true}]) {
|
||||||
|
const invalid = openPgpFixture({subkeys: 1, ...options});
|
||||||
|
assert.equal(validateCertificate(invalid.publicKey, {...fixture, primaryFingerprint: invalid.primaryFingerprint, signingSubkeyFingerprint: invalid.signerFingerprint}).ok, false);
|
||||||
|
}
|
||||||
|
const other = openPgpFixture({subkeys: 1});
|
||||||
|
assert.equal(validateCertificate(key.publicKey + other.publicKey, config).ok, false);
|
||||||
|
assert.equal(validateCertificate('', config).ok, false);
|
||||||
|
assert.equal(validateCertificate('x'.repeat(1024 * 1024 + 1), config).ok, false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user