Skip to content

Reject a non-contributory X25519 peer key (RFC 8446 §4.2.8.2) - #314

Open
Runnin4ik wants to merge 1 commit into
RustCrypto:masterfrom
Runnin4ik:x25519-contributory
Open

Runnin4ik wants to merge 1 commit into
RustCrypto:masterfrom
Runnin4ik:x25519-contributory

Conversation

@Runnin4ik

@Runnin4ik Runnin4ik commented Sep 25, 2026 •

Copy link
Copy Markdown

X25519KeyExchange::complete accepts a peer key of low order. x25519-dalek computes the Diffie-Hellman result for such a key — the identity, a shared secret the peer knows — and only reports that through was_contributory(), so the check has to be made here. RFC 8446 §4.2.8.2 requires the handshake to abort, and ActiveKeyExchange::complete documents it as a requirement of the trait:

This method must return an error if peer_pub_key is invalid: either mis-encoded, or an invalid public key (such as, but not limited to, being in a small order subgroup).

The ring and aws-lc-rs providers reject such a share, so a client that switches provider changes behaviour on that wire input. The two NIST groups in this crate cannot reach the case at all — their curves have cofactor 1, and the identity is not a valid PublicKey — which leaves X25519KeyExchange as the one group that silently continues.

The change is the check itself, plus tests/x25519_low_order.rs:

  • all seven encodings of a low-order point from libsodium's has_small_order table — the points of order 1, 2, 4 and 8, and the three non-canonical encodings at or above the field prime that reduce to them — are rejected with PeerMisbehaved::InvalidKeyShare. A check that only rejected the all-zero key would still accept a key of order 2;
  • a peer key that is not 32 bytes is the same key-share error, not a panic;
  • a real peer key completes, with a non-zero 32-byte secret, as the control a group rejecting every key would fail.

Verified: cargo test --features tls12 and cargo test --no-default-features --features tls12,alloc pass (5 tests each); reverting the check makes x25519_rejects_a_low_order_peer_key fail, so the test does hold the behaviour rather than the code.

Unrelated, and not something this PR touches: on my toolchain (clippy 1.98) make check stops at clippy::from_iter_instead_of_collect in src/lib.rs — the lint has been removed from clippy, so naming it in the crate's #![warn(…)] is itself an error under -D warnings. CI's clippy job is green here, so this is a toolchain-version matter rather than a CI failure, and it is not this change's to fix.

RFC 8446 §4.2.8.2 requires aborting when the X25519 shared secret is the identity:
a peer key of low order makes it all zeros, i.e. a secret the peer knows.
`x25519-dalek` computes that result and only *reports* it through
`was_contributory()`, and `ActiveKeyExchange::complete` documents the rejection as
a requirement of the trait it implements ("must return an error if `peer_pub_key`
is invalid: either mis-encoded, or an invalid public key (such as, but not limited
to, being in a small order subgroup)"). The `ring` and `aws-lc-rs` providers do
reject it, so X25519 is the one group here that silently completes on a predictable
secret.

The two NIST groups cannot reach this case — their curves have cofactor 1, and the
identity is not a valid `PublicKey` — so the check belongs to `X25519KeyExchange`
alone.

`tests/x25519_low_order.rs` pins it: the seven encodings of a low-order point from
libsodium's `has_small_order` table (order 1, 2, 4 and 8, plus the three
non-canonical encodings at or above the field prime that reduce to them) are each
rejected with `PeerMisbehaved::InvalidKeyShare`, a peer key of the wrong length with
them, and a real peer key completes as the control that a group rejecting everything
would fail.
Runnin4ik added a commit to Runnin4ik/dpi-detector that referenced this pull request Sep 25, 2026
…ands

The group in `crates/dpi-core/src/net/x25519.rs` and the substitution in
`crypto_provider()` exist because `rustls-rustcrypto`'s own X25519 completes a
handshake on a low-order peer key. That is a gap in the crate, not a decision it
made — nothing in that project's tracker names `was_contributory` — and it is now
a PR against its `master`:
[RustCrypto/rustls-rustcrypto#314](RustCrypto/rustls-rustcrypto#314),
two files: the check, and `tests/x25519_low_order.rs` over libsodium's seven
low-order encodings with a real-peer control.

Both files here record it, because the next session should not have to find out
again: `vendor/rustls-rustcrypto/README-PATCH.md` says why the check is not in the
patch and what to remove when a release carries it, and the module doc points at
the PR. It is the one piece of that fork whose fate is upstream's to decide: the
`rustls-webpki` swap is already on `master` and only unreleased (ISS #107), so if
the check lands and the next generation ships, the substitution goes and the
provider becomes an ordinary dependency.

Verified before opening it, in a clone at `%TEMP%/pr-provider`: their CI rows pass
— `cargo test --features tls12` and `cargo test --no-default-features --features
tls12,alloc`, five tests each, the three new ones among them — and reverting the
check makes `x25519_rejects_a_low_order_peer_key` fail, which is the test holding
the behaviour rather than the code. Clippy on that tree reports only the
pre-existing `src/lib.rs:16` (`clippy::from_iter_instead_of_collect` was removed
from clippy, so naming it is itself a warning on a current toolchain); the PR body
says so, and it is not this change's to fix.
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