GHOSTSHELL
Motion and effects on this site
Download 0.2.0

Making post-quantum visible: per-session indicators and warnings

Why GHOSTSHELL 0.2.0 shows the key exchange each SSH session actually negotiated rather than the one its policy offered, where it shows it, and what it does not show yet.

A technical paper from the GHOSTSHELL project, describing GHOSTSHELL 0.2.0. It has not been peer reviewed.

Fig. 0 GHOSTSHELL's papers and its reading list as a chart of stars, one row per topic and each work at its year. Accent arcs join this paper to the works its references cite, and a cursor visits them in date order, then this paper.All 19 key exchange names that GHOSTSHELL 0.2.0's SSH library implements were reported as the connection opened and matched the test server's own record; 15 rekeys that changed nothing printed nothing new.

Abstract

A security policy says what a client is willing to offer; the server's choice decides what protects a session, and the client learns that choice only from its SSH library. GHOSTSHELL 0.2.0 reads the negotiated algorithms from its library after the first key exchange and after every rekey, and shows them in four places: a line in the terminal before any password prompt, a badge on the tab, a dialog listing every hop, and a count in Settings → Security beside the stored policy. This paper explains the three states it uses and how they are decided, the words of the summary and of the warning for a classical exchange, why a tab through jump hosts reports its weakest hop, and what is still not shown. Measured on 127.0.0.1, the library's report matched the server's own record for all 19 key exchange names it implements, 15 rekeys printed nothing new, and with the 0.1 library, which cannot report, the app said so instead of guessing.

1Introduction

In SSH the client lists the methods it will accept, in order, and the first one on that list that the server also supports is used [1]. A policy therefore says what was offered, not what runs. Under Quantum-safe preferred, GHOSTSHELL's default, a server without a post-quantum method gets a classical exchange, and a recording of that session can be decrypted once a large enough quantum computer exists [2]. GHOSTSHELL 0.1 printed a PQ tag based on its policy, which said nothing about the server's choice; its SSH library could not report one.

GHOSTSHELL 0.2.0 shows the negotiated result instead, and five rules shaped how:

  • Truthful: never claim more than the library reported, or than the offer guarantees when it reports nothing.
  • Timely: say it before the user types a password.
  • Lasting: keep it visible after the terminal line has scrolled away.
  • Whole: cover every hop of a jump chain, and name the weakest.
  • Quiet: say it again only when something changed.

OpenSSH reached a similar view: from 10.1 its client warns when a connection's key agreement is not post-quantum, and from 10.6 its server logs such clients [3]. Post-quantum by default describes the policies and the SSH library; this paper is about what the user sees.

2Background

2.1What a session's protection depends on

After the key exchange, everything in a session, the login included, is encrypted with keys derived from its shared secret [1]. The key exchange method decides whether a recording can be decrypted later: a hybrid such as mlkem768x25519-sha256 [4] or sntrup761x25519-sha512 [5] is designed to stay secure while either of its halves does [2]. The host key decides who the server is, and only at connection time; the cipher and MAC protect the traffic itself, and a 256-bit cipher is not the weak point [2]. A useful report therefore names the key exchange first, then the host key, then the cipher, MAC and compression.

2.2Where the facts come from

The client learns the server's choice inside its SSH library. GHOSTSHELL's library, a fork of russh, keeps a record of what each completed key exchange negotiated: the method, the host key algorithm (a certificate type when the server presented a certificate), the cipher, the MAC in each direction, shown as <implicit> next to an AEAD cipher as OpenSSH prints it, and the compression in each direction. For the first exchange, the library stores the record only after the host key check has accepted the server, and before connect() returns; each rekey replaces it. The binding offers it as negotiatedAlgorithms(), null before the first exchange, and as negotiatedAlgorithms$, which emits after each one. The library also refuses a host key of any type but the negotiated one, so the host key named is the type that signed the handshake; that refusal was reviewed in the code but not exercised.

3Design

3.1Three states, decided conservatively

