Skip to content

Commit

Permalink
beiboot: Handle ssh host key prompts
Browse files Browse the repository at this point in the history
Stop treating host key prompts as generic conversation messages. We want
the UI to handle them properly, with some verbiage/buttons and the
recipe for validating host keys, instead of letting the user type "yes".
The login page recognizes these through the presence of the `host-key`
authorize field (and irritatingly, an extra `default` field with the
actual value).

We can't use ferny's builtin `do_hostkey()` responder for this, as that
requires `ferny.Session(handle_host_key=True)`, and that API is not
flexible enough to handle our ssh command modifications and the extra
beiboot_helper handler. This needs some bigger redesign, see #19668.

So just recognize and parse SSH's host key prompts, and rely on our
integration tests to spot breakage in future distro releases.

This enables the login page's host key localstorage mechanism, so adjust
TestLogin.testLoginSshBeiboot to only expect the host key on the first
login attempt.
  • Loading branch information
martinpitt committed Aug 28, 2024
1 parent 823f375 commit c022968
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 12 deletions.
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
18 changes: 17 additions & 1 deletion src/cockpit/beiboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import importlib.resources
import logging
import os
import re
import shlex
import tempfile
from pathlib import Path
Expand Down Expand Up @@ -166,12 +167,27 @@ async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[st
# Let's avoid all of that by just showing nothing.
return None

# FIXME: is this a host key prompt? This should be handled more elegantly,
# see https://github.com/cockpit-project/cockpit/pull/19668
fp_match = re.search(r'\n(\w+) key fingerprint is ([^.]+)\.', prompt)
# let ssh resolve aliases, don't use our original "destination"
host_match = re.search(r"authenticity of host '([^ ]+) ", prompt)
args = {}
if fp_match and host_match:
# login.js do_hostkey_verification() expects host-key to be "hostname keytype key"
# we don't have acces to the full key, but the fingerprint is good enough
args['host-key'] = f'{host_match.group(1)} {fp_match.group(1)} {fp_match.group(2)}'
# very oddly named, login.js do_hostkey_verification() expects the fingerprint here
args['default'] = fp_match.group(2)

challenge = 'X-Conversation - ' + base64.b64encode(prompt.encode()).decode()
response = await self.router.request_authorization(challenge,
timeout=None,
messages=messages,
prompt=prompt,
hint=hint,
echo=False)
echo=False,
**args)

b64 = response.removeprefix('X-Conversation -').strip()
response = base64.b64decode(b64.encode()).decode()
Expand Down
5 changes: 3 additions & 2 deletions test/verify/check-client
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ Command = /usr/bin/env python3 -m cockpit.beiboot
b.wait_not_visible("#recent-hosts-list")
b.set_val("#server-field", "10.111.113.2")
b.click("#login-button")
b.wait_in_text("#conversation-group", "authenticity of host '10.111.113.2")
b.set_val("#conversation-input", "yes")
# accept unknown host key
b.wait_in_text("#hostkey-message-1", "10.111.113.2")
b.wait_in_text("#hostkey-fingerprint", "SHA256:")
b.click("#login-button")
b.wait_text("#conversation-prompt", "[email protected]'s password: ")
b.set_val("#conversation-input", "foobar")
Expand Down
31 changes: 25 additions & 6 deletions test/verify/check-static-login
Original file line number Diff line number Diff line change
Expand Up @@ -971,17 +971,17 @@ Command = /usr/bin/env python3 -m cockpit.beiboot
""", append=True)
m.start_cockpit()

def try_login(user, password, server=None):
def try_login(user, password, server=None, expect_hostkey=False):
b.open("/")
b.set_val('#login-user-input', user)
b.set_val('#login-password-input', password)
b.click("#show-other-login-options")
b.set_val("#server-field", server or my_ip)
b.click("#login-button")
# ack unknown host key; FIXME: this should be a proper authorize message, not a prompt
b.wait_in_text("#conversation-prompt", "authenticity of host")
b.set_val("#conversation-input", "yes")
b.click("#login-button")
if expect_hostkey:
b.wait_in_text("#hostkey-message-1", my_ip)
b.wait_in_text("#hostkey-fingerprint", "SHA256:")
b.click("#login-button")

def check_no_processes():
m.execute(f"while pgrep -af '[s]sh .* {my_ip}' >&2; do sleep 1; done")
Expand All @@ -996,9 +996,16 @@ Command = /usr/bin/env python3 -m cockpit.beiboot
b.logout()
check_no_processes()

def check_store_hostkey(expected: str) -> None:
db_hostkey = b.eval_js(f'JSON.parse(window.localStorage.getItem("known_hosts"))["{my_ip}"]')
self.assertEqual(db_hostkey, expected)

# successful login through SSH
try_login("admin", "foobar")
try_login("admin", "foobar", expect_hostkey=True)
check_session()
# wrote full host key into session storage
real_hostkey = m.execute(f"ssh-keyscan -t ssh-ed25519 {my_ip}").strip()
check_store_hostkey(real_hostkey)

# wrong password
try_login("admin", "wrong")
Expand All @@ -1012,6 +1019,18 @@ Command = /usr/bin/env python3 -m cockpit.beiboot
try_login("admin", "foobar", server=f"other@{my_ip}")
check_session()

# non-matching host key
bad_key = "172.27.0.15 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINKEIUL5s7ebg5Y6JdYNq4+mAaaaaaP1VRBDUiVdHT3R"
b.eval_js(f"""window.localStorage.setItem('known_hosts', '{{"{my_ip}": "{bad_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()

# confirmation replaces (not amends) known key
check_store_hostkey(real_hostkey)


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

0 comments on commit c022968

Please sign in to comment.