The pure-PHP X25519 scalar multiplication in phpseclib is not constant-time. Field addition and subtraction each perform a data-dependent conditional modular reduction, so the cost of each Montgomery-ladder step is a linear function of that step's reduction count which is a quantity determined by the secret scalar's prefix.
An observer with per-ladder-step resolution recovers the 251-bit clamped private scalar. This is a per-step leak, not an aggregate one: an instrumented code proof-of-concept recovers 20/20 test keys from 32 observed operations, and an observer that counts libgmp calls instead of timing them recovers a key from a single operation.
This is not a low-order-input issue. Recovery works with the RFC 7748 base point u = 9, with no attacker-chosen input at all. Rejecting low-order public values does not close it.
2. Affected component
Confirmed on phpseclib 3.0.56 (338 files under phpseclib/,sha256(sorted(relpath NUL file_sha256 LF)) = cc7250b611f520e809131aab0931503457c44d8cbfb10d535251c6fec5f62a2b).
The code appears unchanged across the 3.0 series wherever Curve25519 is supported, please confirm the affected range.
| file:line | role |
|---|---|
| Math/PrimeField/Integer.php:189 | add() — conditional subtract($modulo) when the sum ≥ p |
| Math/PrimeField/Integer.php:207 | subtract() — conditional add($modulo) when the result is negative |
| Crypt/EC/BaseCurves/Montgomery.php:229–234 | ladder branch on the secret bit, selecting argument order of doubleAndAddPoint |
| Crypt/EC/Formats/Keys/MontgomeryPrivate.php:66 | multiplyPoint(getBasePoint(), dA) — no engine check of any kind |
| Crypt/EC/Formats/Keys/PKCS8.php:194–200 | the same derivation, correctly gated on ext-sodium — the pattern MontgomeryPrivate is missing |