Skip to content

Commit

Permalink
login: Make SSH known host key compatible with cockpit-beiboot
Browse files Browse the repository at this point in the history
cockpit-ssh sends us full host keys, which the login page puts into
localStore. ferny/cockpit-beiboot can't do that, as there is no portable
way to tell ssh(1) to show full host keys.

Make this check compatible: If the reference key in the db is a full host key
(not including a `:`, which is invalid base64), but the key received from the
auth bridge (cockpit-beiboot) is a fingerprint (starting with "SHA256:"), then
compute the fingerprint of the reference key and compare it to the received
fingerprint.

There is an additional twist: cockpit-ssh and cockpit-beiboot/ssh don't always
agree on the presented key type. We only store one host key (unlike
~/.ssh/known_hosts, which stores all of them), so if the DB and received key
type don't match, then just treat the host as new/unseen, instead of the much
more scary "host key changed" dialog.
  • Loading branch information
martinpitt committed Jun 27, 2024
1 parent 1d96c4e commit 6a9a3e0
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 7 deletions.
50 changes: 43 additions & 7 deletions pkg/static/login.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import "./login.scss";

import sha256 from "js-sha256";
import { base64_decode, base64_encode } from "_internal/base64";

(function(console) {
let localStorage;

Expand Down Expand Up @@ -747,15 +750,48 @@ import "./login.scss";
function do_hostkey_verification(data) {
const key_db = get_known_hosts_db();
const key = data["host-key"];
const key_key = key.split(" ")[0];
const key_type = key.split(" ")[1];
const [key_key, key_type, key_value] = key.split(" ");
let db_key = key_db[key_key];

// did we see this host before?
if (db_key) {
// exact same key → good
if (key_db[key_key] == key) {
converse(data.id, data.default);
return;
}

if (key_db[key_key] == key) {
converse(data.id, data.default);
return;
/* transition case: cockpit-ssh stored full public keys in the DB, like
* "myhost ecdsa-sha2-nistp256 AAAA[..]ToM=\n"; ssh(1) does not show them, only fingerprints, so
* cockpit-beiboot only responds with fingerprints as well; they look like "myhost ED25519 SHA256:JVG[..]xs"
* check if we can match the given fingerprint to an existing entry */
const [db_key_type, db_key_value] = db_key.split(" ").slice(1);
if (key_value.startsWith('SHA256:') && db_key_value && !db_key_value.includes(':')) {
// cockpit-ssh and ssh use different notation: ED25519 vs. ssh-ed25519
if (key_type.replace('ssh-', '').toLowerCase() == db_key_type.replace('ssh-', '').toLowerCase()) {
// cockpit-ssh and ssh agree no which key type to show to the user;
// compute SHA256 fingerprint of reference db key and compare it
const fp_raw = 'SHA256:' + base64_encode(sha256.digest(base64_decode(db_key_value)));
// ssh omits trailing padding
const fp = fp_raw.replace(/=+$/, '');

// if it matches, accept
if (fp == key_value) {
converse(data.id, data.default);
return;
}

// otherwise, the key changed, let user confirm below
} else {
// cockpit-ssh and ssh disagree on which key type to show to the user
console.warn("host key verification: host sent us key type", key_type,
"but our localStore database has type", db_key_type, "; treating as new host");
db_key = null;
}
}
}

if (key_db[key_key]) {
if (db_key) {
id("hostkey-title").textContent = format(_("$0 key changed"), id("server-field").value);
show("#hostkey-warning-group");
id("hostkey-message-1").textContent = "";
Expand Down Expand Up @@ -793,7 +829,7 @@ import "./login.scss";
show_form("hostkey");
show("#get-out-link");

if (key_db[key_key]) {
if (db_key) {
id("login-button").classList.add("pf-m-danger");
id("login-button").classList.remove("pf-m-primary");
}
Expand Down
32 changes: 32 additions & 0 deletions test/verify/check-static-login
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,38 @@ Command = /usr/bin/env python3 -m cockpit.beiboot
try_login("admin", "foobar", server=f"other@{my_ip}")
check_session()

# clear login page's known host keys
b.eval_js("window.localStorage.setItem('known_hosts', '{}')")
# now it prompts for host key again
try_login("admin", "foobar", expect_hostkey=True)
check_session()

# test localStorage compatibility with full host keys from cockpit-ssh

# case 1: cockpit-ssh and ssh agree on key type (ED25519)
hostkey = m.execute(f"ssh-keyscan -t ssh-ed25519 {my_ip}").strip()
b.eval_js(f"""window.localStorage.setItem('known_hosts', '{{"{my_ip}": "{hostkey}"}}')""")
# login works without prompting for host key
try_login("admin", "foobar")
check_session()

# case 2: known key type, but non-matching fingerprint
bad_key = "AAAACdNzaCblZDIbNTEfAAAAICMiOpgxATmTdJzJSvgseN+HZnVytdrBPOatt/jXwefQ"
b.eval_js(f"""window.localStorage.setItem('known_hosts', '{{"{my_ip}": "{bad_key}"}}')""")
# login works without prompting for host key
try_login("admin", "foobar")
b.wait_text("#hostkey-title", f"{my_ip} key changed")
b.wait_in_text("#hostkey-fingerprint", "SHA256:")
b.click("#login-button.pf-m-danger")
check_session()

# case 3: cockpit-ssh and ssh disagree on key type
hostkey = m.execute(f"ssh-keyscan -t ecdsa-sha2-nistp256 {my_ip}").strip()
b.eval_js(f"""window.localStorage.setItem('known_hosts', '{{"{my_ip}": "{hostkey}"}}')""")
# login treats this as new host
try_login("admin", "foobar", expect_hostkey=True)
check_session()


if __name__ == '__main__':
testlib.test_main()

0 comments on commit 6a9a3e0

Please sign in to comment.