diff --git a/.github/workflows/os-check.yml b/.github/workflows/os-check.yml index 25f2b6f881..bb4c453a47 100644 --- a/.github/workflows/os-check.yml +++ b/.github/workflows/os-check.yml @@ -436,7 +436,12 @@ jobs: {"name": "opensslextra-no-filesystem-no-bio", "minutes": 0.9, "configure": ["--enable-opensslextra", "--disable-filesystem", "CPPFLAGS=-DNO_BIO"]}, {"name": "no-examples-no-malloc", "minutes": 0.8, - "configure": ["--disable-examples", "CPPFLAGS=-DWOLFSSL_NO_MALLOC"]} + "configure": ["--disable-examples", "CPPFLAGS=-DWOLFSSL_NO_MALLOC"]}, + {"comment": "WOLFSSL_NO_ASN_STRICT still enforces the RFC 5280 4.2 and 4.2.1.10 MUSTs", + "name": "no-asn-strict-certs", + "configure": ["--enable-testcert", "--enable-opensslextra", "--enable-certgen", + "--enable-certreq", "--enable-certext", "--enable-ecc", + "CPPFLAGS=-DWOLFSSL_NO_ASN_STRICT -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DWOLFSSL_ALT_NAMES"]} ] EOF .github/scripts/parallel-make-check.py \ diff --git a/tests/api.c b/tests/api.c index 238ea67397..c264f781f5 100644 --- a/tests/api.c +++ b/tests/api.c @@ -24314,6 +24314,51 @@ static word32 build_registeredID_san(byte* out, word32 outSz) return (word32)sizeof(ridSan); } +/* Build a SubjectAltName extension value (a SEQUENCE wrapping a single + * directoryName GeneralName) holding the Name "CN=". */ +static word32 build_dirName_san(byte* out, word32 outSz, const char* val7) +{ + static const byte prefix[] = { + 0x30, 0x16, /* SEQUENCE, 22 */ + 0xA4, 0x14, /* [4] dirName, 20 */ + 0x30, 0x12, /* Name SEQUENCE, 18 */ + 0x31, 0x10, /* RDN SET, 16 */ + 0x30, 0x0E, /* ATV SEQUENCE, 14 */ + 0x06, 0x03, 0x55, 0x04, 0x03, /* OID 2.5.4.3 (CN) */ + 0x0C, 0x07 /* UTF8String, 7 */ + }; + /* The UTF8String length above is baked in, so val7 must be exactly 7. */ + if (outSz < sizeof(prefix) + 7 || XSTRLEN(val7) != 7) + return 0; + XMEMCPY(out, prefix, sizeof(prefix)); + XMEMCPY(out + sizeof(prefix), val7, 7); + return (word32)(sizeof(prefix) + 7); +} + +/* Build a NameConstraints extension value carrying a single excludedSubtree + * ([1]) for the directoryName "CN=". */ +static word32 build_dirName_nameConstraints(byte* out, word32 outSz, + const char* val7) +{ + static const byte nc[] = { + 0x30, 0x1A, /* SEQUENCE, 26 */ + 0xA1, 0x18, /* [1] excluded, 24 */ + 0x30, 0x16, /* GeneralSubtree, 22 */ + 0xA4, 0x14, /* [4] dirName, 20 */ + 0x30, 0x12, /* Name SEQUENCE, 18 */ + 0x31, 0x10, /* RDN SET, 16 */ + 0x30, 0x0E, /* ATV SEQUENCE, 14 */ + 0x06, 0x03, 0x55, 0x04, 0x03, /* OID 2.5.4.3 (CN) */ + 0x0C, 0x07 /* UTF8String, 7 */ + }; + /* The UTF8String length above is baked in, so val7 must be exactly 7. */ + if (outSz < sizeof(nc) + 7 || XSTRLEN(val7) != 7) + return 0; + XMEMCPY(out, nc, sizeof(nc)); + XMEMCPY(out + sizeof(nc), val7, 7); + return (word32)(sizeof(nc) + 7); +} + /* Build a NameConstraints extension value with a single excludedSubtree * carrying a registeredID GeneralName for OID 1.2.3.4. wolfSSL enforces * registeredID name constraints by byte-comparing OID bodies, so a leaf @@ -24600,6 +24645,71 @@ static int test_NameConstraints_OtherName(void) return EXPECT_RESULT(); } +/* Verifies wolfSSL applies an issuing CA's directoryName nameConstraints to a + * leaf's directoryName SAN entries, not just to the subject field (RFC 5280 + * 4.2.1.10: "Restrictions of the form directoryName MUST be applied to the + * subject field ... and to any names of type directoryName in the + * subjectAltName extension"). The leaf subject never matches the constraint, + * so only the SAN can trigger it. + * + * Coverage: + * 1. Critical excluded subtree, leaf dirName SAN matches -> reject + * 2. Critical excluded subtree, leaf dirName SAN differs -> accept + * (positive control: pins the rejection to the matching path) + * 3. Non-critical excluded subtree, leaf SAN matches -> reject + * (excluded is enforced regardless of criticality) + * + * A permitted-subtree case is not included: for directoryName the leaf's + * subject field is always checked against the permitted list as well, so the + * generated leaf's subject would drive the result rather than its SAN. + */ +static int test_NameConstraints_DirName(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_ASN_TEMPLATE) && \ + defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \ + defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \ + defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \ + defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS) + byte sanBlocked[64]; + byte sanAllowed[64]; + byte ncExcludedBlocked[64]; + word32 sanBlockedSz, sanAllowedSz; + word32 ncExcludedBlockedSz; + + sanBlockedSz = build_dirName_san(sanBlocked, sizeof(sanBlocked), "blocked"); + sanAllowedSz = build_dirName_san(sanAllowed, sizeof(sanAllowed), "allowed"); + ncExcludedBlockedSz = build_dirName_nameConstraints( + ncExcludedBlocked, sizeof(ncExcludedBlocked), "blocked"); + ExpectIntGT((int)sanBlockedSz, 0); + ExpectIntGT((int)sanAllowedSz, 0); + ExpectIntGT((int)ncExcludedBlockedSz, 0); + + /* (1) Critical excluded directoryName matches the leaf's dirName SAN. + * Must be rejected. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 1, + sanBlocked, sanBlockedSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (2) Positive control: same excluded subtree, leaf carries a DIFFERENT + * dirName SAN, so no match and the chain MUST verify. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 1, + sanAllowed, sanAllowedSz), + 0); + + /* (3) Non-critical excluded subtree, leaf SAN matches: exclusion is + * enforced regardless of criticality. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 0, + sanBlocked, sanBlockedSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); +#endif + return EXPECT_RESULT(); +} + #if defined(WOLFSSL_ASN_TEMPLATE) && \ defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \ defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \ @@ -37030,6 +37140,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_PathLenSelfIssuedAllowed), TEST_DECL(test_PathLenNoKeyUsage), TEST_DECL(test_NameConstraints_OtherName), + TEST_DECL(test_NameConstraints_DirName), TEST_DECL(test_NameConstraints_DnsUriWildcard), TEST_DECL(test_NameConstraints_SubtreeMinMax), TEST_DECL(test_ParseSerial0FixtureMatrix), diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 25c203a4b2..973fd494dc 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -1024,13 +1024,8 @@ int test_wolfSSL_CertManagerNameConstraint2(void) wolfSSL_X509_sign(x509, priv, EVP_sha256()); #endif ExpectNotNull((der = wolfSSL_X509_get_der(x509, &derSz))); -#ifndef WOLFSSL_NO_ASN_STRICT ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); -#else - ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, - WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); -#endif /* check that it still fails if one bad altname and one good altname is in * the certificate */ @@ -1050,13 +1045,8 @@ int test_wolfSSL_CertManagerNameConstraint2(void) wolfSSL_X509_sign(x509, priv, EVP_sha256()); #endif ExpectNotNull((der = wolfSSL_X509_get_der(x509, &derSz))); -#ifndef WOLFSSL_NO_ASN_STRICT ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); -#else - ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, - WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); -#endif /* check it fails with switching position of bad altname */ wolfSSL_X509_free(x509); @@ -1075,13 +1065,8 @@ int test_wolfSSL_CertManagerNameConstraint2(void) wolfSSL_X509_sign(x509, priv, EVP_sha256()); #endif ExpectNotNull((der = wolfSSL_X509_get_der(x509, &derSz))); -#ifndef WOLFSSL_NO_ASN_STRICT ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); -#else - ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, - WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); -#endif wolfSSL_CertManagerFree(cm); wolfSSL_X509_free(x509); @@ -1110,13 +1095,8 @@ int test_wolfSSL_CertManagerNameConstraint2(void) wolfSSL_X509_sign(x509, priv, EVP_sha256()); #endif ExpectNotNull((der = wolfSSL_X509_get_der(x509, &derSz))); -#ifndef WOLFSSL_NO_ASN_STRICT ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); -#else - ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, - WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); -#endif wolfSSL_CertManagerFree(cm); wolfSSL_X509_free(x509); wolfSSL_X509_free(ca); diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 4006b1eb8f..cfd7752211 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -52,7 +52,14 @@ ASN Options: * RSA_DECODE_EXTRA: Decodes extra information in RSA public key. * WOLFSSL_CERT_GEN: Cert generation. Saves extra certificate info in GetName. * WOLFSSL_NO_ASN_STRICT: Disable strict RFC compliance checks to - restore 3.13.0 behavior. + restore 3.13.0 behavior. It no longer disables these RFC 5280 checks: + duplicate detection on the certificate extensions that use + VERIFY_AND_SET_OID (4.2), rejection of a critical extension whose OID is + unrecognized (4.2, subject to any WC_ASN_UNKNOWN_EXT_CB callback), and + application of directoryName name constraints to subjectAltName entries as + well as the subject (4.2.1.10). It still relaxes the critical check for a + recognized-but-unsupported extension (e.g. certificatePolicies without + WOLFSSL_SEP or WOLFSSL_CERT_EXT) and the CRL duplicate-extension check. * WOLFSSL_ASN_ALLOW_0_SERIAL: Even if WOLFSSL_NO_ASN_STRICT is not defined, allow a length=1, but zero value serial number. * WOLFSSL_NO_OCSP_OPTIONAL_CERTS: Skip optional OCSP certs (responder issuer @@ -19048,9 +19055,7 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) } break; case ASN_DIR_TYPE: - #ifndef WOLFSSL_NO_ASN_STRICT name = cert->altDirNames; - #endif /* RFC 5280 section 4.2.1.10 "Restrictions of the form directoryName MUST be