Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

beiboot: feature parity with cockpit-ssh #19401

Merged
merged 9 commits into from
Sep 23, 2024
6 changes: 3 additions & 3 deletions containers/flatpak/test/test-browser-login-ssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ async function test() {
document.getElementById("server-field").value = "%HOST%";
ph_mouse("#login-button", "click");

// unknown host key
await assert_conversation("authenticity of host");
document.getElementById("conversation-input").value = "yes";
// accept unknown host key
await ph_wait_present("#hostkey-message-1");
await ph_wait_in_text("#hostkey-message-1", "%HOST%");
ph_mouse("#login-button", "click");

await ph_wait_present("#conversation-prompt");
Expand Down
13 changes: 7 additions & 6 deletions doc/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,15 @@ Cockpit also supports logging directly into remote machines. The remote machine
connect to is provided by using a application name that begins with `cockpit+=`.
The default command used for this is cockpit-ssh.

The section `SSH-Login` defines the options for all ssh commands. The section
The section `Ssh-Login` defines the options for all ssh commands. The section
has the same options as the other authentication sections with the following additions.

* `host` The default host to log into. Defaults to 127.0.0.1.
* `host` The default host to log into. Defaults to 127.0.0.1. That host's key
will not be checked/validated.
* `connectToUnknownHosts`. By default cockpit will refuse to connect to any machines that
are not already present in ssh's global `known_hosts` file (usually
`/etc/ssh/ssh_known_hosts`). Set this to `true` is to allow those connections
to proceed.
are not already present in ssh's global `known_hosts` file (usually
`/etc/ssh/ssh_known_hosts`). Set this to `true` is to allow those connections
to proceed.

