Add mlkem768x25519-sha256 post-quantum hybrid KEX - #235
Open
Wellz26 wants to merge 1 commit into
Open
Conversation
Implements the OpenSSH 9.9 / IETF draft-kampanakis-curdle-ssh-pq-ke algorithm, combining ML-KEM-768 (FIPS 203) with X25519. K is derived as SHA-256(MLKEM_SS || X25519_SS), giving session keys that remain secure if either primitive is compromised. Built on swift-crypto 4.5.0's public MLKEM768 API (gated to macOS 26 / iOS 26 / watchOS 26 / tvOS 26 / visionOS 26 / macCatalyst 26 where the backing CryptoKit implementation is available). On older deployment targets the algorithm list silently falls back to the existing ECDH set. The new struct slots into supportedKeyExchangeImplementations at the top of the preference order so OpenSSH 9.9+ peers negotiate the hybrid by default, with classical-only peers transparently falling through to curve25519-sha256. To accommodate ML-KEM-768 keygen (which is throwing on swift-crypto) without breaking the existing protocol surface, the keypair is generated lazily inside initiateKeyExchangeClientSide. That method is now `mutating func` on EllipticCurveKeyExchangeProtocol; existing ECDH conformers are unaffected. Open question for maintainers: keep this shape, or grow a throwing init on the protocol? Includes a round-trip test, previous-session-identifier test, truncated-payload rejection test, and algorithm-name spelling test (all green; full existing KEX suite stays green).
Wellz26
marked this pull request as ready for review
May 25, 2026 23:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
OpenSSH 9.9+ ships
mlkem768x25519-sha256as its default key exchange,giving SSH transport sessions resistance to harvest-now-decrypt-later
attacks against a future cryptographically-relevant quantum computer.
swift-nio-ssh currently only supports classical ECDH primitives
(curve25519-sha256, ecdh-sha2-nistp256/384/521), so any iOS or macOS
client built on it (e.g. Termius-style apps using Citadel, or anything
embedding NIOSSH directly) is left behind whenever it talks to a modern
sshd.
Apple shipped the
MLKEM768primitive inswift-crypto 4.5.0
(public
MLKEM768.PublicKey/MLKEM768.PrivateKeywithencapsulate()/decapsulate()), so the underlying KEM is nowavailable without any FFI, BoringSSL detour, or third-party dependency
— only the SSH wire-format integration was missing. This PR adds that
integration.
What this PR does
Sources/NIOSSH/Key Exchange/MLKEM768X25519KeyExchange.swiftimplementing
EllipticCurveKeyExchangeProtocolfor the hybrid:`MLKEM768 encapsulation key (1184 B) || X25519 public key (32 B)`
`MLKEM768 ciphertext (1088 B) || X25519 public key (32 B)`
hashed in as an `mpint` per RFC 4253 §8
gated on `#available(macOS 26.0, iOS 26.0, ...)` (the deployment-target
floor for the backing CryptoKit primitive). Classical peers
transparently fall through to `curve25519-sha256`.
for the public `MLKEM768` API. This is a breaking change for
downstream consumers still on older swift-crypto — flagging it
explicitly so maintainers can decide whether to phase it in
differently.
client-init rejection, algorithm-name spelling.
Spec references
Open design questions for maintainers
(1) Mutating + throwing initiateKeyExchangeClientSide
`MLKEM768.PrivateKey.init()` is `throws`, but
`EllipticCurveKeyExchangeProtocol.init` is neither throwing nor
mutating. To avoid changing the protocol surface, this PR:
`mutating func` on the protocol (a backwards-compatible widening —
existing non-mutating ECDH conformers still work)
`preconditionFailure`s on the (theoretically unreachable)
generation-failure path
A cleaner long-term shape would be a `throws` variant or moving
keypair generation into a separately-throwing init. Happy to refactor
in whichever direction you prefer.
(2) swift-crypto floor bump
`MLKEM768` is only available on swift-crypto >= 4.5.0, and the actual
primitive routes through CryptoKit on the macOS 26 / iOS 26 family
(hence the `#available` gate). Older deployment targets will compile,
link, and simply not register the hybrid algorithm — `mlkem768x25519-sha256`
silently drops out of the negotiation list. Is that the policy you'd
want, or should the gate fail loudly?
(3) ML-KEM-1024 follow-up
swift-crypto 4.5.0 also exposes `MLKEM1024`. OpenSSH hasn't
standardized an `mlkem1024x25519-sha384` algorithm name, but if you'd
like a 1024-variant for higher security margin I can extend the same
pattern in a follow-up.
Status
Opening as DRAFT — the implementation builds clean and the new
test target passes (`swift test --filter MLKEM768X25519KeyExchangeTests`),
but I want maintainer feedback on the design questions above before
I sand the rough edges (interop tests against an OpenSSH 9.9 server,
fuzz coverage, additional KAT vectors, etc.). Happy to iterate.
Verification
— Wellington Mukahiwa (@Wellz26)