Skip to content

image: sshd offers mlkem768x25519-sha256 alone - #245

Merged
aledbf merged 2 commits into
mainfrom
ssh-mlkem
Nov 13, 2026
Merged

aledbf merged 2 commits into
mainfrom
ssh-mlkem

Conversation

@aledbf

@aledbf aledbf commented Nov 13, 2026

Copy link
Copy Markdown
Member

sshd in the image accepts one key exchange, mlkem768x25519-sha256, and refuses a client without ML-KEM (no matching key exchange method found).

Why (bench fleet, boot:ssh-first, 20 boots per row, alternated):

client → key exchange first login p50 a second login p50
OpenSSH 9.6 → sntrup761x25519 489 ms 104 ms
x/crypto/ssh → mlkem768x25519 461 ms 40 ms
x/crypto/ssh → curve25519 461 ms 39 ms

ML-KEM is post-quantum and costs the same as curve25519; sntrup761 (what OpenSSH 8.5–9.8 negotiates) costs ~28 ms on the first login and ~60 ms on every later one. Runs: 43463472762, 43479854381.

Client contract (in image/README.md): OpenSSH ≥ 9.9, or golang.org/x/crypto/ssh ≥ v0.38.0 built with Go ≥ 1.24. The ssh2 npm package (≤ 1.17.0) has no ML-KEM.

Do not merge before the clients move: open-remote-ssh uses ssh2 and would stop connecting; spin ssh uses the user's OpenSSH and needs ≥ 9.9.

Also: boot:ssh-first now logs in with x/crypto/ssh over vsock (the runners' OpenSSH 9.6 is refused) and counts whether a curve25519-only login got in: 0 of 5 on this image, locally. boot/vsockproxy is removed; boot:waits takes FLAGS.

🤖 Generated with Claude Code

aledbf and others added 2 commits November 13, 2026 15:19
A client with no ML-KEM is refused rather than downgraded. ML-KEM is
post-quantum and on the bench fleet cost what curve25519 did - a login
40 against 39 ms (boot:ssh-first, lab run 43479854381) - while
sntrup761x25519, what OpenSSH 8.5-9.8 picks, took 104. A client needs
OpenSSH 9.9 or later, or x/crypto/ssh v0.38.0 or later with Go 1.24;
image/README.md says so and who that leaves out. build.sh reads the
drop-in back from the finished filesystem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runners' OpenSSH is 9.6, which has no ML-KEM, and the image's sshd
now refuses it. The probe dials vsock itself and logs in with
x/crypto/ssh offering mlkem768x25519-sha256 only - what spin's Go
clients negotiate by default - so vsockproxy, its ProxyCommand, goes. A
column counts the boots where a login offering only curve25519 got in:
none on this image, every one on a release from before it.

The rows that answered on the bench fleet are recorded beside the one
left. boot:waits takes FLAGS, so TARGET=sshd-vsock.socket can be
measured with a vsock device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aledbf
aledbf marked this pull request as ready for review November 13, 2026 18:30
@aledbf
aledbf merged commit 5e0cc2f into main Nov 13, 2026
7 checks passed
@aledbf
aledbf deleted the ssh-mlkem branch November 13, 2026 18:30
aledbf added a commit to spin-stack/open-remote-ssh that referenced this pull request Nov 13, 2026
A workspace's sshd will accept one key exchange, mlkem768x25519-sha256 (spin-stack/spin-machine#245),
and ssh2 has no ML-KEM, so the connection is the system's ssh:

- one ControlMaster, carried by `spin tunnel <session>` as its ProxyCommand: the session is
  carried once, so everything rides on that login - the server's start as an exec through it, and
  each connection VS Code makes to the server as `ssh -W` through it;
- held to the claim's host key as the one line of a known_hosts of its own, StrictHostKeyChecking
  yes, no global file; logged in with the certificate alone (CertificateFile, IdentitiesOnly,
  BatchMode); -F /dev/null, so nothing of the person's config changes what it is; and
  -o KexAlgorithms=mlkem768x25519-sha256;
- `ssh -V` is asked first, and an OpenSSH older than 9.9 refused in VS Code before the CLI is run,
  naming the version found, the one needed and why, as the spin CLI does. An unreadable banner is
  let through with a line in the log.

Windows is refused up front: there is no spin CLI for it, and OpenSSH for Windows has no
ControlMaster. LogLevel is INFO, not ERROR: ssh says "no matching key exchange method found" at
INFO, and that refusal is the one this change is about.

ssh2, its types, its patch and the test of its internals go. The tests run OpenSSH on both ends:
a real sshd set up as a workspace's (certificates only, ML-KEM alone), which the connection logs in
to, refuses another principal's certificate and another host key, and cannot negotiate with an
sshd that has no ML-KEM; the resolve test drives the real ssh through a wrapper that records each
run and logs in as the test's user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant