Repository navigation
toyos-ssh: mlkem768x25519-sha256 first, the post-quantum hybrid of RFC 10042 (SSH stage 3) - #1023
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…C 10042 The server offers mlkem768x25519-sha256 before curve25519-sha256, so the switch of sshserver keeps the post-quantum exchange russh negotiates today. The method follows RFC 10042 section 2: C_INIT is an ML-KEM-768 encapsulation key followed by an X25519 key, S_REPLY the ciphertext followed by the server's X25519 key, and K = SHA-256(K_PQ || K_CL), encoded as a string in H and in the key derivation. The primitives are toyos-crypto's ML-KEM-768 and hybrid::for_ssh. A client's message 30 of the wrong length, an encapsulation key that fails FIPS 203 section 7.2's check, and an X25519 key that makes the zero secret are refused as Refusal::KeyExchange, which disconnects with KEY_EXCHANGE_FAILED as RFC 10042 section 2.1 requires. The classical method's wrong-length Q_C now takes that reason too, as RFC 8731 section 3 recommends, rather than PROTOCOL_ERROR. The server's KEXINIT changed, so every recording was made again with OpenSSH_10.3p1, with the stage 1 keys and seeds. Six negotiated the hybrid, and their client logs carry no post-quantum warning. A seventh, forced onto curve25519-sha256, keeps the classical method under the oracle. The test client runs either method, and the replay derives each method's K and keys itself and opens what OpenSSH sealed. Closes issues/toyos-ssh-offers-only-a-classical-key-exchange.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Review, round 1: #1023 at aa65c1f (mlkem768x25519-sha256 in toyos-ssh)Architecture. This is a good fit. The server's half of RFC 10042 is one more Protocol check against RFC 10042 §2, RFC 8731 §3 and RFC 4253 §7.2.
Evidence.
FindingsNo blockers. SUGGESTION SUGGESTION For the orchestrator (PR description): the body links evidence tree LAND |
Problem
toyos-sshoffered onlycurve25519-sha256. The shipped server, on russh, negotiatesmlkem768x25519-sha256with OpenSSH 9.9 and later. So switching sshserver totoyos-ssh(stage 2 ofissues/the-ssh-server-is-toyos-own-and-russh-is-gone.md) would have downgraded every login to a classical exchange. The owner ruled on 2026-10-22: "Use our own crypto where possible; we optimize and improve until done, but we can already use it."Result
mlkem768x25519-sha256first, thencurve25519-sha256. The chosen method is the first one in the client's list that the server runs (RFC 4253 §7.1), so OpenSSH 10.3 negotiates the hybrid.C_INITis an ML-KEM-768 encapsulation key followed by an X25519 key, 1184 + 32 bytes.S_REPLYis the ciphertext followed by the server's X25519 key.K = SHA-256(K_PQ || K_CL). It is encoded as astring, not anmpint, both in H and in the key derivation.toyos-crypto's ML-KEM-768 andhybrid::for_ssh.Refusal::KeyExchangereplacesKeyAgreement. It names its reason: a message 30 of the wrong length, an encapsulation key that fails FIPS 203 §7.2's modulus check, or an X25519 key that makes the zero secret. It disconnects withKEY_EXCHANGE_FAILED, as RFC 10042 §2.1 requires.Q_Con the classical method now takes that reason as well, as RFC 8731 §3 recommends. Before, it disconnected withPROTOCOL_ERROR.issues/toyos-ssh-offers-only-a-classical-key-exchange.mdis closed. The SSH track records stage 3 as landed, and the crypto track's consumer line is updated.Net production change: +75/−31 in
build/ssh/src. Tests: +178/−49, plus the re-recorded fixtures.Decisions
curve25519-sha256stays offered, second. It serves clients older than OpenSSH 9.9. One recording forces it, so the classical path stays under the OpenSSH oracle.Client::handshakenow offers the hybrid first, so every refusal and fuzz session opens on it. The replay test derives each method's K and keys with its own code, then opens what OpenSSH sealed.Checks and evidence
Evidence: https://github.com/ToyOSOrg/evidence/tree/9bd93866959a422fd4854d2d1926e927798c8e84/pr-1023
Head aa65c1f, rebased onto main c8e6e03.
cargo run -- --ci hostat c4e95d5, on the old base 18a3873: exit 0 (ci-host.log).ci-host-enospc.log). The rerun had free space.cargo test -p toyos-sshat aa65c1f, exit 0 (toyos-ssh-tests-rebased.log), so every recording still replays byte for byte. The rebase brought in sha2: one SHA-2, toyos-crypto's; toyos-sha2 and toyos-sha2-hw gone #1016, which makes SHA-2toyos-crypto's; toyos-ssh reaches nothing else that changed.cargo run -- --ci hostat aa65c1f: exit 0, 101 steps green (ci-host-rebased.log)./usr/bin/sshon the Mac) was the black-box client for all seven recordings.mlkem768x25519-sha256, and none of the seven client logs has the post-quantum warning.seqoutputs and the refusals are inrecordings/.toyos-crypto's ACVP test inlib/crypto/tests/nist.rs, which--ci hostruns. RFC 10042 publishes no test vectors of its own.controls/): five checked patches, each applied, built, tested and reverted in one run.S_REPLY's halves fails them too.C_INIT.toyos-sshuntil stage 2.Risks
Ksits in aVecand is freed without being wiped, for either method.issues/toyos-ssh-leaves-its-secrets-in-freed-memory.mdrecords this, updated for the hybrid.curve25519-sha256. Drawing the method changed the fuzz's seeded sequence, and the new sequence missed one refusal kind. The OpenSSHrekeyrecording re-exchanges on the hybrid three times.🤖 Generated with Claude Code
https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q