Each connection gets one of three states (Table 1). With a report, the state is the report's. Without one, the offer alone decides, and only where it leaves no doubt: if every method offered was post-quantum, whatever ran was post-quantum; if none was, the exchange was classical; otherwise the state is unknown. A connection is never shown as post-quantum because a post-quantum method came first in its offer. A method counts as post-quantum if the policy names it or if its name starts with a post-quantum family's (mlkem, sntrup, ntruprime and others), so a method a later library adds is not mistaken for classical.

Table 1. The three states and the badge each gives. "Report" is the library's record of what was negotiated.
ReportOfferStateBadge
A post-quantum methodAnyPost-quantumPost-quantum
A classical methodAnyClassicalClassical key exchange
NoneOnly post-quantum methodsPost-quantumPost-quantum
NoneNo post-quantum methodClassicalClassical key exchange
NoneBoth kindsUnknownKey exchange not reported

3.2The words

The summary names the state, the method and the host key in one line. A classical summary is followed by a warning in two parts (Figure 1). The first is the risk, the same every time: traffic recorded from this connection could be decrypted later by a quantum computer. The second is the reason, which decides who can fix it. Either the server supports none of the post-quantum methods GHOSTSHELL offered, which it lists, with the OpenSSH versions that offer one by default and a note that crypto policies, other servers and network equipment vary; or the profile's own policy offered none, with a pointer to the profile's Ciphers tab. The first reason sends the user to the server's administrator, the second to their own settings. An unknown state gets one sentence saying what would have made the exchange post-quantum. Algorithms that only Legacy or a profile's own ticks allow, such as SHA-1 key exchange, CBC, ssh-rsa or HMAC-SHA1 beside a non-AEAD cipher, get a line of their own.

PQ  Post-quantum key exchange: mlkem768x25519-sha256 · host key ssh-mldsa44-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, mlkem1024nistp384-sha384,
    mlkem768nistp256-sha256, sntrup761x25519-sha512,
    sntrup761x25519-sha512@openssh.com). OpenSSH servers offer one by default
    from version 8.9 (sntrup761) and 9.9 (ML-KEM), unless the system's crypto
    policy removes it; other SSH servers and network equipment vary.

?   Key exchange not reported by this SSH library: post-quantum methods were
    offered first, but a classical one may have been used · host key ssh-ed25519
?   The key exchange is post-quantum only if the server supports one of
    mlkem768x25519-sha256.
Figure 1. The terminal lines for a post-quantum connection, a classical one, and one whose library cannot report (the 0.1 library of Section 5), as the app words them. Each line's SSH prefix is left out and long lines are wrapped here at word boundaries.

3.3Where it shows, and when

Figure 2 places each surface in the life of a connection, and Table 2 lists them. The terminal line comes at the one moment the user is sure to be looking: after the key exchange and before authentication, so no password goes into a classical session without a warning on the screen. It stays in the scrollback. The badge keeps the answer in view for the life of the tab, on the terminal toolbar, which is pinned by default. The dialog holds what no line can: every hop, every algorithm and where the policy came from. Settings → Security puts the stored policy and the open tabs on separate rows, so the page never implies that a setting is in force where a server overruled it; Settings → SSH answers a different question, what this build can do.

When GHOSTSHELL reports a connection's key exchange Two lanes over time. On the wire: the offer, with version strings and algorithm lists; the key exchange, whose reply the server signs with its host key; NEWKEYS, after which the new keys are in use; the login, with a password or key; and later, at any time, a rekey. In GHOSTSHELL: the policy and the profile build the offer; during the key exchange the library verifies the server's signature with the host key; once both NEWKEYS messages have passed, a new or changed host key gets a trust prompt (a known one passes without), and only then is the record stored and the terminal line, badge and dialog appear; the password prompt comes after that line; after a rekey there is a new record, and the line is printed again only if it changed. On the wire Offer versions, lists Key exchange signed reply NEWKEYS new keys in use Login password or key Rekey later, any time In GHOSTSHELL policy and profile build the offer signature verified with the host key if new: prompt; record stored; line and badge password prompt comes after the line new record; line again if changed
Figure 2. When GHOSTSHELL 0.2.0 reports a connection's key exchange. The first record exists only after the host key is accepted, so a trust prompt for a new host cannot show it (Section 6).
Table 2. Where GHOSTSHELL 0.2.0 shows what an SSH connection negotiated.
WhereWhat it showsWhen
TerminalThe summary; the warning when the state is not post-quantum; older algorithms in use; the profile whose policy a jump host took; the proxy-command noticeAfter the first key exchange, before authentication; again only when a rekey changes the record
Tab badgeThe state as in Table 1, or the chain's case as in Table 3; a tooltip with each hop's summaryWhile the tab is connected
Connection security…For every hop: policy, summary, warning, server authentication, cipher, MAC, compression, older algorithms, the post-quantum methods offered, where the policy came fromFrom the badge or the tab's menu
Settings → Security"SSH policy", the stored setting, beside "SSH tabs open now", which counts open tabs by their weakest hop: post-quantum, classical or not reportedWhenever the page is open
Settings → SSHThe post-quantum methods and host keys this build implements, and whether its library reports negotiated algorithmsAlways

