Skip to content

Fix EdDSA lazy public-key derivation race on shared private-only keys - #478

Open
sameehj wants to merge 2 commits into
wolfSSL:masterfrom
sameehj:test/eddsa-lazy-pubkey-race
Open

Fix EdDSA lazy public-key derivation race on shared private-only keys#478
sameehj wants to merge 2 commits into
wolfSSL:masterfrom
sameehj:test/eddsa-lazy-pubkey-race

Conversation

@sameehj

@sameehj sameehj commented Aug 27, 2026

Copy link
Copy Markdown

Summary

A private-only EdDSA key (a seed-only PKCS#8 Ed25519 or Ed448 key) has no
public half after import. wolfProvider derives the public half later, on
first use. The derivation writes into the shared wolfSSL key object without
holding the key mutex. When two or more threads first use the same
EVP_PKEY at the same time, they sign or export with a partly written
public key. This produces failed or invalid signatures.

Reported by: Fenrir finding 11559.

Root cause

wc_ed25519_make_public / wc_ed448_make_public set pubKeySet when they
write to the output buffer. They do not always fill key->p. Only
wc_ed*_import_public stores the value into key->p. The old code derived
the public half in three places without the mutex:

  • wp_ed25519_export_public / wp_ed448_export_public
  • wp_ed25519_digest_sign / wp_ed448_digest_sign (derived before the lock;
    only wc_ed*_sign_msg was locked)
  • wp_Ed25519PublicKeyToDer / wp_Ed448PublicKeyToDer

A concurrent first use could read key->p while another thread wrote it.

Fix

Derive the public half in one place, under the key mutex.

  • Add a derivePub callback to wp_EcxData (set for Ed25519 and Ed448,
    NULL for X25519/X448).
  • Add wp_ecx_ensure_pub(). It takes the key mutex, then calls the derive
    helper. The helper calls make_public into a local buffer, then
    import_public, which is the only call that sets key->p and pubKeySet.
  • Remove the unlocked derivation from the export and DER helpers. They are
    now plain export/encode.
  • Call wp_ecx_ensure_pub() from every first-use site: the sign paths,
    wp_ecx_get_params_enc_pub_key, wp_ecx_match_pub_key,
    wp_ecx_export_keypair, wp_ecx_dup, and the SPKI branch of
    wp_ecx_encode.

The SPKI-only guard in wp_ecx_encode keeps the public key out of a private
key encoding. In wp_ecx_dup, the derive runs before the key copy, so the
copy also gets a happens-before edge against a concurrent first use.

Scope: other algorithms

I reviewed the other key types for the same pattern.

  • ECC also derives a public key from a private key, but it does so at
    import/decode time, while the object has one owner. No race.
  • ML-DSA does not derive on first use. It reports hasPub = 0 for a
    private-only key and fails cleanly.
  • RSA, DH, ECDSA, and ML-KEM have no first-use public derivation.

EdDSA was the only affected type.

Test

test_ecx_shared_key_first_use (test 195) loads one seed-only key, then uses
it from 4 threads at the same time. Half the threads sign, half export. The
signers verify against a separate public-only key, so a bad public half
cannot hide a bad signature. The workload is fixed and small, so any failure
is a defect, not timing.

  • Without the fix: the test fails on every run (10/10 local runs).
  • With the fix: the test passes, and the full unit suite passes.

ThreadSanitizer (the tsan job) is the reliable detector for the data race
itself. The test is the deterministic correctness guard.

Severity

Medium. The window is first use of a shared private-only key. The impact is
failed or invalid signatures, not key disclosure or forgery. A TLS load path
that warms the public key via X509_check_private_key is not affected. A
sign-only service with a raw private import and a thread pool is the
realistic case.

Commits

  • test: adds the failing test.
  • fix: adds the fix.

The first commit fails on its own by design (red), the second makes it pass
(green). Please merge as a unit; do not run per-commit CI or bisect across
the pair.

@sameehj
sameehj force-pushed the test/eddsa-lazy-pubkey-race branch from 57f29c9 to caaafa7 Compare August 28, 2026 06:33
@sameehj
sameehj force-pushed the test/eddsa-lazy-pubkey-race branch from caaafa7 to c95f40d Compare August 28, 2026 08:38
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.

2 participants