Fix EdDSA lazy public-key derivation race on shared private-only keys - #478
Fix EdDSA lazy public-key derivation race on shared private-only keys#478sameehj wants to merge 2 commits into
Conversation
57f29c9 to
caaafa7
Compare
caaafa7 to
c95f40d
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #478
Scan targets checked: wolfprovider-bugs, wolfprovider-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
| static int wp_ecx_shared_sign(wp_ecx_shared_args* w) | ||
| { | ||
| int err; | ||
| unsigned char sig[ED448_SIG_SIZE]; |
There was a problem hiding this comment.
New test helpers reference ED448 sizes outside any WP_HAVE_ED448 guard · Conditional compilation / build break
ED448_SIG_SIZE (line 1211) and ED448_PUB_KEY_SIZE (line 1236) are only defined by wolfssl/wolfcrypt/ed448.h under HAVE_ED448, but both helpers sit under #ifdef WP_HAVE_ECX_SHARED_KEY_TEST, which is gated on pthreads only. An Ed25519-only build fails to compile, unlike the rest of the file (see lines 210-212, 267-283) which guards every ED448 constant.
Fix: Size both local buffers from a macro that falls back to the ED25519 sizes when WP_HAVE_ED448 is undefined, mirroring the #if defined(...) && defined(...) / #elif pattern already used at lines 267-276.
| static int wp_ecx_shared_sign(wp_ecx_shared_args* w) | ||
| { | ||
| int err; | ||
| unsigned char sig[ED448_SIG_SIZE]; |
There was a problem hiding this comment.
New shared-key test helpers use ED448 size constants outside any WP_HAVE_ED448 guard · Memory management
wp_ecx_shared_sign sizes sig[] with ED448_SIG_SIZE and wp_ecx_shared_get_pub sizes pub[] with ED448_PUB_KEY_SIZE inside a block gated only on WP_HAVE_ED25519 || WP_HAVE_ED448. wolfSSL defines these only under HAVE_ED448, so an Ed25519-only build fails to compile; every other ED448 constant in this file (lines 211, 282, 423, 1158) is guarded.
Fix: Size both buffers from a locally computed maximum that is guarded by #ifdef WP_HAVE_ED448/#ifdef WP_HAVE_ED25519, as done elsewhere in this file.
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.