3.4Jump hosts: the weakest hop

A tab that goes through jump hosts has one connection per hop, and each negotiates on its own. The tab's state is its weakest hop's: classical if any hop is classical, else unknown if any hop did not report, else post-quantum. The badge names the case rather than averaging it away (Table 3). A post-quantum target behind a classical bastion reads Classical jump host, because the outer connection carries the bastion login and the target's address. A post-quantum chain through a proxy command reads Post-quantum inside proxy, in grey, because GHOSTSHELL cannot see the proxy's own connection. The tooltip lists each hop's summary, and the terminal names the profile whose policy a jump host was raised to.

Table 3. The badge of a tab with jump hosts or a proxy command.
TargetJump hosts and proxyBadge
Post-quantumAll post-quantum, no proxy commandPost-quantum
Post-quantumAt least one classicalClassical jump host
Post-quantumOne not reported, none classicalJump host not reported
Post-quantumAll post-quantum, through a proxy commandPost-quantum inside proxy
Classical or not reportedAnyThe target's own badge (Table 1)

3.5Shared connections and rekeys

Two tabs can share one connection. The second tab never saw the first one's lines, so it prints the shared connection's summary and warning itself, after "Sharing the open connection to". A rekey is a new key exchange inside the session, and the server could choose differently. After each one the library emits an event, and the app reads the record again and prints the summary only if the record changed, so a long session's routine rekeys add nothing to the terminal while a change of method would be announced. The badge and the dialog always show the latest record.

4Implementation

connectionSecurity.ts is a pure module with no native imports, unit-tested on its own. It reads negotiatedAlgorithms() by duck typing: a library without the method, a call that throws or rejects, and an answer of the wrong shape all mean "not reported", never an error, and a missing field reads as empty. It then assesses the state and writes every word the surfaces show. session/ssh.ts calls it right after connect() and before authentication, prints the result, and subscribes to negotiatedAlgorithms$ once authenticated. sshTab.component.ts derives the weakest hop and the badge from each hop's record, the dialog lists every hop's details, Settings → Security counts the open tabs' weakest hops, and Settings → SSH asks the library whether it can report at all. The host key in a summary comes from the record or, without one, from the type the host key check saw.

5Evaluation

5.1Method

The SSH library the app installed (russh 0.2.0-ghostshell.1) connected over 127.0.0.1 to the project's test server, which runs the same russh fork and answers an exec request with its own record of what it negotiated. The host keys (ML-DSA-44 + Ed25519, and Ed25519) were throwaway keys made in a temporary folder, and the password a random throwaway. What the library reported went through the app's own connectionSecurity.ts, as the app passes it. Four checks ran on 12 November 2026, on Windows 11 x64 with Node 24.22.2:

  1. Every key exchange name the library implements, 5 post-quantum and 14 classical (sntrup761 and curve25519 each have two), one connection each offering only that name: was the record there when connect() returned, did it match the server's record in method, host key, cipher, MACs and compression, was the state right, and was the record the same after password authentication?
  2. Three server-initiated rekeys for each of the five post-quantum names: did each event arrive, and how many lines would the app's rule print?
  3. A two-hop chain, the inner session reaching a second server through a forwarded channel of the first, with a classical and then a post-quantum outer hop.
  4. The 0.1 library (npm russh 0.1.38), which cannot report, under three policies against the same server, whose record says what really ran.

The badge for each chain follows the rule of sshTab.component.ts, applied to the measured states; the component itself was not run.

