From c2df7c6e1ea5968de163ead9e9c23e18782b8421 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 14:09:23 +0000 Subject: [PATCH] Add opt-in X.509 cross-consistency checks (EKU chaining, RSA-PSS key params, critical BasicConstraints) Follow-up to the algorithm-confusion hardening in #10131 (SigOidMatchesKeyOid). An audit of X.509 parsing/validation found three further "logically related fields that should agree but are not cross-checked" gaps. Each fix is gated behind a new, default-off macro so existing builds are byte-for-byte unchanged (the full testsuite passes with the macros off). WOLFSSL_CHECK_EKU_CHAIN (B1): The issuing CA's extendedKeyUsage did not constrain the certs it issued (only the leaf's EKU was checked, at TLS time). Propagate the CA's effective EKU onto its Signer in FillSigner and, during chain verification, reject a certificate that asserts an EKU not permitted by the issuing CA. An absent CA EKU or anyExtendedKeyUsage imposes no restriction. WOLFSSL_CHECK_RSAPSS_KEY_PARAMS (B2): An id-RSASSA-PSS issuer key's restricting parameters (RFC 4055) were parsed and then discarded (the long-standing "TODO: store parameters so that usage can be checked" in DecodeRsaPublicKey). Capture the SPKI PSS parameters on the DecodedCert/Signer and require a signature made by that key to use the mandated hash/MGF/salt. WOLFSSL_REQUIRE_CRITICAL_BASIC_CONSTRAINTS (B7): RFC 5280 4.2.1.9 requires CA certificates to mark basicConstraints critical; wolfSSL stored the critical bit but never required it. Reject an intermediate CA whose basicConstraints is not critical. Self-signed trust anchors are exempt, as their own extensions are not processed during path validation. All three are validated with dedicated proof-of-concept programs and EKU positive/negative controls; B1+B2 pass the standard testsuite, and B7's rejections are limited to non-conforming (non-critical-BC) intermediates. https://claude.ai/code/session_01NSH5QDCbE9n1hKYQUYbKQA --- wolfcrypt/src/asn.c | 106 ++++++++++++++++++++++++++++++++++++++++ wolfssl/wolfcrypt/asn.h | 21 ++++++++ 2 files changed, 127 insertions(+) diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 56cf31d3f5c..06b7123d990 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -21474,6 +21474,31 @@ static int DecodeCertInternal(DecodedCert* cert, int verify, int* criticalExt, cert->keyOID = dataASN[X509CERTASN_IDX_TBS_SPUBKEYINFO_ALGO_OID].data.oid.sum; cert->certBegin = dataASN[X509CERTASN_IDX_TBS_SEQ].offset; +#if defined(WC_RSA_PSS) && defined(WOLFSSL_CHECK_RSAPSS_KEY_PARAMS) + /* Capture RSA-PSS parameters carried in this cert's SubjectPublicKey- + * Info algorithm identifier (RFC 4055). They restrict the hash/MGF/ + * salt usable in signatures made by this key; store them so the + * restriction can be enforced when this cert acts as an issuer. */ + if ((cert->keyOID == RSAPSSk) && + (dataASN[X509CERTASN_IDX_TBS_SPUBKEYINFO_ALGO_P_SEQ].tag != 0)) { + word32 pssOff = + dataASN[X509CERTASN_IDX_TBS_SPUBKEYINFO_ALGO_P_SEQ].offset; + enum wc_HashType pssHash = WC_HASH_TYPE_SHA; + int pssMgf = WC_MGF1SHA1; + int pssSaltLen = 20; + /* DecodeRsaPssParams bounds itself by the SEQUENCE length, so a + * generous size to end-of-cert is safe. */ + if (DecodeRsaPssParams(cert->source + pssOff, + cert->maxIdx - pssOff, &pssHash, &pssMgf, + &pssSaltLen) == 0) { + cert->keyPssHash = pssHash; + cert->keyPssMgf = pssMgf; + cert->keyPssSaltLen = pssSaltLen; + cert->keyPssParamsSet = 1; + } + } +#endif + /* No bad date error - don't always care. */ badDate = 0; /* Find the item with the ASN_BEFORE date and check it. */ @@ -23157,6 +23182,21 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, void* cm, } #endif + #ifdef WOLFSSL_REQUIRE_CRITICAL_BASIC_CONSTRAINTS + /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + * Conforming CAs MUST include [basicConstraints] in all CA + * certificates ... and MUST mark the extension as critical. + * Enforced on intermediate CAs only: a trust anchor (self-signed root) + * is an explicitly-trusted input and its own extensions are not + * processed during RFC 5280 sec 6.1 path validation. */ + if (cert->isCA && !cert->selfSigned && cert->extBasicConstSet && + !cert->extBasicConstCrit) { + WOLFSSL_MSG("Intermediate CA basicConstraints not marked critical"); + WOLFSSL_ERROR_VERBOSE(ASN_CRIT_EXT_E); + return ASN_CRIT_EXT_E; + } + #endif + #ifndef NO_SKID if (cert->extSubjKeyIdSet == 0 && cert->publicKey != NULL && cert->pubKeySize > 0) { @@ -23419,6 +23459,52 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, void* cm, return ret; } + #ifdef WOLFSSL_CHECK_EKU_CHAIN + /* RFC 5280 4.2.1.12 / CA-Browser Forum EKU chaining: the + * extendedKeyUsages this certificate asserts must be a subset + * of those the issuing CA permits (effective EKU, propagated + * in FillSigner). A CA EKU of 0 (absent) or anyExtendedKeyUsage + * imposes no restriction; a child asserting anyExtendedKeyUsage + * under a constrained CA is likewise rejected. */ + if (cert->extExtKeyUsageSet && + (cert->ca->extKeyUsage != 0) && + ((cert->ca->extKeyUsage & EXTKEYUSE_ANY) == 0) && + (((cert->extExtKeyUsage & EXTKEYUSE_ANY) != 0) || + ((cert->extExtKeyUsage & + (byte)~cert->ca->extKeyUsage) != 0))) { + WOLFSSL_MSG("Cert EKU not permitted by issuing CA EKU"); + WOLFSSL_ERROR_VERBOSE(EXTKEYUSAGE_E); + return EXTKEYUSAGE_E; + } + #endif + + #if defined(WC_RSA_PSS) && defined(WOLFSSL_CHECK_RSAPSS_KEY_PARAMS) + /* RFC 4055: when the issuing key is RSASSA-PSS with restricting + * parameters, the signature's PSS parameters must conform to + * the key's mandated hash/MGF/salt. */ + if (cert->ca->keyPssParamsSet) { + enum wc_HashType sigPssHash = WC_HASH_TYPE_SHA; + int sigPssMgf = WC_MGF1SHA1; + int sigPssSaltLen = 20; + int pssRet = 0; + if (cert->sigParamsLength > 0) { + pssRet = DecodeRsaPssParams( + cert->source + cert->sigParamsIndex, + cert->sigParamsLength, &sigPssHash, &sigPssMgf, + &sigPssSaltLen); + } + if ((pssRet != 0) || + ((int)sigPssHash != cert->ca->keyPssHash) || + (sigPssMgf != cert->ca->keyPssMgf) || + (sigPssSaltLen != cert->ca->keyPssSaltLen)) { + WOLFSSL_MSG("Sig PSS params don't match issuer key " + "PSS params"); + WOLFSSL_ERROR_VERBOSE(ASN_SIG_OID_E); + return ASN_SIG_OID_E; + } + } + #endif + #ifdef WOLFSSL_DUAL_ALG_CERTS if ((ret == 0) && cert->extAltSigAlgSet && cert->extAltSigValSet) { @@ -23642,6 +23728,26 @@ int FillSigner(Signer* signer, DecodedCert* cert, int type, DerBuffer *der) signer->keyUsage = cert->extKeyUsageSet ? cert->extKeyUsage : 0xFFFF; signer->extKeyUsage = cert->extExtKeyUsage; +#ifdef WOLFSSL_CHECK_EKU_CHAIN + /* Propagate the issuing CA's extendedKeyUsage restriction onto this + * signer so it applies transitively down the path (RFC 5280 + * 4.2.1.12 chaining). A CA EKU of 0 (absent) or anyExtendedKeyUsage + * imposes no restriction. */ + if (cert->ca != NULL && cert->ca->extKeyUsage != 0 && + (cert->ca->extKeyUsage & EXTKEYUSE_ANY) == 0) { + if (signer->extKeyUsage == 0) + signer->extKeyUsage = cert->ca->extKeyUsage; + else + signer->extKeyUsage = + (byte)(signer->extKeyUsage & cert->ca->extKeyUsage); + } +#endif +#if defined(WC_RSA_PSS) && defined(WOLFSSL_CHECK_RSAPSS_KEY_PARAMS) + signer->keyPssParamsSet = cert->keyPssParamsSet; + signer->keyPssHash = (int)cert->keyPssHash; + signer->keyPssMgf = cert->keyPssMgf; + signer->keyPssSaltLen = cert->keyPssSaltLen; +#endif signer->next = NULL; /* If Key Usage not set, all uses valid. */ cert->publicKey = 0; /* in case lock fails don't free here. */ cert->pubKeyStored = 0; diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 5db17afab1b..fd117f248cb 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -1779,6 +1779,17 @@ struct DecodedCert { #ifdef WC_RSA_PSS word32 sigParamsIndex; /* start of signature parameters */ word32 sigParamsLength; /* length of signature parameters */ +#if defined(WOLFSSL_CHECK_RSAPSS_KEY_PARAMS) + /* SPKI-level RSA-PSS parameters (RFC 4055) constraining how this cert's + * key may be used to verify signatures. Captured so the constraint can be + * enforced against signatures made by this key (resolves the historical + * "TODO: store parameters so that usage can be checked" in + * DecodeRsaPublicKey). */ + enum wc_HashType keyPssHash; /* mandated message-digest hash */ + int keyPssMgf; /* mandated MGF1 hash (WC_MGF1...) */ + int keyPssSaltLen; /* mandated salt length */ + byte keyPssParamsSet; /* 1 when key carries PSS params */ +#endif #endif int version; /* cert version, 1 or 3 */ DNS_entry* altNames; /* alt names list of dns entries */ @@ -2185,6 +2196,16 @@ struct Signer { word32 keyOID; /* key type */ word16 keyUsage; byte extKeyUsage; +#if defined(WC_RSA_PSS) && defined(WOLFSSL_CHECK_RSAPSS_KEY_PARAMS) + /* SPKI-level RSA-PSS parameters of this CA's key, propagated from the + * DecodedCert so a signature made by this key can be checked to conform + * (RFC 4055). Gated by the macro so the default Signer layout - which is + * load-bearing for PERSIST_CERT_CACHE - is unchanged. */ + byte keyPssParamsSet; + int keyPssHash; + int keyPssMgf; + int keyPssSaltLen; +#endif word16 maxPathLen; WC_BITFIELD selfSigned:1; #ifndef IGNORE_NAME_CONSTRAINTS