Snyk has a proof-of-concept or detailed explanation of how to exploit this vulnerability.
The probability is the direct output of the EPSS model, and conveys an overall sense of the threat of exploitation in the wild. The percentile measures the EPSS probability relative to all known EPSS scores. Note: This data is updated daily, relying on the latest available EPSS model version. Check out the EPSS documentation for more details.
In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.
Test your applicationsUpgrade jose to version 1.11.13 or higher.
Affected versions of this package are vulnerable to Timing Attack via the pure-Erlang jose_jwa_x25519:curve25519/2 scalar-multiplication ladder, which executes value-dependent branching based on the bits of the private scalar. An attacker who can submit attacker-controlled ECDH-ES ciphertexts to a victim service and measure decryption response times can recover information about the victim's long-term X25519 private key through a timing side channel.
Note: This is only exploitable when the crypto_fallback application environment variable is set to true, enabling the pure-Erlang Curve25519 implementation instead of a constant-time native provider.
The attached PoC encodes an outside-attacker threat model. A victim service holds a long-term X25519 private key and decrypts attacker-supplied ECDH-ES messages through the public entry point JOSE.JWK.box_decrypt_ecdh_es/2. That call reaches the vulnerable pure-Erlang jose_jwa_x25519:curve25519/2 ladder. The attacker cannot read the key, but can submit ciphertexts and time the responses.
To make the leak observable, the script first forces the pure-Erlang fallback so the vulnerable ladder — not a constant-time NIF — is exercised:
JOSE.crypto_fallback(true)
JOSE.curve25519_module(:jose_jwa_curve25519)
It then builds two victims that differ only in their long-term private scalar, using two 32-byte seeds with deliberately different bit structure (0x11… and 0xEE…) so the secret-bit-driven ladder routing diverges maximally. The attacker crafts ciphertexts with a single fixed ephemeral key (JOSE.JWK.generate_key({:okp, :X25519})) and encrypts to each victim's public key via box_encrypt_ecdh_es, then compacts to the JWE binaries that would arrive over the wire. Fixing the ephemeral keeps the base input point identical for both victims, so any timing difference is attributable to the private scalar alone. A sanity check confirms both ciphertexts decrypt to "attacker message", so the timed operation is the real decryption.
The mechanism the script exploits is that box_decrypt_ecdh_es performs the ECDH scalar multiplication over the victim's secret scalar inside the branching, value-dependent ladder described above. With the same input point and an operation-count-balanced, fixed-depth (254-step) ladder, the two decryptions perform an identical amount of work — so any stable, reproducible timing separation is driven by the secret bits themselves.
To measure this robustly against scheduler and JIT noise, the script warms up (50 iterations), takes the median of 400 :timer.tc samples per measurement, interleaves the two victims across 9 rounds, and reports the median-of-medians per victim. It prints each victim's median decryption time and the secret-correlated delta, then asserts:
if delta_us > 0 do
IO.puts("VERIFIED: box_decrypt runtime is a stable function of the private X25519 scalar ...")
A non-zero, reproducible delta_us between two victims that differ only in their private scalar — while using the same attacker-supplied input point — demonstrates the timing side channel reachable from the public JOSE.JWK.box_decrypt_ecdh_es/2 API. A constant-time implementation would show no secret-correlated difference. The full script is attached below under "Scripts and Logs" so the reader can run it.
Note: The PoC requires JOSE.crypto_fallback(true) and JOSE.curve25519_module(:jose_jwa_curve25519) to be set explicitly to force the vulnerable pure-Erlang ladder.