Fix EdDSA lazy public-key derivation race on shared private-only keys - #478
Open
sameehj wants to merge 2 commits into
Open
Fix EdDSA lazy public-key derivation race on shared private-only keys#478sameehj wants to merge 2 commits into
sameehj wants to merge 2 commits into
Conversation
sameehj
force-pushed
the
test/eddsa-lazy-pubkey-race
branch
from
August 28, 2026 06:33
57f29c9 to
caaafa7
Compare
sameehj
force-pushed
the
test/eddsa-lazy-pubkey-race
branch
from
August 28, 2026 08:38
caaafa7 to
c95f40d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_PKEYat the same time, they sign or export with a partly writtenpublic key. This produces failed or invalid signatures.
Reported by: Fenrir finding 11559.
Root cause
wc_ed25519_make_public/wc_ed448_make_publicsetpubKeySetwhen theywrite to the output buffer. They do not always fill
key->p. Onlywc_ed*_import_publicstores the value intokey->p. The old code derivedthe public half in three places without the mutex:
wp_ed25519_export_public/wp_ed448_export_publicwp_ed25519_digest_sign/wp_ed448_digest_sign(derived before the lock;only
wc_ed*_sign_msgwas locked)wp_Ed25519PublicKeyToDer/wp_Ed448PublicKeyToDerA concurrent first use could read
key->pwhile another thread wrote it.Fix
Derive the public half in one place, under the key mutex.
derivePubcallback towp_EcxData(set for Ed25519 and Ed448,NULLfor X25519/X448).wp_ecx_ensure_pub(). It takes the key mutex, then calls the derivehelper. The helper calls
make_publicinto a local buffer, thenimport_public, which is the only call that setskey->pandpubKeySet.now plain export/encode.
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 ofwp_ecx_encode.The SPKI-only guard in
wp_ecx_encodekeeps the public key out of a privatekey encoding. In
wp_ecx_dup, the derive runs before the key copy, so thecopy also gets a happens-before edge against a concurrent first use.
Scope: other algorithms
I reviewed the other key types for the same pattern.
import/decode time, while the object has one owner. No race.
hasPub = 0for aprivate-only key and fails cleanly.
EdDSA was the only affected type.
Test
test_ecx_shared_key_first_use(test 195) loads one seed-only key, then usesit 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.
ThreadSanitizer (the
tsanjob) is the reliable detector for the data raceitself. 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_keyis not affected. Asign-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.