Post-quantum by default: hybrid key exchange in GHOSTSHELL's SSH client
How GHOSTSHELL 0.2.0 chooses, orders, reports and enforces post-quantum key exchange on every SSH connection, and how that was tested.
A technical paper from the GHOSTSHELL project, describing GHOSTSHELL 0.2.0. It has not been peer reviewed.
Abstract
An SSH session recorded today is only as safe as its key exchange, and the client's preference list decides which exchange a server runs. GHOSTSHELL 0.2.0 offers every post-quantum SSH key exchange published as an RFC ahead of every classical method, by default and on every hop: mlkem768x25519-sha256, mlkem1024nistp384-sha384, mlkem768nistp256-sha256 and sntrup761x25519-sha512 under both of its names. This paper explains the order and the four policies, why a classical fallback remains, how the exchange hash and strict key exchange resist downgrades, how each tab reports the exchange that actually ran, which bypasses were closed, shared connections and weaker jump hosts, and which one can only be reported, proxy commands. It reports the evaluation against OpenSSH 10.5 and OpenSSL 3.5, and what remains untested or unbuilt.
1 Introduction and threat
An attacker who records an SSH session today and later runs Shor's algorithm on its classical key exchange decrypts everything in it; the companion paper Harvest now, decrypt later sets out that threat for network engineers. In SSH the client's list decides the key exchange: the server runs the first method in it that the server also supports [1]. A client that lists post-quantum methods last, or not at all, gets classical exchanges from servers that could have done better.
GHOSTSHELL is a terminal and session manager for Windows built on Tabby, whose profiles store algorithm lists alphabetically, so that with Tabby's defaults ML-KEM came last. GHOSTSHELL's 0.1 releases imposed an order and offered mlkem768x25519-sha256 first, the only post-quantum method their SSH library had, but could not say which method a server chose. Version 0.2.0 changes three things:
- it offers four post-quantum hybrid methods, under five names, ahead of every classical method, by default and on every hop;
- it shows the method each connection actually negotiated, and warns when it is classical;
- it closes two ways around the policy, shared connections and weaker jump hosts, and reports a third, proxy commands, which no client setting can reach.
2 Background
2.1 Negotiation and the exchange hash
Each side's KEXINIT lists its algorithms in order of preference. Unless both sides' first guesses agree, the client's list decides: the key exchange is the first in the client's list that the server also supports, and the same rule picks ciphers, MACs and compression [1]. The exchange hash H covers both version strings, both KEXINIT payloads, the server's host key and the exchange values. The server signs H with its host key, and the client checks the signature against the H it computed itself [1].
2.2 The hybrid methods
NIST describes hybrids as designed to stay secure while at least one component does [2]. RFC 10042 defines three ML-KEM hybrids for SSH [3] and RFC 9941 one NTRU Prime hybrid [4], all with one shape. The client sends C_INIT, a KEM public key followed by a classical ephemeral key; the server answers S_REPLY, a KEM ciphertext followed by its own classical key; both derive K = HASH(K_PQ ‖ K_CL) and encode it in H as a string [3] [4].
ML-KEM is FIPS 203's module-lattice KEM [5]; NIST recommends ML-KEM-768 as the default parameter set and places ML-KEM-1024 in its highest security category [2]. OpenSSH made mlkem768x25519-sha256 its default in 10.0 [6]. sntrup761 is Streamlined NTRU Prime [7], OpenSSH's default from 9.0 until that switch; OpenSSH added its RFC name in 9.9, and older releases know only sntrup761x25519-sha512@openssh.com [6]. Table 1 gives each method's parts and message sizes.
| Method | Post-quantum half | Classical half | Hash | C_INIT | S_REPLY | Defined in |
|---|---|---|---|---|---|---|
| mlkem768x25519-sha256 | ML-KEM-768 | X25519 | SHA-256 | 1,216 | 1,120 | RFC 10042 [3] |
| mlkem1024nistp384-sha384 | ML-KEM-1024 | ECDH P-384 | SHA-384 | 1,665 | 1,665 | RFC 10042 [3] |
| mlkem768nistp256-sha256 | ML-KEM-768 | ECDH P-256 | SHA-256 | 1,249 | 1,153 | RFC 10042 [3] |
| sntrup761x25519-sha512 | sntrup761 | X25519 | SHA-512 | 1,190 | 1,071 | RFC 9941 [4] |
| curve25519-sha256 | none | X25519 | SHA-256 | 32 | 32 | classical |
2.3 Why hybrids, and the Terrapin attack
A hybrid combiner of this kind stays secure while either half holds, given assumptions about the hash [8]; the classical half guards against a young post-quantum algorithm failing, as SIKE did against a classical attack [9]. Separately, the Terrapin attack let a network attacker silently delete the first encrypted packets of an SSH connection under ChaCha20-Poly1305 or CBC with encrypt-then-MAC, because sequence numbers were not reset when keys changed [10]. Its countermeasure, OpenSSH's strict key exchange, resets them after every NEWKEYS and ends the connection on any unexpected message during the first exchange; it is an IETF working-group draft, not yet an RFC [11].
3 Design
3.1 The preference order
Every policy puts the post-quantum methods it offers in the order of Table 2, ahead of every classical method; Compatible offers only those a profile ticks. The order matters only when a server supports several: an OpenSSH 10 server runs mlkem768x25519-sha256, and an older one that knows only sntrup761x25519-sha512@openssh.com runs that. Post-quantum names the policy does not know yet follow the known ones of their kind, still ahead of every classical method.
| # | Method | Why here |
|---|---|---|
| 1 | mlkem768x25519-sha256 | ML-KEM-768, NIST's default parameter set [2], with X25519; OpenSSH's default since 10.0 [6] |
| 2 | mlkem1024nistp384-sha384 | ML-KEM-1024 with P-384, the strongest hybrid, for servers that offer it; OpenSSH 10.5 does not |
| 3 | mlkem768nistp256-sha256 | ML-KEM-768 with P-256, for servers limited to NIST curves |
| 4 | sntrup761x25519-sha512 | Streamlined NTRU Prime with X25519, the only post-quantum method OpenSSH had before 9.9; not a NIST standard, so after ML-KEM |
| 5 | sntrup761x25519-sha512@openssh.com | The same method under the name OpenSSH used before 9.9 |
| – | mlkem1024-sha384 | ML-KEM-1024 alone, after every hybrid; not in this build's library, so never offered (section 6) |
3.2 Four policies
The policy is set globally in Settings → Security or per profile (Table 3). Quantum-safe preferred, the default, adds every hybrid even to profiles saved before a method existed, whose stored lists do not name it, and new profiles tick every post-quantum method. No policy offers a null cipher or MAC (none, clear), not even Legacy, and not when a profile ticks one.
| Policy | Key exchange | Ciphers and MACs | Host keys |
|---|---|---|---|
| Quantum-safe only | Every post-quantum method, nothing classical | AES-256-GCM, ChaCha20-Poly1305; SHA-2 encrypt-then-MAC | The profile's, without ssh-rsa and ssh-dss, plus the post-quantum hybrids |
| Quantum-safe preferred (default) | Every post-quantum hybrid, then the profile's classical methods without SHA-1 | No CBC, AES-128/192 or SHA-1; AES-256-GCM first | As above |
| Compatible | The profile's ticked methods, post-quantum first | The profile's | The profile's |
| Legacy | Everything the library supports, post-quantum first, SHA-1 last | CBC and HMAC-SHA1 included | ssh-rsa and ssh-dss included |
3.3 Why the classical fallback stays
Under Quantum-safe preferred, a server with no post-quantum method still gets a classical exchange. Refusing would cut engineers off from equipment whose SSH server they cannot change, and NIST expects both kinds of algorithm to be fielded side by side for interoperability during the transition [2]. The fallback is kept, but never silent (section 3.5), and Quantum-safe only is one setting away for hosts that must not fall back; it refuses a classical-only server and lists what the server offers.
3.4 Downgrade resistance
An active attacker could try to strip the post-quantum names from the client's KEXINIT so that the server picks a classical method. Both KEXINIT payloads are inside H, which the server signs [1] [3]: the server signs the H computed from the KEXINIT it received, the client checks against the H computed from the one it sent, and an edit makes them differ, so the handshake fails (Figure 1). Stripping the methods therefore takes the server's host key, a forged host-key signature in real time, or the user accepting an unknown key. The library also appends kex-strict-c-v00@openssh.com to every list, so strict key exchange runs whenever the server supports it [11], and every policy sorts AES-256-GCM, which Terrapin does not affect [10], ahead of ChaCha20-Poly1305.
mlkem768x25519-sha256 exchange as RFC 10042 and RFC 4253 define it [3] [1]. Both algorithm lists are inside H, which the host key signs.3.5 Reporting what was negotiated
The policy says what was offered; only the server's choice says what protects the session. After the first key exchange, before any password prompt, the terminal prints what was negotiated (Figure 2). Older algorithms in use, such as SHA-1, CBC or ssh-rsa, get a line of their own. Rekeys arrive through the library's negotiatedAlgorithms$ event, and the line is printed again only when the result changes. OpenSSH 10.1 added a comparable client warning for key agreement that is not post-quantum [6].
PQ Post-quantum key exchange: mlkem768x25519-sha256 · host key ssh-ed25519
! Classical key exchange: curve25519-sha256 · host key ssh-ed25519
! Not post-quantum: traffic recorded from this connection could be decrypted later by a quantum computer.
The server supports none of the post-quantum methods GHOSTSHELL offered (mlkem768x25519-sha256, …).
connectionSecurity.ts words them (the warning is wrapped and shortened here).The tab's toolbar badge reads Post-quantum, Classical key exchange or Key exchange not reported, and Connection security…, in the tab's menu, lists policy, key exchange, host key, ciphers, MACs and compression for every hop. The state comes from the library's report; with a library that cannot report, as the 0.1 binding could not, GHOSTSHELL claims post-quantum only when nothing else was offered.
3.6 Closing the bypasses
- Shared connections. A profile can reuse an open connection to the same host. GHOSTSHELL 0.2.0 shares one only when address, user, proxy, policy and every algorithm list match, so a Quantum-safe only tab never rides a connection negotiated under a weaker policy.
- Weaker jump hosts. A jump host's own connection carries the bastion login and names the destination, with the target's connection inside it. Its policy is raised to at least the target's, recursively along a chain, and the terminal names the profile that required it.
- Proxy commands. The proxy program makes its own connection, which no GHOSTSHELL policy reaches, and Windows' built-in
ssh.exe(OpenSSH 9.5) has no post-quantum method at all. GHOSTSHELL says so in the terminal, the profile's proxy-command field and Connection security…, and suggests a jump host; launching WinSCP gets the same notice.
3.7 Host keys
Host keys only have to hold during the handshake, but an attacker with a quantum computer at that moment could forge a classical one. For a server not yet trusted, GHOSTSHELL offers the composite ML-DSA-44 + Ed25519 first, under its IETF name ssh-mldsa44-ed25519 [12] and then as ssh-mldsa44-ed25519@openssh.com, the name of OpenSSH 10.4 and 10.5 [6]; then ML-DSA alone, from FIPS 204 [13] under the names of draft-sfluhrer-ssh-mldsa [14]; then Ed25519, ECDSA and RSA with SHA-2. A trusted server keeps its known key type first, and the prompt says when a known server presents a key of a new type. The library refuses a server whose host key is not of the negotiated type, so the reported host key algorithm is the one that signed H.
4 Implementation
4.1 The SSH stack
The app's SSH sessions run on russh through a native Node binding, and 0.2.0 builds its own: a fork of russh-napi, packaged as npm russh 0.2.0-ghostshell.1, on vendored forks of russh 0.65.1, the release line that fixes the October 2026 russh advisories, and ssh-key 0.7.0-rc.11, for Windows x64 only. The new post-quantum code uses RustCrypto's ml-kem 0.3.4, sntrup-kem 0.1.0-rc.0 and ml-dsa 0.1.2, while mlkem768x25519-sha256 keeps russh's own implementation. Each fork's GHOSTSHELL-CHANGES.md lists every change from upstream.
4.2 One generic hybrid
The russh fork adds one generic exchange in src/kex/hybrid_pq.rs, HybridKex<K, C, D>, over a post-quantum KEM, a classical Diffie–Hellman and a hash (Figure 3); the @openssh.com name maps to the same type.
sntrup761x25519-sha512 HybridKex<Sntrup761, X25519, Sha512>
mlkem768nistp256-sha256 HybridKex<MlKem<MlKem768>, NistEcdh<NistP256>, Sha256>
mlkem1024nistp384-sha384 HybridKex<MlKem<MlKem1024>, NistEcdh<NistP384>, Sha384>
Every received value is checked before use. The init message must be exactly one string with nothing after it, and each part must have exactly its length. An ML-KEM encapsulation key must pass FIPS 203's modulus check [5]; a NIST-curve point must lie on the curve and not be the identity, compressed or not, as RFC 10042 allows [3]; and an all-zero X25519 result is refused, as russh does for curve25519-sha256. The session only ever sees the combined K = D(K_PQ ‖ K_CL), never a half.
4.3 Reporting through the binding
A kex_done handler stores the negotiated names after the first exchange, which russh runs only after the host key was accepted, and after every rekey. negotiatedAlgorithms() returns the key exchange, host key, both ciphers, both MACs and both compression methods, with the MACs reported as <implicit> for AEAD ciphers, and negotiatedAlgorithms$ emits after each completed exchange. The binding appends ext-info-c and kex-strict-c-v00@openssh.com to whatever list the app passes.
4.4 The app side
securityPolicy.ts computes each connection's lists from the profile, the policy, what the library supports and the host's known key types, and builds the reuse key; connectionSecurity.ts classifies what negotiatedAlgorithms() reports as post-quantum, classical or unknown; session/ssh.ts applies the jump-host floor, connects, and prints the result before authentication starts. Both pure modules are unit-tested without the native binding.
4.5 ML-DSA keys
ML-DSA lives in the ssh-key fork's src/mldsa.rs, so host-key proofs and user logins use it unchanged. For the composite, both halves sign one representative, M′ = Prefix ‖ Label ‖ 0 ‖ SHA-512(M), with the prefix CompositeAlgorithmSignatures2025 and the label COMPSIG-MLDSA44-Ed25519-SHA512, which is also ML-DSA-44's context string [12]. The signature is the 2,420-byte ML-DSA-44 signature followed by the 64-byte Ed25519 one [13], and it verifies only if both halves do, with Ed25519 checked strictly. A signature must name the key's own algorithm, so a composite key never accepts a bare ML-DSA signature; pure ML-DSA uses an empty context [14]. Private keys are stored as 32-byte seeds, as OpenSSH stores them, and the ML-DSA half of each signature is made with fresh randomness.
5 Evaluation
All checks ran on Windows 11 x64 over 127.0.0.1, with keys made fresh in temporary folders. OpenSSH got an empty configuration, its own known-hosts files and no agent, and every run ended by checking that the user's ~/.ssh was unchanged. The OpenSSH, OpenSSL and unit runs in Table 4 and the binding run under Node were repeated independently after the library was finished; the binding run inside Electron and the Rust tests are as the library work reported them.
| Suite | What it does | Result |
|---|---|---|
| OpenSSH interop | OpenSSH 10.5p1 as the client against the fork's test server: each shared key exchange with ML-DSA-44 + Ed25519 and Ed25519 host keys pinned in known_hosts, a user-key login and a command; ECDSA and RSA host keys; a server-initiated rekey per post-quantum method, OpenSSH logging two exchanges; eight cipher and MAC sets; an unknown host key refused; SSHSIG both ways | 60 of 60 |
| Binding | The installed binding through SSHClient.connect(): every key exchange and host key type, fingerprints against ssh-keygen's, negotiatedAlgorithms() against the server's report, rekeys, ML-DSA user keys, SFTP, forwarding, and mlkem1024nistp384-sha384 inside mlkem768x25519-sha256 through a jump host | 94 of 94 in Electron 43.7.0's runtime; 106 of 106 under Node, 12 of them checks against the 0.1 binding's interface |
| OpenSSL cross-check | OpenSSL 3.5 derives ML-DSA-44, -65, -87 and composite public keys from the fork's seeds, verifies its signatures, signs messages it must verify, and changed messages and broken Ed25519 halves are refused | 28 of 28 |
| Rust tests | 217 library tests, among them RFC 9941's shared-secret vector, published sntrup761 vectors, ML-KEM-768 and ML-KEM-1024 against OpenSSL both ways, and the length, point and modulus rejections | All passed |
| App unit suite | Policy order and lists, reporting, jump-host floors, connection sharing, the proxy-command notice; a scripted loopback server that records the client's KEXINIT; handshakes against the test server | 245 tests: 244 pass, 1 skipped (pure ML-KEM-1024) |
A completed OpenSSH session pins the encoding: one different byte in C_INIT, S_REPLY, K or H would change the session keys and fail the connection. In these runs OpenSSH was the client and the fork the server; the client side of each hybrid was checked against the fork's own server and, for ML-KEM, against OpenSSL. In the scripted-server tests, a server offering only one of the five post-quantum methods got that method, and Quantum-safe preferred fell back to curve25519-sha256 only when a server offered none.
6 Limitations and what is not done
- Pure
mlkem1024-sha384is not built. The CNSA 2.0 profile for SSH requires it [15], but its appendix A.1.2 putsstring K_PQin the exchange hash while A.1.3 says K_PQ is an mpint, and only SSH_MSG_KEX_KEM_INIT has a message number; draft-harrison-sshm-mlkem-02, its basis, reads both ways too [16]. Implementations reading it differently cannot connect, so this build never offers it. ssh-mldsa87-p384is not built. The composite draft gives its ML-DSA-87 signature as 4,896 bytes [12], FIPS 204's size for an ML-DSA-87 private key, where the signature is 4,627 [13], and it leaves the ECDSA hash unspecified.- Not tested against peers: GHOSTSHELL's client against an OpenSSH server or any other server;
mlkem1024nistp384-sha384against another implementation;ssh-mldsa44-ed25519under its IETF name and pure ML-DSA against OpenSSH 10.6 or any other SSH implementation; an ML-DSA user-key login to a real sshd. - Not exercised: SOCKS, HTTP and proxy-command transports, remote and X11 forwarding, agent login, ML-DSA certificates and agent-held ML-DSA keys. The host-key type check is covered by code review only.
- The terminal line, badge and Connection security… were checked through unit tests of the code that produces them, not in the running app. Host certificates are checked as bare keys, not as certificates.
ml-kem,sntrup-kemandml-dsahave not been independently audited, andsntrup-kemand ssh-key are release candidates. ML-KEM code must run in constant time [17]; the fork requiresml-kem0.3.4 or later, after side-channel fixes in 0.3.3 and 0.3.4. The binding is built for Windows x64 only.
7 Conclusion
Post-quantum by default in SSH comes down to order, reporting and coverage. GHOSTSHELL 0.2.0 puts every post-quantum key exchange published as an RFC ahead of every classical method, keeps the classical fallback but reports it before any password is typed, carries the policy along jump chains and shared connections, and names the paths it cannot cover. OpenSSH 10.5 completed sessions with the library for every post-quantum method the two share. What remains is in section 6: testing the client against real servers and equipment, and the two methods whose drafts are not yet consistent.
References
- [1]T. Ylonen and C. Lonvick (Ed.). The Secure Shell (SSH) Transport Layer Protocol. RFC 4253, January 2006.
- [2]D. Moody, R. Perlner, A. Regenscheid, A. Robinson and D. Cooper. Transition to Post-Quantum Cryptography Standards. NIST Internal Report 8547, final, October 2026. doi:10.6028/NIST.IR.8547.
- [3]P. Kampanakis, D. Stebila and T. Hansen. Post-Quantum/Traditional Hybrid Key Exchange with the Module-Lattice-Based Key-Encapsulation Mechanism for Use in SSH. RFC 10042, August 2026.
- [4]M. Friedl, J. Mojzis and S. Josefsson. Secure Shell (SSH) Key Exchange Method Using Hybrid Streamlined NTRU Prime sntrup761 and X25519 with SHA-512: sntrup761x25519-sha512. RFC 9941, April 2026.
- [5]National Institute of Standards and Technology. Module-Lattice-Based Key-Encapsulation Mechanism Standard. FIPS 203, August 2024. doi:10.6028/NIST.FIPS.203.
- [6]OpenSSH project. OpenSSH release notes: 9.0 (8 April 2022), 9.9 (19 September 2024), 10.0 (9 April 2025), 10.1 (6 October 2025), 10.4 (6 July 2026) and 10.6 (6 October 2026).
- [7]D. J. Bernstein, C. Chuengsatiansup, T. Lange and C. van Vredendaal. NTRU Prime: Reducing Attack Surface at Low Cost. Selected Areas in Cryptography (SAC 2017), LNCS 10719, pp. 235–260. doi:10.1007/978-3-319-72565-9_12.
- [8]N. Bindel, J. Brendel, M. Fischlin, B. Goncalves and D. Stebila. Hybrid Key Encapsulation Mechanisms and Authenticated Key Exchange. Post-Quantum Cryptography (PQCrypto 2019), LNCS 11505, pp. 206–226, 2019. doi:10.1007/978-3-030-25510-7_12.
- [9]W. Castryck and T. Decru. An Efficient Key Recovery Attack on SIDH. Advances in Cryptology (EUROCRYPT 2023), LNCS 14008, pp. 423–447, 2023. doi:10.1007/978-3-031-30589-4_15.
- [10]F. Bäumer, M. Brinkmann and J. Schwenk. Terrapin Attack: Breaking SSH Channel Integrity By Sequence Number Manipulation. 33rd USENIX Security Symposium (USENIX Security 2024), pp. 7463–7480.
- [11]D. Miller. SSH Strict KEX extension. Internet-Draft draft-ietf-sshm-strict-kex-02, July 2026, work in progress.
- [12]D. Miller. Post-Quantum Composite Signatures in SSH. Internet-Draft draft-ietf-sshm-composite-sigs-00, August 2026, work in progress.
- [13]National Institute of Standards and Technology. Module-Lattice-Based Digital Signature Standard. FIPS 204, August 2024. doi:10.6028/NIST.FIPS.204.
- [14]S. Fluhrer, R. Petrov and R. Appel. SSH Support of ML-DSA. Internet-Draft draft-sfluhrer-ssh-mldsa-08, August 2026, individual submission, work in progress.
- [15]A. Becker, C. Wynn and M. Jenkins. Commercial National Security Algorithm (CNSA) Suite 2.0 Profile for SSH. Internet-Draft draft-becker-cnsa2-ssh-profile-07, October 2026, individual submission, work in progress.
- [16]A. Harrison, A. Benhase and P. Kampanakis. Module-Lattice Key Exchange in SSH. Internet-Draft draft-harrison-sshm-mlkem-02, May 2026, individual submission, work in progress.
- [17]D. J. Bernstein, K. Bhargavan, S. Bhasin, A. Chattopadhyay et al. KyberSlash: Exploiting secret-dependent division timings in Kyber implementations. IACR Transactions on Cryptographic Hardware and Embedded Systems 2025(2):209–234. doi:10.46586/tches.v2025.i2.209-234.