This uses the [cockpit-ssh](https://github.com/cockpit-project/cockpit/tree/main/src/ssh)
bridge. After the user authentication with the `"*"` challenge, if the remote
Expand Down Expand Up @@ -159,7 +160,7 @@ Actions
Setting an action can modify the behavior for an auth scheme. Currently two actions
are supported.

* **remote-login-ssh** Use the `SSH-Login` section instead.
* **remote-login-ssh** Use the `Ssh-Login` section instead.
* **none** Disable this auth scheme.

To configure an action add the `action` option. For example to disable basic authentication,
Expand Down
109 changes: 99 additions & 10 deletions pkg/static/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ function debug(...args) {
return document.getElementById(name);
}

// strip off "user@", "*:port", and IPv6 brackets from login target (but keep two :: intact for IPv6)
function parseHostname(ssh_target) {
return ssh_target
.replace(/^.*@/, '')
.replace(/(?<!:):[0-9]+$/, '')
.replace(/^\[/, '')
.replace(/\]$/, '');
}

// Hide an element (or set of elements) based on a boolean
// true: element is hidden, false: element is shown
function hideToggle(elements, toggle) {
Expand Down Expand Up @@ -596,10 +605,18 @@ function debug(...args) {

// value of #server-field at the time of clicking "Login"
let login_machine = null;
/* set by do_hostkey_verification() for a confirmed unknown host fingerprint;
* setup_localstorage() will then write the received full known_hosts entry to the known_hosts
* database for this host */
let login_data_host = null;
/* set if our known_host database has a non-matching host key, and we re-attempt the login
* with asking the user for confirmation */
let ssh_host_key_change_host = null;

function call_login() {
login_failure(null);
login_machine = id("server-field").value;
login_data_host = null;
const user = trim(id("login-user-input").value);
if (user === "" && !environment.is_cockpit_client) {
login_failure(_("User name cannot be empty"));
Expand Down Expand Up @@ -631,8 +648,27 @@ function debug(...args) {
/* Keep information if login page was used */
localStorage.setItem('standard-login', true);

let known_hosts = '';
if (login_machine) {
if (ssh_host_key_change_host == login_machine) {
/* We came here because logging in ran into invalid-hostkey; so try the next
* round without sending the key. do_hostkey_verification() will notice the
change and show the correct dialog. */
debug("call_login(): previous login attempt into", login_machine, "failed due to changed key");
} else {
// If we have a known host key, send it to ssh
const keys = get_hostkeys(login_machine);
if (keys) {
debug("call_login(): sending known_host key", keys, "for logging into", login_machine);
known_hosts = keys;
} else {
debug("call_login(): no known_hosts entry for logging into", login_machine);
}
}
}

const headers = {
Authorization: "Basic " + window.btoa(utf8(user + ":" + password)),
Authorization: "Basic " + window.btoa(utf8(user + ":" + password + '\0' + known_hosts)),
"X-Superuser": superuser,
};
// allow unknown remote hosts with interactive logins with "Connect to:"
Expand Down Expand Up @@ -766,29 +802,36 @@ function debug(...args) {
}
}

function set_known_hosts_db(db) {
function get_hostkeys(host) {
return get_known_hosts_db()[parseHostname(host)];
}

function set_hostkeys(host, keys) {
try {
const db = get_known_hosts_db();
db[parseHostname(host)] = keys;
localStorage.setItem("known_hosts", JSON.stringify(db));
} catch (ex) {
console.warn("Can't write known_hosts database to localStorage", ex);
}
}

function do_hostkey_verification(data) {
const key_db = get_known_hosts_db();
const key = data["host-key"];
const key_host = key.split(" ")[0];
const key_type = key.split(" ")[1];
const db_keys = get_hostkeys(key_host);

if (key_db[key_host] == key) {
// code path for old C cockpit-ssh, which doesn't set a known_hosts file in advance (like beiboot)
if (db_keys == key) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

debug("do_hostkey_verification: received key matches known_hosts database, auto-accepting fingerprint", data.default);
converse(data.id, data.default);
return;
}

if (key_db[key_host]) {
if (db_keys) {
debug("do_hostkey_verification: received key fingerprint", data.default, "for host", key_host,
"does not match key in known_hosts database:", key_db[key_host], "; treating as changed");
"does not match key in known_hosts database:", db_keys, "; treating as changed");
id("hostkey-title").textContent = format(_("$0 key changed"), login_machine);
show("#hostkey-warning-group");
id("hostkey-message-1").textContent = "";
Expand Down Expand Up @@ -818,8 +861,16 @@ function debug(...args) {
function call_converse() {
id("login-button").removeEventListener("click", call_converse);
login_failure(null, "hostkey");
key_db[key_host] = key;
set_known_hosts_db(key_db);
if (key.endsWith(" login-data")) {
// cockpit-beiboot sends only a placeholder, defer to login-data in setup_localstorage()
login_data_host = key_host;
debug("call_converse(): got placeholder host key (beiboot code path) for", login_data_host,
", deferring db update");
} else {
// cockpit-ssh already sends the actual key here
set_hostkeys(key_host, key);
debug("call_converse(): got real host key (cockpit-ssh code path) for", login_data_host);
}
converse(data.id, data.default);
}

Expand All @@ -828,7 +879,7 @@ function debug(...args) {
show_form("hostkey");
show("#get-out-link");

if (key_db[key_host]) {
if (db_keys) {
id("login-button").classList.add("pf-m-danger");
id("login-button").classList.remove("pf-m-primary");
}
Expand Down Expand Up @@ -945,6 +996,22 @@ function debug(...args) {
} else {
if (window.console)
console.log(xhr.statusText);
/* did the user confirm a changed SSH host key? If so, update database */
if (ssh_host_key_change_host) {
try {
const keys = JSON.parse(xhr.responseText)["known-hosts"];
if (keys) {
debug("send_login_request(): got updated known-hosts for changed host keys of", ssh_host_key_change_host, ":", keys);
set_hostkeys(ssh_host_key_change_host, keys);
ssh_host_key_change_host = null;
} else {
debug("send_login_request():", ssh_host_key_change_host, "changed key, but did not get an updated key from response");
}
} catch (ex) {
console.error("Failed to parse response text as JSON:", xhr.responseText, ":", JSON.stringify(ex));
Comment on lines +1010 to +1011
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

}
}

if (xhr.statusText.startsWith("captured-stderr:")) {
show_captured_stderr(decodeURIComponent(xhr.statusText.replace(/^captured-stderr:/, '')));
} else if (xhr.statusText.indexOf("authentication-not-supported") > -1) {
Expand All @@ -959,7 +1026,17 @@ function debug(...args) {
} else if (xhr.statusText.indexOf("unknown-host") > -1) {
host_failure(_("Refusing to connect. Host is unknown"));
} else if (xhr.statusText.indexOf("invalid-hostkey") > -1) {
host_failure(_("Refusing to connect. Hostkey does not match"));
/* ssh/ferny/beiboot immediately fail in this case, it's not a conversation;
* ask the user for confirmation and try again */
if (ssh_host_key_change_host === null) {
debug("send_login_request(): invalid-hostkey, trying again to let the user confirm");
ssh_host_key_change_host = login_machine;
call_login();
} else {
// but only once, to avoid loops; this is also the code path for cockpit-ssh
debug("send_login_request(): invalid-hostkey, and already retried, giving up");
host_failure(_("Refusing to connect. Hostkey does not match"));
}
} else if (is_conversation) {
login_failure(_("Authentication failed"));
} else {
Expand Down Expand Up @@ -1038,6 +1115,18 @@ function debug(...args) {
localStorage.setItem(application + 'login-data', str);
/* Backwards compatibility for packages that aren't application prefixed */
localStorage.setItem('login-data', str);

/* When confirming a host key with cockpit-beiboot, login-data contains the known_hosts pubkey;
* update our database */
if (login_data_host) {
const hostkey = response["login-data"]["known-hosts"];
if (hostkey) {
console.debug("setup_localstorage(): updating known_hosts database for deferred host key for", login_data_host, ":", hostkey);
set_hostkeys(login_data_host, hostkey);
} else {
console.error("login.js internal error: setup_localstorage() received a pending login-data host, but login-data does not contain known-hosts");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

}
}
}

/* URL Root is set by cockpit ws and shouldn't be prefixed
Expand Down
Loading