5.2Results

Table 4. Results of the four checks, and of the app's unit suite run the same day.
CheckResult
1. Record present when connect() returned19 of 19 names
1. Record equal to the server's19 of 19
1. StateRight for 19 of 19: 5 post-quantum, 14 classical
1. Record unchanged by authentication19 of 19
1. Older algorithms lineFor the 3 SHA-1 key exchanges, and no other method
2. Rekeys15 of 15 events; 0 lines printed again; median 5.8 to 33.2 ms per name from the request to the event
3. Classical outer hop, post-quantum innerWeakest hop classical: Classical jump host
3. Post-quantum outer and inner hopsPost-quantum
4. 0.1 library, Quantum-safe preferredKey exchange not reported; the server ran mlkem768x25519-sha256
4. 0.1 library, Quantum-safe onlyPost-quantum, the only kind offered; the server ran mlkem768x25519-sha256
4. 0.1 library, curve25519-sha256 onlyClassical key exchange; the server ran curve25519-sha256
4. Can the library report? (the check Settings → SSH makes)0.2.0 library: yes; 0.1 library: no
App unit suite245 tests: 244 pass, 1 skipped (pure ML-KEM-1024, not built)

The first 0.1 row is the case the conservative rule exists for: under the default policy the server did run a post-quantum method, and GHOSTSHELL still said it could not tell, because nothing it had could tell it. The rekey times include the exec request that started each rekey, so they bound how soon a changed record would reach the screen. One cost shows in Figure 1: the classical warning is 487 characters long, with five method names, and with its summary it fills 8 rows of an 80-column terminal. It appears only on classical connections, but it is long.

6What is not shown, and other limits

  • The screens themselves were not inspected. The states and words were checked through the app's modules, its unit tests and the runs above; the templates that place them, the badge, the dialog and the settings rows, were read but not looked at in the running app.
  • The trust prompt for a new host cannot show the key exchange, because the first record exists only after the host key is accepted. The decision to trust a new server comes before the line that says whether its session is post-quantum.
  • Other parts of the window show nothing. The HUD, the session tree and the host bar do not show the state, although the tab's state and records are exported for them. With the toolbar unpinned, the badge appears only on hover.
  • There is no history. Closed sessions leave no record of what they negotiated, nothing can be exported for an inventory, and Settings → Security counts only open tabs.
  • The warning cannot be acknowledged per host, so a team with many classical devices sees it on every connection; OpenSSH lets its users turn its own warning off with the WarnWeakCrypto option [3].
  • HTTPS shows its configuration, not its connections. The Node row of Settings → Security reads the TLS groups in force, not what each HTTPS connection negotiated; with the groups the packaged app logs, it reads "Post-quantum first: X25519MLKEM768, MLKEM1024; classical fallback · TLS 1.2+". The Chromium row is a fixed statement of Chromium's default.
  • Other programs' connections are out of sight. RDP runs in Windows' own client and a proxy command in its own program; Telnet and raw sockets are warned as unencrypted.
  • One chain case is understated. With a library that cannot report, a target that is not reported behind a classical jump host shows Key exchange not reported rather than naming the classical hop. The 0.2.0 library reported every connection in Section 5, so the case does not arise with it.
  • The new messages are in English only, and the badge's accessible name, its label followed by "Connection security details", has not been tried with a screen reader.

7Conclusion

A policy is a promise about the offer; only the server's choice is evidence about the session. GHOSTSHELL 0.2.0 takes that evidence from its SSH library after every key exchange, states it in the terminal before any password is typed, keeps it on the tab, adds every hop in one dialog, and sets the open tabs beside the stored policy in Settings. When the library cannot say, GHOSTSHELL says that, rather than counting an offer as a result. What remains is mostly reach: the parts of the window outside the tab, a record of sessions that have closed, and the connections other programs make.

References

  1. [1]T. Ylonen and C. Lonvick (Ed.). The Secure Shell (SSH) Transport Layer Protocol. RFC 4253, January 2006.
  2. [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. [3]OpenSSH project. OpenSSH release notes: 10.1 (6 October 2025) and 10.6 (6 October 2026).
  4. [4]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.
  5. [5]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.