mirror of
https://github.com/LamGC/quickly-conf-sshd.git
synced 2026-10-13 07:43:57 +00:00
feat: restore quick usage and privilege-aware setup with signed audit records
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createServer } from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { commandsFor, renderPage } from "../cf-worker/src/view.js";
|
||||
import { commandsFor, pathsFor, renderPage } from "../cf-worker/src/view.js";
|
||||
|
||||
/** Real page rendering and real local assets, with no live Worker or upstream calls. */
|
||||
export async function startBrowserFixture() {
|
||||
@@ -20,7 +20,7 @@ export async function startBrowserFixture() {
|
||||
label: comment,
|
||||
fingerprint: "SHA256:" + createHash("sha256").update(Buffer.from(encoded, "base64")).digest("base64").replace(/=+$/, ""),
|
||||
};
|
||||
const config = { baseUrl: "https://keys.example.test", repositoryUrl: "https://github.com/example/quickly-conf-sshd" };
|
||||
const config = { baseUrl: "https://keys.example.test", repositoryUrl: "https://github.com/example/quickly-conf-sshd", signedRelease: {listId: "fixture-signing-list"}, clientDefaults: {gpgVerification: "off"} };
|
||||
const base = {
|
||||
schema: 1,
|
||||
revision: "0123456789abcdef0123456789abcdef01234567",
|
||||
@@ -31,12 +31,14 @@ export async function startBrowserFixture() {
|
||||
testFixture: true,
|
||||
...config,
|
||||
commands: commandsFor(config),
|
||||
paths: pathsFor(config),
|
||||
keys: [key],
|
||||
signedMetadata: null,
|
||||
};
|
||||
const snapshots = {
|
||||
repository: { ...base, publicCertificate: "HOMEPAGE-MUST-NOT-LEAK-CERTIFICATE", source: { type: "repository" } },
|
||||
gist: { ...base, keyRevision: "d".repeat(64), signedMetadata: {listId: "fixture-signing-list", sequence: 42, documentSha256: "d".repeat(64)}, keysText: text, signedDocument: text, signature: "-----BEGIN PGP SIGNATURE-----\nComment: HOMEPAGE-MUST-NOT-LEAK-SIGNATURE\n\nAQID\n-----END PGP SIGNATURE-----\n", signedBundle: text, source: {type: "gist", id: "f".repeat(32), gistRevision: "e".repeat(40), url: "https://gist.github.com/example/" + "f".repeat(32) + "/" + "e".repeat(40), extraPrivateComment: comment} },
|
||||
"signature-disabled": { ...base, signedRelease: null, signatureAvailable: true, signedMetadata: {listId: "runtime-metadata-only"}, paths: pathsFor({...config, signedRelease: null}), source: {type: "repository"} },
|
||||
fallback: { ...base, source: { type: "fallback", url: "https://keys.example.test/team.keys?comment=<script>window.__injected=true</script>" } },
|
||||
unavailable: { ...base, keys: [], keyCount: 0, sha256: null, source: { type: "unavailable" } },
|
||||
pending: { ...base, keys: [], keyCount: 0, sha256: null, source: { type: "pending" } },
|
||||
|
||||
@@ -81,6 +81,7 @@ test("real browser homepage integration", { skip: process.env.BROWSER_TESTS !==
|
||||
await page.goto(fixture.origin + "/repository");
|
||||
await page.waitForSelector("html.enhanced");
|
||||
await noPageOverflow(page, width);
|
||||
assert.deepEqual(await page.locator("main > section.content-section").evaluateAll((sections) => sections.map((section) => section.id)), ["quick-start", "key-status", "signature", "paths"]);
|
||||
const install = fixture.snapshots.repository.commands.find((command) => command.id === "install").command;
|
||||
assert.equal(await page.locator("#command-install").textContent(), install);
|
||||
assert.equal(await page.locator(".key-card, [data-copy-target^='key-'], #copy-all-keys, #copy-all-commands").count(), 0);
|
||||
@@ -137,6 +138,14 @@ test("real browser homepage integration", { skip: process.env.BROWSER_TESTS !==
|
||||
await page.waitForFunction((id) => document.querySelector(`[data-copy-target="command-${id}"]`).dataset.state === "success", command.id);
|
||||
assert.equal(await page.evaluate(() => window.__clipboardTest.values.at(-1)), command.command);
|
||||
}
|
||||
for (const path of fixture.snapshots.repository.paths) {
|
||||
assert.equal(await page.locator(`#path-${path.id}`).textContent(), path.url);
|
||||
const button = page.locator(`[data-copy-target="path-${path.id}"]`);
|
||||
await button.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForFunction((id) => document.querySelector(`[data-copy-target="path-${id}"]`).dataset.state === "success", path.id);
|
||||
assert.equal(await page.evaluate(() => window.__clipboardTest.values.at(-1)), path.url);
|
||||
}
|
||||
await noPageOverflow(page, width);
|
||||
|
||||
await page.evaluate(() => { window.__clipboardTest.reject = true; });
|
||||
@@ -200,6 +209,10 @@ test("real browser homepage integration", { skip: process.env.BROWSER_TESTS !==
|
||||
// Repository appearance is already captured in both viewports above: 12 screenshots in total.
|
||||
if (state !== "repository") await capture(page, "source-" + state);
|
||||
}
|
||||
await page.goto(fixture.origin + "/signature-disabled");
|
||||
assert.deepEqual(await page.locator("main > section.content-section").evaluateAll((sections) => sections.map((section) => section.id)), ["quick-start", "key-status", "paths"]);
|
||||
assert.equal(await page.locator("#signature, #path-signature, #path-certificate").count(), 0, "runtime signature metadata must not enable the Signature section");
|
||||
assert.equal(await page.locator("#path-root-script").textContent(), fixture.snapshots.repository.baseUrl + "/");
|
||||
assert.deepEqual(externalRequests, []);
|
||||
} finally { await context.close(); }
|
||||
});
|
||||
@@ -214,6 +227,8 @@ test("real browser homepage integration", { skip: process.env.BROWSER_TESTS !==
|
||||
assert.equal(await page.locator("#key-status").isVisible(), true);
|
||||
assert.equal(await page.locator(".key-card, [data-copy-target^='key-'], #copy-all-keys").count(), 0);
|
||||
assert.equal(await page.locator("#command-install").isVisible(), true);
|
||||
assert.equal(await page.locator("#path-root-script").isVisible(), true);
|
||||
assert.equal(await page.locator("#path-homepage").textContent(), fixture.snapshots.repository.baseUrl + "/");
|
||||
assert.equal(await page.locator('[data-copy-target="command-install"]').isVisible(), false);
|
||||
assert.equal(await page.locator("#theme-picker").isVisible(), false);
|
||||
assert.match(await page.locator("noscript").textContent(), /手动选择复制/);
|
||||
|
||||
@@ -37,7 +37,8 @@ test('empty revocation must be explicit; protocol is deterministic',async()=>{
|
||||
test('homepage excludes keys and commands use explicit install and fixed local sync',async()=>{
|
||||
const commands=commandsFor(config);
|
||||
assert.ok(commands.find(x=>x.id==='install').command.includes('--install'));
|
||||
assert.ok(commands.find(x=>x.id==='root-on').advanced);
|
||||
assert.equal(Boolean(commands.find(x=>x.id==='root-on').advanced), false);
|
||||
assert.ok(commands.find(x=>x.id==='root-off').advanced);
|
||||
assert.ok(commandsFor({...config,defaultCron:'0 */6 * * *'})[0].description.includes('0 */6 * * *'));
|
||||
assert.ok(commands.find(x=>x.id==='uninstall').advanced);
|
||||
assert.ok(commands.every(x=>!x.command.includes('github.io')));
|
||||
|
||||
+355
-5
@@ -98,7 +98,8 @@ else
|
||||
cp "$1" "$TEST_CRONTAB"
|
||||
fi
|
||||
''')
|
||||
for cmd in ('sshd', 'apt-get', 'yum', 'dnf', 'systemctl', 'service'):
|
||||
self.mock('sshd', '#!/bin/sh\nif [ \"$#\" = 1 ] && [ \"$1\" = -T ]; then exit 0; fi\necho \"UNEXPECTED sshd $*\" >> \"$TEST_CALLS\"\nexit 98\n')
|
||||
for cmd in ('apt-get', 'yum', 'dnf', 'systemctl', 'service'):
|
||||
self.mock(cmd, f'#!/bin/sh\necho "UNEXPECTED {cmd} $*" >> "$TEST_CALLS"\nexit 98\n')
|
||||
|
||||
def tearDown(self):
|
||||
@@ -130,7 +131,7 @@ fi
|
||||
lines.append(f'{path.name}() {{ {shlex.quote(str(path))} "$@"; }}')
|
||||
# Hyphenated command names such as apt-get are not POSIX function
|
||||
# identifiers and have no BusyBox applets; their PATH mocks remain.
|
||||
for name in ('id', 'crontab'):
|
||||
for name in ('id', 'crontab', 'sshd', 'apt-get', 'dnf', 'yum'):
|
||||
lines.extend([
|
||||
f'if [ "$({name} --conf-sshd-isolation-probe)" != conf-sshd-mock:{name} ]; then',
|
||||
f' printf \'%s\\n\' \'TEST ISOLATION FAILED: {name} mock was bypassed\' >&2',
|
||||
@@ -180,7 +181,7 @@ fi
|
||||
subprocess.run([*SHELL, '-n', str(self.script)], check=True)
|
||||
|
||||
def test_isolation_probe_aborts_before_installer_when_mock_is_bypassed(self):
|
||||
for name in ('id', 'crontab'):
|
||||
for name in ('id', 'crontab', 'sshd', 'apt-get', 'dnf', 'yum'):
|
||||
with self.subTest(name=name):
|
||||
mock_path = self.bin / name
|
||||
original = mock_path.read_text()
|
||||
@@ -535,6 +536,7 @@ fi
|
||||
fixture = SCRIPT.replace('config=/etc/ssh/sshd_config', f"config='{config}'")
|
||||
fixture = fixture.replace('check_path /etc/ssh dir', f"check_path '{config_dir}' dir")
|
||||
fixture = fixture.replace('new_temp /etc/ssh;', f"new_temp '{config_dir}';")
|
||||
fixture = fixture.replace('/etc/ssh', str(config_dir))
|
||||
self.assertNotIn('/etc/ssh', fixture, 'All root-policy paths must stay inside test fixtures')
|
||||
self.script.write_text(fixture)
|
||||
self.mock('id', '#!/bin/sh\nprintf "0\\n"\n')
|
||||
@@ -544,6 +546,11 @@ fi
|
||||
printf 'sshd %s\\n' "$*" >> "$TEST_CALLS"
|
||||
operation=$1
|
||||
shift
|
||||
if [ "$#" = 0 ] && [ "$operation" = -T ]; then
|
||||
[ "${TEST_DEFAULT_SSHD_FAIL:-0}" = 0 ] || exit 1
|
||||
echo 'permitrootlogin no'
|
||||
exit 0
|
||||
fi
|
||||
[ "$1" = -f ] || exit 90
|
||||
config=$2
|
||||
[ "${TEST_SSHD_FAIL:-}" != "$operation" ] || exit 1
|
||||
@@ -616,13 +623,13 @@ fi
|
||||
self.assertTrue(config.read_text().startswith('PermitRootLogin yes\n'))
|
||||
self.assertIn('sshd -t -f', self.calls.read_text())
|
||||
self.assertIn('sshd -T -f', self.calls.read_text())
|
||||
self.assertFalse(self.auth.exists())
|
||||
self.assertEqual(self.auth.read_bytes(), BEGIN + KEY + END)
|
||||
self.assert_clean()
|
||||
self.assertEqual(list(config.parent.glob('.conf-sshd.*')), [])
|
||||
|
||||
def test_root_policy_include_match_invalid_effective_and_dry_run(self):
|
||||
config = self.mock_root_config()
|
||||
for text in ['Include sshd_config.d/*.conf\n', ' Match User test\n',
|
||||
for text in ['Include other/*.conf\n', ' Match User test\n',
|
||||
'Port 2222\nINCLUDE other\n', 'mAtCh all\n',
|
||||
'Include=other\n', 'Match=User root\n', '"Include"=other\n',
|
||||
'"Match" User root\n', '"Match"User root\n']:
|
||||
@@ -735,6 +742,182 @@ fi
|
||||
self.assertFalse(self.auth.exists())
|
||||
|
||||
|
||||
def simulate_dependencies(self, *, manager='apt-get', uid='0', sshd=True, client=True, cron=True):
|
||||
real_stat=shutil.which('stat')
|
||||
self.mock('id',f'#!/bin/sh\nprintf "%s\\n" "{uid}"\n')
|
||||
self.mock('stat',f'#!/bin/sh\n"{real_stat}" "$@" | sed \'s/^[0-9][0-9]*/{uid}/\'\n')
|
||||
fixture=self.script.read_text()
|
||||
for function,command,marker,present in [('sshd_present','sshd','sshd-ready',sshd),('cron_present','crontab','cron-ready',cron),('ssh_client_present','ssh-keygen','client-ready',client)]:
|
||||
old=f'{function}() {{ command -v {command} >/dev/null 2>&1; }}'
|
||||
new=f'{function}() {{ [ -f "$TEST_ROOT/{marker}" ]; }}'
|
||||
self.assertIn(old,fixture)
|
||||
fixture=fixture.replace(old,new)
|
||||
path=self.root/marker
|
||||
if present: path.touch()
|
||||
elif path.exists(): path.unlink()
|
||||
for tool in ('apt-get','dnf','yum'):
|
||||
fixture=fixture.replace(f'command -v {tool} >/dev/null 2>&1',f'[ "$TEST_PACKAGE_MANAGER" = {tool} ]')
|
||||
self.script.write_text(fixture)
|
||||
self.env['TEST_PACKAGE_MANAGER']=manager
|
||||
self.mock('sshd','''#!/bin/sh
|
||||
printf 'sshd %s\\n' "$*" >> "$TEST_CALLS"
|
||||
[ "${TEST_DAEMON_INVALID:-0}" = 0 ] || exit 1
|
||||
[ "$#" = 1 ] && [ "$1" = -T ] || exit 98
|
||||
''')
|
||||
for tool in ('apt-get','dnf','yum'):
|
||||
self.mock(tool,f'''#!/bin/sh
|
||||
printf 'PKG {tool} %s\\n' "$*" >> "$TEST_CALLS"
|
||||
[ "${{TEST_PACKAGE_ERROR:-0}}" = 0 ] || exit 77
|
||||
last=
|
||||
for arg do last=$arg; done
|
||||
case $last in
|
||||
openssh-client|openssh-clients) : > "$TEST_ROOT/client-ready" ;;
|
||||
openssh-server) : > "$TEST_ROOT/sshd-ready" ;;
|
||||
cron|crontabs) : > "$TEST_ROOT/cron-ready" ;;
|
||||
esac
|
||||
''')
|
||||
|
||||
def test_root_apt_installs_missing_server_and_cron_only_after_valid_keys(self):
|
||||
self.simulate_dependencies(sshd=False,cron=False)
|
||||
self.run_script('-c')
|
||||
calls=self.calls.read_text()
|
||||
self.assertIn('PKG apt-get install -y openssh-server',calls)
|
||||
self.assertIn('PKG apt-get install -y cron',calls)
|
||||
self.assertEqual(calls.count('PKG apt-get update\n'),1)
|
||||
self.assertLess(calls.index('/ssh.keys'),calls.index('PKG apt-get update'))
|
||||
self.assertNotIn('upgrade',calls)
|
||||
self.assertNotIn('systemctl',calls)
|
||||
self.assertNotIn('service ',calls)
|
||||
self.assertEqual(self.auth.read_bytes(),BEGIN+KEY+END)
|
||||
|
||||
def test_root_yum_and_dnf_install_recognized_packages(self):
|
||||
for manager in ('yum','dnf'):
|
||||
with self.subTest(manager=manager):
|
||||
self.script.write_text(SCRIPT)
|
||||
self.simulate_dependencies(manager=manager,sshd=False,cron=False)
|
||||
self.calls.write_text('')
|
||||
self.run_script('-c')
|
||||
calls=self.calls.read_text()
|
||||
self.assertIn(f'PKG {manager} install -y openssh-server',calls)
|
||||
self.assertIn(f'PKG {manager} install -y crontabs',calls)
|
||||
self.assertNotIn('PKG apt-get',calls)
|
||||
|
||||
def test_nonroot_missing_daemon_fails_without_packages_but_keys_only_works(self):
|
||||
self.simulate_dependencies(uid='1001',sshd=False)
|
||||
self.run_script(ok=False)
|
||||
self.assertFalse(self.auth.exists())
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
self.run_script('-o')
|
||||
self.assertEqual(self.auth.read_bytes(),BEGIN+KEY+END)
|
||||
self.assertNotIn('sshd -T',self.calls.read_text())
|
||||
|
||||
def test_no_install_skips_missing_daemon_and_never_bootstraps_missing_client(self):
|
||||
self.simulate_dependencies(sshd=False)
|
||||
self.run_script('-n')
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
(self.root/'client-ready').unlink()
|
||||
self.run_script('-n',ok=False)
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
self.run_script('-o',ok=False)
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
|
||||
def test_existing_invalid_daemon_is_not_treated_as_missing_or_reinstalled(self):
|
||||
self.simulate_dependencies(sshd=True)
|
||||
original=self.old_auth()
|
||||
result=self.run_script(ok=False,env={'TEST_DAEMON_INVALID':'1'})
|
||||
self.assertIn('configuration cannot be evaluated',result.stderr)
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
self.assertEqual(self.auth.read_bytes(),original)
|
||||
|
||||
def test_invalid_keys_do_not_install_server_or_cron(self):
|
||||
self.simulate_dependencies(sshd=False,cron=False)
|
||||
self.payload.write_bytes(b'not a public-key document\n')
|
||||
self.run_script('-c',ok=False)
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
self.assertFalse(self.auth.exists())
|
||||
self.assertNotIn(CB,self.cron.read_text())
|
||||
|
||||
def test_missing_client_bootstrap_is_root_normal_only_and_precedes_key_validation(self):
|
||||
self.simulate_dependencies(client=False,sshd=False)
|
||||
self.run_script()
|
||||
calls=self.calls.read_text()
|
||||
self.assertLess(calls.index('install -y openssh-client'),calls.index('/ssh.keys'))
|
||||
self.assertLess(calls.index('/ssh.keys'),calls.index('install -y openssh-server'))
|
||||
self.assertEqual(calls.count('PKG apt-get update\n'),1)
|
||||
|
||||
def test_package_failure_or_unsupported_manager_preserves_keys_and_cron(self):
|
||||
self.simulate_dependencies(sshd=False,cron=False)
|
||||
original=self.old_auth()
|
||||
before=self.cron.read_bytes()
|
||||
result=self.run_script('-c',ok=False,env={'TEST_PACKAGE_ERROR':'1'})
|
||||
self.assertIn('may have changed',result.stderr)
|
||||
self.assertEqual(self.auth.read_bytes(),original)
|
||||
self.assertEqual(self.cron.read_bytes(),before)
|
||||
self.run_script('-c',ok=False,env={'TEST_PACKAGE_MANAGER':'unsupported'})
|
||||
self.assertEqual(self.auth.read_bytes(),original)
|
||||
|
||||
def test_root_policy_combines_with_install_and_cron_then_updates_keys(self):
|
||||
config=self.mock_root_config()
|
||||
self.run_script('--install','-p','no','-c','5 2 * * *')
|
||||
self.assertTrue(config.read_text().startswith('PermitRootLogin prohibit-password\n'))
|
||||
self.assertEqual(self.auth.read_bytes(),BEGIN+KEY+END)
|
||||
self.assertIn('5 2 * * * /bin/sh',self.cron.read_text())
|
||||
self.assertTrue(self.installed.exists())
|
||||
calls=self.calls.read_text()
|
||||
self.assertNotIn('systemctl',calls)
|
||||
self.assertNotIn('service ',calls)
|
||||
|
||||
def test_standard_global_include_preserved_and_effective_policy_checked(self):
|
||||
config=self.mock_root_config('Include sshd_config.d/*.conf\nPort 2222\n')
|
||||
directory=config.parent/'sshd_config.d'
|
||||
directory.mkdir(mode=0o700)
|
||||
drop=directory/'10-settings.conf'
|
||||
drop.write_text('PasswordAuthentication no\nPermitRootLogin yes\n')
|
||||
original=drop.read_bytes()
|
||||
self.run_script('-p','no')
|
||||
self.assertIn('Include sshd_config.d/*.conf\n',config.read_text())
|
||||
self.assertTrue(config.read_text().startswith('PermitRootLogin prohibit-password\n'))
|
||||
self.assertEqual(drop.read_bytes(),original)
|
||||
self.assertIn('sshd -T -f',self.calls.read_text())
|
||||
self.assert_clean()
|
||||
|
||||
def test_nested_include_match_and_symlink_dropins_fail_closed(self):
|
||||
config=self.mock_root_config('Include sshd_config.d/*.conf\n')
|
||||
directory=config.parent/'sshd_config.d'
|
||||
directory.mkdir(mode=0o700)
|
||||
drop=directory/'10-settings.conf'
|
||||
original=config.read_bytes()
|
||||
for body in ('Match User root\nPermitRootLogin yes\n','Include nested/*.conf\n','"Match" User root\n'):
|
||||
drop.write_text(body)
|
||||
self.run_script('-p','no',ok=False)
|
||||
self.assertEqual(config.read_bytes(),original)
|
||||
self.assertFalse(self.auth.exists())
|
||||
drop.unlink()
|
||||
outside=self.root/'outside-settings'
|
||||
outside.write_text('PermitRootLogin yes\n')
|
||||
drop.symlink_to(outside)
|
||||
self.run_script('-p','no',ok=False)
|
||||
self.assertEqual(config.read_bytes(),original)
|
||||
self.assertEqual(outside.read_text(),'PermitRootLogin yes\n')
|
||||
|
||||
def test_concurrent_include_inventory_change_blocks_root_policy_commit(self):
|
||||
config=self.mock_root_config('Include sshd_config.d/*.conf\n')
|
||||
directory=config.parent/'sshd_config.d'
|
||||
directory.mkdir(mode=0o700)
|
||||
(directory/'10-settings.conf').write_text('PasswordAuthentication no\n')
|
||||
old=config.read_bytes()
|
||||
mock=self.bin/'sshd'
|
||||
text=mock.read_text()
|
||||
injected=f'''config=$2
|
||||
if [ "$operation" = -T ]; then printf 'PermitRootLogin yes\\n' > {shlex.quote(str(directory/'20-racing.conf'))}; fi
|
||||
'''
|
||||
mock.write_text(text.replace('config=$2\n',injected,1))
|
||||
result=self.run_script('-p','no',ok=False)
|
||||
self.assertIn('inventory changed concurrently',result.stderr)
|
||||
self.assertEqual(config.read_bytes(),old)
|
||||
self.assertFalse(self.auth.exists())
|
||||
|
||||
|
||||
class SignatureTests(ShellTests):
|
||||
"""Pure sandbox OpenPGP/TTY fixtures; all network paths intercepted."""
|
||||
@classmethod
|
||||
@@ -1296,6 +1479,173 @@ fi
|
||||
self.assert_clean()
|
||||
|
||||
|
||||
@property
|
||||
def audit(self):
|
||||
return self.home/'.conf-sshd/audit'
|
||||
|
||||
def audit_pointer(self,name='current'):
|
||||
path=self.audit/name
|
||||
if not path.exists(): return None
|
||||
lines=path.read_text().splitlines()
|
||||
self.assertEqual(lines[0],'conf-sshd-audit-pointer-v1')
|
||||
self.assertIn(lines[1],('slot a','slot b'))
|
||||
return self.audit/lines[1].split()[1]
|
||||
|
||||
def audit_snapshot(self):
|
||||
return {str(path.relative_to(self.audit)):path.read_bytes() for path in self.audit.rglob('*') if path.is_file()}
|
||||
|
||||
def test_audit_preserves_exact_pair_metadata_actual_signer_and_bounded_history(self):
|
||||
self.enroll()
|
||||
first=self.audit_pointer()
|
||||
self.assertEqual((first/'ssh.keys').read_bytes(),self.payload.read_bytes())
|
||||
self.assertEqual((first/'ssh.keys.sig').read_bytes(),self.signature.read_bytes())
|
||||
metadata=(first/'metadata').read_text()
|
||||
self.assertIn('source '+ORIGIN+'/ssh.keys\n',metadata)
|
||||
self.assertIn('signer-fingerprint '+self.fixtures['subkeys'][0]+'\n',metadata)
|
||||
self.assertIn('primary-fingerprint '+self.fixtures['primary']+'\n',metadata)
|
||||
self.assertIn('pinned-fingerprint '+self.fixtures['primary']+'\n',metadata)
|
||||
self.assertIn('document-sha256 '+hashlib.sha256(self.payload.read_bytes()).hexdigest()+'\n',metadata)
|
||||
self.assertRegex(metadata,r'installed-at [0-9]+\n')
|
||||
self.set_release('sibling')
|
||||
self.run_script('-o')
|
||||
current=self.audit_pointer()
|
||||
self.assertNotEqual(current,first)
|
||||
self.assertEqual(self.audit_pointer('previous'),first)
|
||||
self.assertIn('signer-fingerprint '+self.fixtures['subkeys'][1]+'\n',(current/'metadata').read_text())
|
||||
self.run_script('-o')
|
||||
self.assertEqual(sorted(path.name for path in self.audit.iterdir() if path.is_dir()),['a','b'])
|
||||
for path in self.audit.rglob('*'):
|
||||
self.assertEqual(path.stat().st_mode & 0o777,0o700 if path.is_dir() else 0o600)
|
||||
self.assert_clean()
|
||||
|
||||
def test_failed_fetch_or_signature_leaves_current_and_history_byte_identical(self):
|
||||
self.enroll()
|
||||
self.run_script('-o')
|
||||
original=self.audit_snapshot()
|
||||
self.run_script('-o',ok=False,env={'TEST_SIG_STATUS':'404'})
|
||||
self.assertEqual(self.audit_snapshot(),original)
|
||||
self.set_release('wrongsigner')
|
||||
self.run_script('-o',ok=False)
|
||||
self.assertEqual(self.audit_snapshot(),original)
|
||||
|
||||
def test_off_updates_invalidate_current_without_new_signature_or_audit_record(self):
|
||||
self.enroll()
|
||||
old=self.audit_pointer()
|
||||
old_pair=((old/'ssh.keys').read_bytes(),(old/'ssh.keys.sig').read_bytes())
|
||||
self.calls.write_text('')
|
||||
self.run_tty('-o','--gpg-verification','off')
|
||||
self.assertIsNone(self.audit_pointer())
|
||||
self.assertEqual(self.audit_pointer('previous'),old)
|
||||
self.assertEqual(((old/'ssh.keys').read_bytes(),(old/'ssh.keys.sig').read_bytes()),old_pair)
|
||||
self.assertNotIn('/ssh.keys.sig',self.calls.read_text())
|
||||
self.assertNotIn('/public-key.gpg',self.calls.read_text())
|
||||
self.assertFalse((self.home/'.conf-sshd/pending.source').exists())
|
||||
|
||||
def test_audit_failure_windows_recover_without_mixed_or_falsely_current_pair(self):
|
||||
self.enroll()
|
||||
real_mv=shutil.which('mv')
|
||||
destinations=['audit-slot','auth','replay','audit-current']
|
||||
for destination in destinations:
|
||||
with self.subTest(destination=destination):
|
||||
before_slot=self.audit_pointer()
|
||||
before_pair=((before_slot/'ssh.keys').read_bytes(),(before_slot/'ssh.keys.sig').read_bytes())
|
||||
self.set_release('next')
|
||||
self.old_auth(b'# manual key before commit\n')
|
||||
self.mock('mv',f'''#!/bin/sh
|
||||
last=
|
||||
for arg do last=$arg; done
|
||||
case "$TEST_AUDIT_FAILURE:$last" in
|
||||
audit-slot:"$HOME/.conf-sshd/audit/"*/ssh.keys.sig|auth:"$HOME/.ssh/authorized_keys"|replay:"$HOME/.conf-sshd/replay.state"|audit-current:"$HOME/.conf-sshd/audit/current") exit 74 ;;
|
||||
esac
|
||||
exec "{real_mv}" "$@"
|
||||
''')
|
||||
self.run_script('-o',ok=False,env={'TEST_AUDIT_FAILURE':destination})
|
||||
self.assertTrue((self.home/'.conf-sshd/pending.state').exists())
|
||||
if destination=='audit-slot':
|
||||
self.assertEqual(self.audit_pointer(),before_slot)
|
||||
else:
|
||||
self.assertIsNone(self.audit_pointer())
|
||||
self.assertEqual(self.audit_pointer('previous'),before_slot)
|
||||
self.assertEqual(((before_slot/'ssh.keys').read_bytes(),(before_slot/'ssh.keys.sig').read_bytes()),before_pair)
|
||||
(self.bin/'mv').unlink()
|
||||
self.calls.write_text('')
|
||||
self.run_script('-o')
|
||||
recovered=self.audit_pointer()
|
||||
self.assertEqual((recovered/'ssh.keys').read_bytes(),self.payload.read_bytes())
|
||||
self.assertEqual((recovered/'ssh.keys.sig').read_bytes(),self.signature.read_bytes())
|
||||
metadata=(recovered/'metadata').read_text()
|
||||
self.assertIn('source '+ORIGIN+'/ssh.keys\n',metadata)
|
||||
self.assertIn('signer-fingerprint '+self.fixtures['subkeys'][0]+'\n',metadata)
|
||||
self.assertNotIn('curl ',self.calls.read_text())
|
||||
self.assertFalse((self.home/'.conf-sshd/pending.source').exists())
|
||||
self.assert_clean()
|
||||
|
||||
def test_legacy_pending_without_source_context_records_truthful_unknown_source(self):
|
||||
self.enroll()
|
||||
self.set_release('next')
|
||||
real_mv=shutil.which('mv')
|
||||
self.old_auth(b'# retained manual\n')
|
||||
self.mock('mv',f'''#!/bin/sh
|
||||
last=
|
||||
for arg do last=$arg; done
|
||||
[ "$last" != "$HOME/.ssh/authorized_keys" ] || exit 74
|
||||
exec "{real_mv}" "$@"
|
||||
''')
|
||||
self.run_script('-o',ok=False)
|
||||
(self.bin/'mv').unlink()
|
||||
(self.home/'.conf-sshd/pending.source').unlink()
|
||||
self.run_script('-o')
|
||||
self.assertIn('source local-pending:original-source-unavailable\n',(self.audit_pointer()/'metadata').read_text())
|
||||
|
||||
def test_pending_source_context_must_match_document_and_signature(self):
|
||||
self.enroll()
|
||||
self.set_release('next')
|
||||
real_mv=shutil.which('mv')
|
||||
self.old_auth(b'# retained manual\n')
|
||||
self.mock('mv',f'''#!/bin/sh
|
||||
last=
|
||||
for arg do last=$arg; done
|
||||
[ "$last" != "$HOME/.ssh/authorized_keys" ] || exit 74
|
||||
exec "{real_mv}" "$@"
|
||||
''')
|
||||
self.run_script('-o',ok=False)
|
||||
(self.bin/'mv').unlink()
|
||||
context=self.home/'.conf-sshd/pending.source'
|
||||
context.write_text(context.read_text().replace('sequence 6\n','sequence 999\n'))
|
||||
original=self.auth.read_bytes()
|
||||
self.run_script('-o',ok=False)
|
||||
self.assertEqual(self.auth.read_bytes(),original)
|
||||
self.assertIsNone(self.audit_pointer())
|
||||
|
||||
def test_audit_symlinks_are_refused_without_touching_external_targets(self):
|
||||
self.enroll()
|
||||
current=self.audit_pointer()
|
||||
other='b' if current.name=='a' else 'a'
|
||||
outside=self.root/'audit-outside'
|
||||
outside.mkdir()
|
||||
(outside/'ssh.keys').write_text('outside unchanged\n')
|
||||
(self.audit/other).symlink_to(outside,target_is_directory=True)
|
||||
original=self.auth.read_bytes()
|
||||
self.set_release('next')
|
||||
self.run_script('-o',ok=False)
|
||||
self.assertEqual(self.auth.read_bytes(),original)
|
||||
self.assertEqual((outside/'ssh.keys').read_text(),'outside unchanged\n')
|
||||
self.assertEqual(self.audit_pointer(),current)
|
||||
|
||||
def test_signature_consent_precedes_any_root_client_bootstrap(self):
|
||||
self.simulate_dependencies(client=False,sshd=False)
|
||||
self.enroll(answer='cancel',ok=False)
|
||||
self.assertNotIn('PKG ',self.calls.read_text())
|
||||
self.assertNotIn('curl ',self.calls.read_text())
|
||||
self.assertFalse(self.auth.exists())
|
||||
self.enroll()
|
||||
calls=self.calls.read_text()
|
||||
self.assertIn('install -y openssh-client',calls)
|
||||
self.assertIn('install -y openssh-server',calls)
|
||||
self.assertLess(calls.index('/public-key.gpg'),calls.index('install -y openssh-client'))
|
||||
self.assertLess(calls.index('/ssh.keys.sig'),calls.index('install -y openssh-server'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite=unittest.TestSuite()
|
||||
suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(ShellTests))
|
||||
|
||||
+69
-8
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { commandsFor, renderPage } from "../cf-worker/src/view.js";
|
||||
import { commandsFor, pathsFor, renderPage } from "../cf-worker/src/view.js";
|
||||
import { makeSignedDocument, parsePublishedSignedRelease } from "../cf-worker/src/signed-release.js";
|
||||
|
||||
const script = await readFile(new URL("../site/assets/app.js", import.meta.url), "utf8");
|
||||
@@ -78,14 +78,14 @@ function setup({ commandTexts = commands.map(({command}) => command), savedTheme
|
||||
}
|
||||
|
||||
function snapshot(overrides = {}) {
|
||||
return {revision: "a".repeat(40), publishedAt: "2026-10-12T00:00:00Z", keys: [], keyCount: 1, sha256: "b".repeat(64), keyRevision: "c".repeat(64), commands, ...config, ...overrides};
|
||||
return {revision: "a".repeat(40), publishedAt: "2026-10-12T00:00:00Z", keys: [], keyCount: 1, sha256: "b".repeat(64), keyRevision: "c".repeat(64), commands, ...config, signedRelease: {listId: "test-list"}, ...overrides};
|
||||
}
|
||||
function embeddedMetadata(page) {
|
||||
return JSON.parse(page.match(/<script id="release-data" type="application\/json">([\s\S]*?)<\/script>/)[1]);
|
||||
}
|
||||
|
||||
test("homepage prerenders metadata and commands, never a public-key listing", () => {
|
||||
for (const token of ["RELEASE_JSON", "COMMANDS_HTML", "REVISION", "PUBLISHED_AT", "KEY_COUNT", "REPOSITORY_URL", "RELEASE_LABEL", "PREVIEW_NOTICE", "SOURCE_LABEL", "SOURCE_STATUS", "SIGNATURE_STATUS", "CERTIFICATE_STATUS", "CERTIFICATE_LINK", "CONTENT_REVISION", "KEYS_NOTICE"]) {
|
||||
for (const token of ["RELEASE_JSON", "COMMANDS_HTML", "PATHS_HTML", "REVISION", "PUBLISHED_AT", "KEY_COUNT", "REPOSITORY_URL", "RELEASE_LABEL", "PREVIEW_NOTICE", "SOURCE_LABEL", "SOURCE_STATUS", "SIGNATURE_STATUS", "CERTIFICATE_STATUS", "CERTIFICATE_LINK", "SIGNATURE_SECTION_START", "SIGNATURE_SECTION_END", "VERIFICATION_DEFAULT", "PATHS_INDEX", "CONTENT_REVISION", "KEYS_NOTICE"]) {
|
||||
assert.ok(html.includes(`@@${token}@@`), `${token} placeholder is required`);
|
||||
}
|
||||
assert.match(html, /<html lang="zh-CN">/);
|
||||
@@ -108,7 +108,7 @@ test("homepage projection excludes raw keys, fingerprints, comments and signed d
|
||||
assert.doesNotMatch(page, new RegExp(secretMarker));
|
||||
assert.doesNotMatch(page, /ssh-ed25519|SHA256:|data-copy-target="key-|id="key-\d|class="key-card"/);
|
||||
const embedded = embeddedMetadata(page);
|
||||
assert.deepEqual(Object.keys(embedded).sort(), ["schema", "revision", "publishedAt", "keyCount", "sha256", "keyRevision", "source", "sourceStatus", "signaturePresent", "signatureStatus", "certificatePresent", "certificateStatus", "repositoryUrl", "testFixture"].sort());
|
||||
assert.deepEqual(Object.keys(embedded).sort(), ["schema", "revision", "publishedAt", "keyCount", "sha256", "keyRevision", "source", "sourceStatus", "signatureEnabled", "signaturePresent", "signatureStatus", "certificatePresent", "certificateStatus", "repositoryUrl", "testFixture"].sort());
|
||||
assert.deepEqual(embedded.source, {type: "gist", url: "https://gist.github.com/example/example"});
|
||||
assert.equal(embedded.keyCount, 1);
|
||||
assert.equal(embedded.keyRevision, "c".repeat(64));
|
||||
@@ -194,17 +194,31 @@ test("commands use an explicit installer and fixed installed client; no self-upd
|
||||
assert.equal(byId.help.command, "curl -fsSL 'https://example.test/script.sh' | sh -s -- -h");
|
||||
assert.match(byId.help.description, /不建立信任/);
|
||||
assert.match(byId.install.description, /备份.*保留本地配置/);
|
||||
assert.match(byId.install.description, /首次安装或明确重装都会信任你执行的脚本/);
|
||||
assert.match(byId.install.description, /推荐先独立核验安装脚本/);
|
||||
assert.match(byId.reinstall.description, /首次安装或明确重装都会信任你执行的脚本/);
|
||||
assert.match(byId.reinstall.description, /推荐先独立核验安装脚本/);
|
||||
assert.doesNotMatch(byId.install.description, /重装须使用.*本地脚本/);
|
||||
assert.deepEqual(commands.filter(({advanced}) => !advanced).map(({id}) => id), ["install", "deploy-once", "keys-only", "root-on", "help"]);
|
||||
assert.equal(byId["deploy-once"].command, "curl -fsSL 'https://example.test/script.sh' | sh -s --");
|
||||
assert.equal(byId["keys-only"].command, "curl -fsSL 'https://example.test/script.sh' | sh -s -- -o");
|
||||
assert.equal(byId["root-on"].command, "curl -fsSL 'https://example.test/script.sh' | sh -s -- -p yes");
|
||||
assert.equal(byId["skip-sshd-install"].command, "curl -fsSL 'https://example.test/script.sh' | sh -s -- -n");
|
||||
assert.equal(byId["cron-custom"].command, 'sh "$HOME/.conf-sshd/conf-sshd.sh" -c \'0 */6 * * *\'');
|
||||
assert.equal(byId["cron-stop"].command, 'sh "$HOME/.conf-sshd/conf-sshd.sh" -c false');
|
||||
assert.equal(byId.status.command, 'sh "$HOME/.conf-sshd/conf-sshd.sh" --status');
|
||||
assert.equal(byId.reinstall.command, "curl -fsSL 'https://example.test/script.sh' | sh -s -- --install --download-script");
|
||||
assert.match(byId.install.description, /root 可按需安装 SSH Server 与 cron,非 root 缺少依赖时退出/);
|
||||
assert.match(byId["deploy-once"].description, /不新增定时任务/);
|
||||
assert.match(byId["keys-only"].description, /跳过 SSH Server 检查与软件包配置/);
|
||||
assert.match(byId["root-on"].description, /须以 root 执行.*并部署公钥.*不自动重载/);
|
||||
assert.match(byId.reinstall.description, /代替原 -u/);
|
||||
assert.equal(byId.update, undefined);
|
||||
for (const id of ["root-off", "root-on", "uninstall"]) {
|
||||
for (const id of ["root-off", "uninstall"]) {
|
||||
assert.equal(byId[id].advanced, true);
|
||||
assert.ok(byId[id].command.startsWith('sh "$HOME/.conf-sshd/conf-sshd.sh"'));
|
||||
}
|
||||
assert.ok(commands.every(({command}) => !/(?:^|\s)-u(?:\s|$)|--update/.test(command)));
|
||||
assert.match(html, /预期 SHA-256.*pin/);
|
||||
assert.match(html, /验签默认关闭/);
|
||||
assert.match(renderPage(html, snapshot()), /验签默认关闭/);
|
||||
assert.match(html, /--install --gpg-verification on/);
|
||||
assert.match(html, /远端发布内容不能关闭本地验签/);
|
||||
assert.match(html, /本地终端确认完整指纹,不能跳过确认/);
|
||||
@@ -216,6 +230,53 @@ test("commands use an explicit installer and fixed installed client; no self-upd
|
||||
assert.doesNotMatch(html, /\/key-release/);
|
||||
});
|
||||
|
||||
test("Quick Usage and Paths retain original categories without repository setup instructions", () => {
|
||||
assert.match(html, /Quick Usage · 快速使用/);
|
||||
assert.match(html, /Paths · 访问地址/);
|
||||
assert.doesNotMatch(html, /<h[1-6][^>]*>Install|Fork 本仓库|wrangler publish/);
|
||||
const paths = pathsFor({...config, signedRelease: {listId: "test-list"}});
|
||||
assert.deepEqual(paths.slice(0, 4).map(({id, url}) => ({id, url})), [
|
||||
{id: "keys", url: "https://example.test/ssh.keys"},
|
||||
{id: "script", url: "https://example.test/script.sh"},
|
||||
{id: "root-script", url: "https://example.test/"},
|
||||
{id: "homepage", url: "https://example.test/"},
|
||||
]);
|
||||
assert.match(paths.find(({id}) => id === "root-script").description, /Curl.*Shell.*text\/html/);
|
||||
assert.equal(paths.find(({id}) => id === "signature").url, "https://example.test/ssh.keys.sig");
|
||||
assert.equal(paths.find(({id}) => id === "certificate").url, "https://example.test/public-key.gpg");
|
||||
const page = renderPage(html, snapshot());
|
||||
for (const path of paths) {
|
||||
assert.ok(page.includes(`id="path-${path.id}">${path.url}</code>`));
|
||||
assert.ok(page.includes(`data-copy-target="path-${path.id}"`));
|
||||
}
|
||||
assert.doesNotMatch(page, /copy-all-paths|copy-all-commands/);
|
||||
});
|
||||
|
||||
test("section order and Signature visibility depend only on trusted signing configuration", () => {
|
||||
for (const [state, enabled] of [
|
||||
[{signedRelease: null, clientDefaults: {gpgVerification: "off"}}, false],
|
||||
[{signedRelease: null, clientDefaults: {gpgVerification: "off"}, signatureAvailable: true, signedMetadata: {listId: "untrusted-metadata"}, publicCertificate: "opaque-cert"}, false],
|
||||
[{signedRelease: {listId: "configured"}, clientDefaults: {gpgVerification: "off"}, signatureAvailable: false}, true],
|
||||
[{signedRelease: null, clientDefaults: {gpgVerification: "on"}, signatureAvailable: false}, true],
|
||||
]) {
|
||||
const page = renderPage(html, snapshot(state));
|
||||
const sections = [...page.matchAll(/<section id="([^"]+)" class="content-section/g)].map((match) => match[1]);
|
||||
assert.deepEqual(sections, enabled ? ["quick-start", "key-status", "signature", "paths"] : ["quick-start", "key-status", "paths"]);
|
||||
assert.equal(embeddedMetadata(page).signatureEnabled, enabled);
|
||||
assert.ok(page.includes(`href="${config.repositoryUrl}#readme"`), "installation instructions remain linked even without Signature");
|
||||
assert.equal(page.includes('id="path-signature"'), enabled);
|
||||
assert.equal(page.includes('id="path-certificate"'), enabled);
|
||||
assert.doesNotMatch(page, /@@[A-Z_]+@@/);
|
||||
if (enabled) {
|
||||
assert.match(page, /Signature · 签名与本地信任/);
|
||||
assert.match(page, /未提供签名包/);
|
||||
if (state.clientDefaults.gpgVerification === "on") assert.match(page, /本仓库默认开启客户端验签/);
|
||||
else assert.match(page, /客户端验签默认关闭/);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(pathsFor(config).map(({id}) => id), ["keys", "script", "root-script", "homepage"]);
|
||||
});
|
||||
|
||||
test("default follows system and explicit light/dark/system choices persist", () => {
|
||||
const page = setup();
|
||||
const select = page.elements.get("theme-picker");
|
||||
|
||||
Reference in New Issue
Block a user