ChromeOS allows SSH authentication through either the built-in Terminal application or the Secure Shell extension. The way this works is pretty cool! An “HTML terminal”, named hterm provides the terminal emulator. An SSH client does the rest.

# A tale of two clients

You might now be asking: Wait, how do you get an SSH client running in the browser?

Here is where things get complicated:

  • Back in the day, the SSH client was nassh, an SSH client built for Chrome’s Native Client (NaCl).
  • In 2020, NaCl has been deprecated in favor of WASM. wassh, a WASM SSH client, replaced nassh.

Secure Shell and the Terminal will default to the newer wassh, unless forced to pick nassh through the --ssh-client-version=pnacl relay server option.

# Yubikey support

Now things get messy. I generate my SSH identities on a Yubikey. Terminal and Secure Shell support Yubikeys thanks to the Smart Card Connector app and the --ssh-agent=gsc relay option.

Unfortunately, wassh never worked for me on this configuration, failing with the following error:

A screenshot of the Terminal application failing to establish an SSH connection.

Program exited with status code [object Object].

For a while, I could work around the issue by using the old SSH client through --ssh-client-version=pnacl. New Chromebooks, however, do not ship NaCl anymore. This makes Terminal hang on “Loading pnacl program…” when trying to start the client. Unsurprisingly, this breaks the workaround.

# Patching wassh

This left me with no other solution than to debug the wassh client. I started by opening the developer console ((Ctrl+Shift+J)) in a failing Terminal and diving into the 🐇 rabbit hole that starts with this:

terminating process due to runtime error: Error while handling syscall: TypeError: onSuccess is not a function
TypeError: onSuccess is not a function
    at SshAgentStream.asyncWrite (chrome-untrusted://terminal/js/nassh_stream_sshagent.js:105:3)
    at UnixSocket.write (chrome-untrusted://terminal/wassh/js/sockets.js:1523:26)
    at RemoteReceiverWasiPreview1.handle_fd_write (chrome-untrusted://terminal/wassh/js/syscall_handler.js:299:15)
    at Background.onMessage_syscall (chrome-untrusted://terminal/wasi-js-bindings/js/process.js:293:40)
    at Background.onMessage (chrome-untrusted://terminal/wasi-js-bindings/js/process.js:276:28)

Through some JavaScript abominations and Chrome local overrides, I managed to work around one JavaScript issue after the other. Then, a helpful answer on the chromium-hterm Google Group pointed me to a stale pending change that fixes the issue. I applied it to the overrides and verified that it works.

# Profit?

Kinda. As long as the changes are pending, SSH from the Terminal requires to:

  1. Configure the connection.
  2. Launch the client.
  3. See it fail.
  4. Open the developer console, which loads the overrides.
  5. Force-reloading the client with Ctrl+Shift+R.

If all the steps went correctly, this should result in the Yubikey PIN prompt. It works, and it is great that it does since it provides a fallback in case something else breaks and you need SSH from ChromeOS. But the UX sucks.

It might be possible to make things less wonky by packaging a patched version of the Secure Shell extension and using it, but I have not tried that. But that might be just a temporary stopgap: ChromeOS will be deprecating Chrome Apps, including the Smart Card Connector. Instead, I think that a better solution can now just rely on Fido2 for SSH. That would allow re-using the browser support for WebAuthn and remove the need for the connector altogether.