feat: downstream host key pinning (#1)

This commit is contained in:
2026-08-02 18:19:35 -04:00
parent 463111dfd6
commit 0f6be51c35
2 changed files with 7 additions and 2 deletions
+5 -2
View File
@@ -23,7 +23,7 @@ function counter(onBytes) {
// Connect the upstream ssh2.Client, retrying once after a short pause if the // Connect the upstream ssh2.Client, retrying once after a short pause if the
// first attempt fails auth (SSSD/AuthorizedKeysCommand cache lag right after a // first attempt fails auth (SSSD/AuthorizedKeysCommand cache lag right after a
// first-time key injection). // first-time key injection).
function connectUpstream({ host, port, username, privateKey, cert, onHostKey, uid, justInjected }) { function connectUpstream({ host, port, username, privateKey, cert, onHostKey, uid, justInjected, expectedHostKeyFp }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let attempted = false; let attempted = false;
const dial = (allowRetry) => { const dial = (allowRetry) => {
@@ -48,7 +48,10 @@ function connectUpstream({ host, port, username, privateKey, cert, onHostKey, ui
hostVerifier: (key) => { hostVerifier: (key) => {
const fp = 'SHA256:' + crypto.createHash('sha256').update(key).digest('base64').replace(/=+$/, ''); const fp = 'SHA256:' + crypto.createHash('sha256').update(key).digest('base64').replace(/=+$/, '');
if (onHostKey) onHostKey(fp); if (onHostKey) onHostKey(fp);
return true; // v1: trust-on-use, fingerprint audited. Pinning = follow-up. if (expectedHostKeyFp && expectedHostKeyFp !== fp) {
return false;
}
return true; // v1: trust-on-use if not pinned, fingerprint audited.
}, },
}); });
}; };
+2
View File
@@ -150,6 +150,7 @@ async function resolveAndConnect(state, record, { onHostKey } = {}) {
host: endpoint.address, port: endpoint.port, host: endpoint.address, port: endpoint.port,
username: state.uid, privateKey: JUMP_KEYS.clientKey, cert, username: state.uid, privateKey: JUMP_KEYS.clientKey, cert,
uid: state.uid, justInjected, onHostKey, uid: state.uid, justInjected, onHostKey,
expectedHostKeyFp: host && host.metadata && host.metadata.sshHostKeyFp,
}); });
} catch (err) { throw fail('upstream-unreachable', err.message, host ? host.slug : undefined); } } catch (err) { throw fail('upstream-unreachable', err.message, host ? host.slug : undefined); }
@@ -261,6 +262,7 @@ async function runTuiSession(session, client, state) {
host: endpoint.address, port: endpoint.port, host: endpoint.address, port: endpoint.port,
username: state.uid, privateKey: JUMP_KEYS.clientKey, cert, username: state.uid, privateKey: JUMP_KEYS.clientKey, cert,
uid: state.uid, justInjected, onHostKey: (fp) => record.patch({ hostKeyFp: fp }), uid: state.uid, justInjected, onHostKey: (fp) => record.patch({ hostKeyFp: fp }),
expectedHostKeyFp: tui.host && tui.host.metadata && tui.host.metadata.sshHostKeyFp,
}); });
} catch (err) { } catch (err) {
try { tui.channel.write(`\r\n Could not reach ${endpoint.address}.\r\n`); tui.channel.close(); } catch (_) {} try { tui.channel.write(`\r\n Could not reach ${endpoint.address}.\r\n`); tui.channel.close(); } catch (_) {}