Knocking, but encrypted
A reliable byte stream is a lovely thing to type a password into — and a catastrophe, because anyone on the wire can read it. SSH turns that raw stream into a channel that's private, tamper-evident, and sure of who it's talking to. Alquist does it from scratch, with no libraries to trust and none to attack.
Everyone reaches for a library here. We didn't. Not out of bravado, but budget: the whole system must fit on a floppy, and a dependency you didn't write is a dependency you can't fully trust, can't fully audit, and can't rebuild from knowledge. So every curve, every cipher, every hash lives in the tree, small enough to read in an afternoon.
What SSH actually is
Strip away the folklore and SSH is three layers stacked on a TCP connection:
- Transport (RFC 4253): agree on a shared secret nobody watching can derive, prove the server is who it claims, and switch the stream to authenticated encryption.
- User authentication (RFC 4252): now that the pipe is private, prove you are allowed in — by password or public key.
- Connection / channels (RFC 4254): multiplex a session inside the encrypted pipe — for us, one channel carrying a shell.
Chapters 6 and 7 built the reliable stream. Chapter 8 built the door a program opens it through. SSH is the first serious program to walk through that door.
Four primitives, all ours
The cryptographic core (the ssh_crypto module, being ported into
src/crypto/) is deliberately tiny — exactly the modern set OpenSSH uses
by default, and nothing more:
- X25519 — elliptic-curve Diffie-Hellman for the key exchange. Both sides send a public point; both compute the same shared secret; a watcher learns nothing.
- Ed25519 — the server signs the exchange with its host key, so the client can prove it's not talking to an impostor.
- ChaCha20-Poly1305 — authenticated encryption for every packet after key exchange: confidential and tamper-evident, no separate MAC to get wrong.
- SHA-256 / SHA-512 — the hashes underneath the exchange hash, key derivation, and Ed25519.
No OpenSSL, no libc, no bignum library. Each primitive carries its own known-answer self-test so a miscompiled curve fails loudly at boot rather than quietly on the wire. This is the "from scraps" philosophy made concrete: given the RFCs and the papers, the whole stack is re-derivable.
The handshake, step by step
RFC 4253 is a fixed dance, and Alquist follows it exactly:
- Version exchange. Each side sends a banner line
(
SSH-2.0-...). Plaintext, and folded into the exchange hash so it can't be quietly rewritten. - KEXINIT. Both list the algorithms they support; the negotiation picks
curve25519-sha256for key exchange and[email protected]for the cipher. - Key exchange. The X25519 public keys cross; each side computes the shared secret K and the exchange hash H over everything said so far plus the server's host key.
- Host authentication. The server signs H with its Ed25519 host key. The client verifies the signature against the key whose fingerprint it either remembers or is being asked to trust.
- NEWKEYS. Keys are derived from K and H; every packet after this point is encrypted and authenticated. The plaintext era is over.
Proof it's real: a conversation with OpenSSH
This is not a paper design. Our from-scratch implementation completed a full, standards- compliant handshake against production OpenSSH 10.2p1 — the same daemon on hundreds of millions of servers. In the run, our code:
- negotiated
curve25519-sha256+[email protected]against the real server; - computed the exchange hash and reproduced OpenSSH's advertised host-key fingerprint bit-for-bit — independent proof our SHA and key parsing match the world;
- verified the server's Ed25519 signature over the exchange hash with our own verifier;
- brought up the ChaCha20-Poly1305 channel and exchanged an
encrypted
SERVICE_REQUEST/SERVICE_ACCEPT.
If any primitive were off by a byte — a wrong-endian length, a curve constant transposed, a hash misfed — the fingerprint would not match and the signature would not verify. They did. That is the strongest kind of interoperability evidence there is: the other end is the reference implementation, and it was satisfied.
From transport to a shell
With the pipe private, the last two layers are comparatively small, and they connect directly to work happening in parallel elsewhere in the system:
- User authentication reads its expected credentials — a password
hash or an authorized public key — from the Alquist config file
(
/sd/etc/alquist.conf), never from source. Secrets live on the SD card, not in the repository. - The session channel is bridged to the shell's source-agnostic terminal core: the same line editor that serves the UART serves an SSH session, because it was built to not care where its keystrokes come from. SSH is simply another input adapter feeding the same shell.
So the end state is the one the whole series was pointed at: ssh to the Pi
and be met, over an encrypted channel, by a real Alquist prompt.
Try it
The transport is exercised today on the host, driving the real interop handshake:
# In the SSH prototype sandbox: run our client against a live OpenSSH server.
make kex # negotiates, verifies the host signature, opens the encrypted channel
And the destination, once the in-kernel service completes its last mile:
ssh [email protected] # met by an encrypted Alquist shell
Gotchas worth remembering
- Crypto is expensive; the kernel is cooperative. A key exchange must be sliced across bounded passes so one handshake never freezes the single core — the same discipline as every other EL0 task.
- The wall still holds. SSH parses only bytes copied into its own EL0 buffers. A malformed packet can crash a session, never the network service or the kernel.
- From-scratch means responsible-for-scratch. Owning every primitive means owning constant-time care and known-answer tests. The upside — auditability, no supply chain, floppy-sized — is worth the duty, but the duty is real.
- No secrets in git. Host keys and password hashes live on the SD card; the repository carries only placeholders.
A stranger can now knock, prove who they are, and be let in — and the wire between us reveals nothing. From ARP asking a name to a shell behind real encryption, the stack stands. What's left is to make it observable, and then to make it mean under attack.