Skip to content

X25519 lacks low-order/twist point checks #40

@curious-rabbit

Description

@curious-rabbit

src/agreement.rs:66-68:

  pub fn agree(&self, their_public: &PublicKey) -> SharedSecret {
      SharedSecret(&self.0 * &their_public.0)
  }

PublicKey::from accepts any 32 bytes; there is no explicit on-curve
check, no low-order-point rejection, and no twist-point rejection.

This is consistent with RFC 7748, which defines X25519 as accepting
any 32-byte input. The security argument relies on:

  • X25519 clamping (src/agreement.rs:93-95) forces the secret scalar
    to s ≡ 0 (mod 8). Therefore s · P = identity (all-zero) for any
    point P of order dividing 8. An adversary providing a low-order
    public key gets an all-zero shared secret and learns nothing
    about the device's private scalar.
  • Curve25519's twist has its smallest non-cofactor factor at
    approximately 2^125, blocking practical twist attacks within the
    threat model's adversary capabilities.

The lack of explicit checks is therefore not a key-recovery
vulnerability. However, an adversary submitting low-order public
keys receives a deterministic all-zero shared secret. A host
application that uses the shared secret directly as an encryption
key (without HKDF mixing in the public keys) would produce
ciphertexts with the same key as everyone else — a functional
security failure at the application level.

In the Nitrokey 3 firmware, all current downstream uses of X25519
feed the shared secret through HKDF or HPKE key_schedule that
binds the public keys into the KDF input, providing structural
resistance to this concern. The finding is therefore defense-in-
depth only: the salty layer provides no protection of its own;
resistance depends on the caller doing the right thing.

Recommended remediation.

Add an optional reject_zero or reject_low_order method to
SharedSecret that returns an error rather than the all-zero
value. Document the existing behavior and the new API in the
crate README.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions