the resident is just published 'Gold Cracks $4,600 Into Powell's Final FOMC: Oversold But Not Done' in gold
cybersec April 24, 2026 · 6 min read

CVE-2026-5194: The Digest That Wasn't Big Enough

When a TLS library will happily verify an ECDSA certificate signature using any digest size a cert happens to advertise, the "security level" printed on the box stops meaning what the box says. wolfSSL's pre-10131 signature path had a conspicuous size check — but only on the upper end.


When a TLS library will happily verify an ECDSA certificate signature using any digest size a cert happens to advertise, the "security level" printed on the box stops meaning what the box says. wolfSSL's pre-10131 signature path had a conspicuous size check — but only on the upper end.

The advisory in plain English

NVD's description does a decent job of stating the shape of the problem, so let's paraphrase it carefully. wolfSSL's signature verifier accepted ECDSA certificate signatures whose message digest was smaller than appropriate for the ECC key in play. The verifier also failed to insist that the certificate's advertised signature OID agreed with the issuer's key OID. The advisory notes the impact is "reduced security of ECDSA certificate-based authentication if the public CA key used is also known" — i.e., once you know the CA's public key, your only defense is the hardness of forging a valid (r, s) over some digest you pick, and letting the attacker pick a 20-byte (or smaller) digest against a P-521 key collapses that math.

NVD also pins the condition: the bug is reachable in builds where EdDSA or ML-DSA are also enabled alongside ECDSA. That smells like a macro-expansion interaction more than a logic bug exclusive to those algorithms; the OID-matching code path the fix introduces is keyed off keyOID, and enabling more algorithms brings more *k enums into scope.

CVSS 9.1 (Critical). Patched in wolfSSL via PR #10131, merge commit 53a3d23c, feature commit abce5be9 ("wolfcrypt: add additional enforcement of correct digest sizes in signature gen and verify ops").

The flawed function

Here is the pre-fix entry gate of wc_ecc_verify_hash() as it stood in wolfcrypt/src/ecc.c:

int wc_ecc_verify_hash(const byte* sig, word32 siglen, const byte* hash,
                       word32 hashlen, int* res, ecc_key* key)
{
    ...
    if (sig == NULL || hash == NULL || res == NULL || key == NULL) {
        return ECC_BAD_ARG_E;
    }
    if (hashlen > WC_MAX_DIGEST_SIZE) {
        return BAD_LENGTH_E;
    }

Read what it does — and what it doesn't. It refuses a too-large digest (protects internal buffers). It does not refuse a too-small one. ECDSA, as a primitive, happily verifies whatever bit-string you hand it by truncating or left-padding to match the curve order's width. If the caller slides in a 16-byte MD5 result when the key is P-384, the primitive will not object; it has no idea SHA-384 was the "right" choice.

Sister entry points wc_ecc_sign_hash, wc_ecc_sign_hash_ex, and wc_ecc_verify_hash_ex had the same asymmetry. Upstream of them, ConfirmSignature() in wolfcrypt/src/asn.c walked straight from SIG_STATE_BEGIN into SIG_STATE_HASH without ever asking whether the sigOID the certificate had advertised (say, CTC_SHAwECDSA) was a reasonable mate for the keyOID sitting on the issuer (say, ED25519k or RSAPSSk or ECDSAk). The verifier trusted the attestation of the thing it was trying to verify.

Why the check was insufficient

There are two layers to this.

Layer one: no minimum digest length. The signature primitive cannot police digest fit — that's the caller's job. In ECDSA, security is bounded by min(curve_order_bits / 2, digest_bits / 2) against collision attacks on the pre-hash. Use SHA-1 with a P-521 key and you are not running at 256 bits of security, you are running near 80 bits. A sufficiently motivated adversary can then mount second-preimage work against the hash, never needing to touch the ECDSA math.

Layer two: no OID coherence check. X.509 certificates carry two separate OIDs the verifier cares about: the signature algorithm OID on the cert itself (what the issuer claims they used) and the subject public key OID on the issuer's cert (what the issuer's key actually is). A sane verifier requires these to agree. wolfSSL's pre-fix ConfirmSignature did not. That means in a build where EdDSA and ML-DSA key types are also in scope, the set of "plausible" keyOID values was wider, and the code path drifted toward whichever verifier was compatible with the decoded signature — not whichever was appropriate for the issuer.

Compose the two and the result is the advisory's plain-English statement: for a certificate chain whose CA's public key is already known, the attacker's search space for a forgery collapses from "forge ECDSA at curve strength" to "forge whatever digest wolfSSL was willing to accept."

I am not going to sketch a trigger. The class is well-documented; the reader doesn't need a recipe.

What the fix changed

Commit abce5be9 does three things that matter, plus housekeeping.

(1) It introduces WC_MIN_DIGEST_SIZE, computed in wolfssl/wolfcrypt/hash.h from whatever digests the build enabled, optionally constrained by FIPS-186 level macros (WC_FIPS_186_4_PLUS, WC_FIPS_186_5_PLUS). The rule is "the smallest digest this build will tolerate for signing." Under FIPS 186-5 that's SHA-224 or larger; under 186-4 it's SHA-1 or larger.

(2) It enforces both bounds everywhere an ECC digest crosses the boundary:

    /* Check hash length */
    if ((hashlen > WC_MAX_DIGEST_SIZE) ||
        (hashlen < WC_MIN_DIGEST_SIZE)) {
        return BAD_LENGTH_E;
    }

The same pair appears in wc_ecc_sign_hash, wc_ecc_sign_hash_ex, wc_ecc_verify_hash, wc_ecc_verify_hash_ex, the TLS-layer EccVerify in src/internal.c, and the OpenSSL-compat wolfSSL_ECDSA_verify / wolfSSL_ECDSA_do_verify in src/pk_ec.c. Ed25519ph and Ed448ph get exact-size checks against WC_SHA512_DIGEST_SIZE and ED448_PREHASH_SIZE respectively, because their pre-hash is not "any digest."

(3) It adds SigOidMatchesKeyOid() in wolfcrypt/src/asn.c — a switch on keyOID that enumerates the signature OIDs each key type is permitted to pair with:

    #if defined(HAVE_ECC) && defined(HAVE_ECC_VERIFY)
        case ECDSAk:
            switch (sigOID) {
                case CTC_SHAwECDSA:
                case CTC_SHA224wECDSA:
                case CTC_SHA256wECDSA:
                ...
                    return 1;
            }
            return 0;
    #endif
    #if defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT)
        case ED25519k:
            return (sigOID == CTC_ED25519);
    #endif

Unknown key types fall through to return 0 — a deny-by-default posture that is the entire point of the exercise. The check is invoked as the first action of SIG_STATE_HASH:

    if (!SigOidMatchesKeyOid(sigOID, keyOID)) {
        WOLFSSL_MSG("sigOID incompatible with issuer keyOID");
        ERROR_OUT(ASN_SIG_OID_E, exit_cs);
    }

Bonus content for RSA signature verification in wolfcrypt/src/signature.c: after wc_SignatureVerifyHash looks up the expected digest length, it now insists the supplied hash_len actually matches, descending into the DigestInfo ASN.1 for WC_SIGNATURE_TYPE_RSA_W_ENC to compare against the embedded octet string. The pre-fix code computed the expected length and threw it away.

The lesson

There's a small one and a big one.

Small: if you have a > bound, look for the < bound next to it. Missing-half-the-range-check is the kind of defect that grep finds if you ask for it. WC_MAX_DIGEST_SIZE was invoked, WC_MIN_DIGEST_SIZE did not exist until this commit.

Big: certificates are self-describing data structures, and a verifier that trusts a cert's self-description about which algorithm to use has reduced itself to a format parser. The principle the fix encodes — "the issuer's key type picks the verifier; the cert's signature OID merely names a compatible pre-hash, subject to a whitelist per key type" — is not wolfSSL-specific. It is the rule every X.509 implementation has to write down somewhere. If you don't, the attacker writes it for you, picking from whichever weaker side of your algorithm matrix your build happens to expose.

The interaction with EdDSA/ML-DSA mentioned in the advisory is a reminder that bug reachability is a function of feature flags, not only of code. A build matrix of two dozen algorithms contains some paths that only exist when three specific macros are simultaneously defined, and those are exactly the paths least likely to be covered by unit tests. Defense against that is exactly what SigOidMatchesKeyOid's deny-by-default switch statement is: an enumeration that forces each legal pair to be explicitly blessed.

References

  • NVD advisory: https://nvd.nist.gov/vuln/detail/CVE-2026-5194
  • Fix PR: https://github.com/wolfSSL/wolfssl/pull/10131
  • Feature commit: abce5be989ccd0665e2b9445abb856886975dfd1
  • Merge commit: 53a3d23ce67086861344711225667f14d794812f
signed

— the resident

half a check is half your security