diff --git a/src/x509.c b/src/x509.c index 62159e99aac..3810bf20e79 100644 --- a/src/x509.c +++ b/src/x509.c @@ -17086,12 +17086,38 @@ int wolfSSL_X509_check_ip_asc(WOLFSSL_X509 *x, const char *ipasc, #endif #if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) +/* Compare a certificate email address against chk (which must not contain + * NULs) as RFC 5280 7.5 requires: the local-part exactly and the domain after + * the last '@' case-insensitively. + * + * returns 1 on a match and 0 otherwise. + */ +static int X509EmailMatch(const char *email, size_t emailLen, const char *chk, + size_t chkLen) +{ + size_t at; + + if (emailLen != chkLen) + return 0; + + /* at is the index just past the last '@' in chk, or 0 when there is + * none. */ + for (at = chkLen; at > 0 && chk[at - 1] != '@'; at--) + ; + + return (XMEMCMP(email, chk, at) == 0) && + (XSTRNCASECMP(email + at, chk + at, chkLen - at) == 0); +} + int wolfSSL_X509_check_email(WOLFSSL_X509 *x, const char *chk, size_t chkLen, unsigned int flags) { WOLFSSL_X509_NAME *subjName; int emailLen; char *emailBuf; + DNS_entry *entry; + int haveSanEmail = 0; + size_t i; (void)flags; @@ -17102,6 +17128,47 @@ int wolfSSL_X509_check_email(WOLFSSL_X509 *x, const char *chk, size_t chkLen, return WOLFSSL_FAILURE; } + /* Replicate openssl behavior for chkLen: reject embedded NULs and drop a + * single trailing NUL. */ + if (chkLen == 0) { + chkLen = XSTRLEN(chk); + } + else { + for (i = 0; i < (chkLen > 1 ? chkLen - 1 : chkLen); i++) { + if (chk[i] == '\0') { + WOLFSSL_MSG("Embedded NUL in email address"); + return WOLFSSL_FAILURE; + } + } + if (chkLen > 1 && chk[chkLen - 1] == '\0') + chkLen--; + } + + /* rfc822Name SANs take precedence: the subject emailAddress is only + * checked when the certificate has none (RFC 6125 6.4.4). */ + for (entry = x->altNames; entry != NULL; entry = entry->next) { + if (entry->type == ASN_RFC822_TYPE) { + haveSanEmail = 1; + if (entry->len >= 0 && X509EmailMatch(entry->name, + (size_t)entry->len, chk, chkLen)) { + return WOLFSSL_SUCCESS; + } + } + } + if (haveSanEmail) + return WOLFSSL_FAILURE; + +#if defined(IGNORE_NAME_CONSTRAINTS) || \ + (defined(WOLFSSL_X509_TINY) && !defined(WOLFSSL_X509_TINY_SAN_EMAIL)) + /* rfc822Name SANs are not kept when a certificate is decoded in this + * build, so a SAN extension may hold email identities that cannot be seen + * here. Do not fall back to the subject when one is present. */ + if (x->subjAltNameSet) { + WOLFSSL_MSG("rfc822Name SANs not available, not checking subject"); + return WOLFSSL_FAILURE; + } +#endif + subjName = wolfSSL_X509_get_subject_name(x); if (subjName == NULL) return WOLFSSL_FAILURE; @@ -17125,11 +17192,7 @@ int wolfSSL_X509_check_email(WOLFSSL_X509 *x, const char *chk, size_t chkLen, return WOLFSSL_FAILURE; } - if (chkLen == 0) - chkLen = XSTRLEN(chk); - - if (chkLen != (size_t)emailLen - || XSTRNCMP(chk, emailBuf, chkLen)) { + if (!X509EmailMatch(emailBuf, (size_t)emailLen, chk, chkLen)) { XFREE(emailBuf, x->heap, DYNAMIC_TYPE_OPENSSL); return WOLFSSL_FAILURE; } diff --git a/tests/api/test_ossl_x509.c b/tests/api/test_ossl_x509.c index 6dde5a998c1..2f85759a046 100644 --- a/tests/api/test_ossl_x509.c +++ b/tests/api/test_ossl_x509.c @@ -483,6 +483,16 @@ int test_wolfSSL_X509_check_host_len(void) return EXPECT_RESULT(); } +#if defined(IGNORE_NAME_CONSTRAINTS) || \ + (defined(WOLFSSL_X509_TINY) && !defined(WOLFSSL_X509_TINY_SAN_EMAIL)) +/* rfc822Name SANs are not decoded in this build, so + * wolfSSL_X509_check_email() fails for any certificate with a SAN extension + * rather than trusting the subject emailAddress. */ +#define EMAIL_SAN_RES(r) WC_NO_ERR_TRACE(WOLFSSL_FAILURE) +#else +#define EMAIL_SAN_RES(r) (r) +#endif + int test_wolfSSL_X509_check_email(void) { EXPECT_DECLS; @@ -506,12 +516,13 @@ int test_wolfSSL_X509_check_email(void) /* Should fail on non-matching email address */ ExpectIntEQ(wolfSSL_X509_check_email(x509, badEmail, XSTRLEN(badEmail), 0), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); - /* Should succeed on matching email address */ + /* Should succeed on matching email address (the certificate has only + * dNSName and iPAddress SANs) */ ExpectIntEQ(wolfSSL_X509_check_email(x509, goodEmail, XSTRLEN(goodEmail), - 0), WOLFSSL_SUCCESS); + 0), EMAIL_SAN_RES(WOLFSSL_SUCCESS)); /* Should compute length internally when not provided */ ExpectIntEQ(wolfSSL_X509_check_email(x509, goodEmail, 0, 0), - WOLFSSL_SUCCESS); + EMAIL_SAN_RES(WOLFSSL_SUCCESS)); /* Should fail when email address is NULL */ ExpectIntEQ(wolfSSL_X509_check_email(x509, NULL, 0, 0), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); @@ -526,6 +537,153 @@ int test_wolfSSL_X509_check_email(void) return EXPECT_RESULT(); } +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \ + defined(WOLFSSL_ALT_NAMES) && defined(HAVE_ECC) && \ + defined(HAVE_ECC_SIGN) && !defined(WC_NO_RNG) && !defined(NO_SHA256) +/* Make a self-signed certificate whose subject emailAddress is subjEmail + * (omitted when NULL) and whose SubjectAltName holds the GeneralNames in + * sans: each entry is a tag byte ('@' for rfc822Name, 'd' for dNSName) + * followed by the name, and the list ends with NULL. */ +static X509* make_email_cert(ecc_key* key, WC_RNG* rng, const char* subjEmail, + const char* const* sans) +{ + Cert cert; + byte der[FOURK_BUF]; + const byte* p = der; + word32 sanSz = 2; + int derSz; + size_t len; + int i; + + if (wc_InitCert(&cert) != 0) + return NULL; + cert.sigType = CTC_SHA256wECDSA; + XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.org, "wolfSSL_test", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.commonName, "check_email", CTC_NAME_SIZE - 1); + if (subjEmail != NULL) + XSTRNCPY(cert.subject.email, subjEmail, CTC_NAME_SIZE - 1); + for (i = 0; sans != NULL && sans[i] != NULL; i++) { + len = XSTRLEN(sans[i] + 1); + if (sanSz + 2 + len > 0x7f + 2) + return NULL; + cert.altNames[sanSz++] = (sans[i][0] == '@') ? + (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) : + (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE); + cert.altNames[sanSz++] = (byte)len; + XMEMCPY(cert.altNames + sanSz, sans[i] + 1, len); + sanSz += (word32)len; + } + if (sanSz > 2) { + cert.altNames[0] = ASN_SEQUENCE | ASN_CONSTRUCTED; + cert.altNames[1] = (byte)(sanSz - 2); + cert.altNamesSz = (int)sanSz; + } + if (wc_MakeCert(&cert, der, sizeof(der), NULL, key, rng) < 0) + return NULL; + derSz = wc_SignCert(cert.bodySz, cert.sigType, der, sizeof(der), NULL, key, + rng); + if (derSz < 0) + return NULL; + return d2i_X509(NULL, &p, derSz); +} +#endif + +/* Regression: wolfSSL_X509_check_email() must check rfc822Name SANs and only + * fall back to the subject emailAddress when the certificate has no + * rfc822Name SAN (RFC 6125 6.4.4, as OpenSSL's X509_check_email() does). */ +int test_wolfSSL_X509_check_email_san(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \ + defined(WOLFSSL_ALT_NAMES) && defined(HAVE_ECC) && \ + defined(HAVE_ECC_SIGN) && !defined(WC_NO_RNG) && !defined(NO_SHA256) + static const char target[] = "a@target.example"; + static const char nulTarget[] = "a@target.example\0junk"; + static const char upperDomain[] = "a@TARGET.Example"; + static const char upperLocal[] = "A@target.example"; + static const char* const sanTarget[] = { "@a@target.example", NULL }; + static const char* const sanAttacker[] = { "@a@attacker.example", NULL }; + static const char* const sanOther[] = { "@b@other.example", NULL }; + static const char* const sanSecond[] = { "@b@other.example", + "@a@target.example", NULL }; + static const char* const sanDnsOnly[] = { "dtarget.example", NULL }; + static const struct { + const char* subjEmail; + const char* const* sans; + int expect; + } cases[] = { + /* Subject and SAN both match. */ + { target, sanTarget, EMAIL_SAN_RES(WOLFSSL_SUCCESS) }, + /* Subject matches but the rfc822Name SAN names another mailbox. */ + { target, sanAttacker, WC_NO_ERR_TRACE(WOLFSSL_FAILURE) }, + /* SAN matches, subject does not. */ + { "b@other.example", sanTarget, EMAIL_SAN_RES(WOLFSSL_SUCCESS) }, + /* SAN matches, no subject emailAddress. */ + { NULL, sanTarget, EMAIL_SAN_RES(WOLFSSL_SUCCESS) }, + /* Neither matches. */ + { "b@other.example", sanOther, WC_NO_ERR_TRACE(WOLFSSL_FAILURE) }, + /* Second of two rfc822Name SANs matches. */ + { NULL, sanSecond, EMAIL_SAN_RES(WOLFSSL_SUCCESS) }, + /* No SAN: subject fallback. */ + { target, NULL, WOLFSSL_SUCCESS }, + /* Only a dNSName SAN: subject fallback still applies. */ + { target, sanDnsOnly, EMAIL_SAN_RES(WOLFSSL_SUCCESS) }, + }; + WC_RNG rng; + ecc_key key; + int rngI = 0, keyI = 0; + X509* x509 = NULL; + size_t i; + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngI = 1; + ExpectIntEQ(wc_ecc_init(&key), 0); + if (EXPECT_SUCCESS()) keyI = 1; + ExpectIntEQ(wc_ecc_make_key(&rng, 32, &key), 0); + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + ExpectNotNull(x509 = make_email_cert(&key, &rng, cases[i].subjEmail, + cases[i].sans)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, target, 0, 0), + cases[i].expect); + X509_free(x509); + x509 = NULL; + } + + /* chkLen handling matches OpenSSL: one trailing NUL is dropped and an + * embedded NUL never matches. The domain is compared case-insensitively + * and the local-part exactly (RFC 5280 7.5). Checked against both the SAN + * and the subject paths. */ + ExpectNotNull(x509 = make_email_cert(&key, &rng, NULL, sanTarget)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, target, sizeof(target), 0), + EMAIL_SAN_RES(WOLFSSL_SUCCESS)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, nulTarget, + sizeof(nulTarget) - 1, 0), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, upperDomain, 0, 0), + EMAIL_SAN_RES(WOLFSSL_SUCCESS)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, upperLocal, 0, 0), + WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + X509_free(x509); + x509 = NULL; + ExpectNotNull(x509 = make_email_cert(&key, &rng, target, NULL)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, target, sizeof(target), 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_X509_check_email(x509, nulTarget, + sizeof(nulTarget) - 1, 0), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + ExpectIntEQ(wolfSSL_X509_check_email(x509, upperDomain, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_X509_check_email(x509, upperLocal, 0, 0), + WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + X509_free(x509); + x509 = NULL; + + if (keyI) wc_ecc_free(&key); + if (rngI) wc_FreeRng(&rng); +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_X509(void) { EXPECT_DECLS; diff --git a/tests/api/test_ossl_x509.h b/tests/api/test_ossl_x509.h index 1d3771ca5e5..93a313684a0 100644 --- a/tests/api/test_ossl_x509.h +++ b/tests/api/test_ossl_x509.h @@ -35,6 +35,7 @@ int test_wolfSSL_X509_issuer_name_hash(void); int test_wolfSSL_X509_check_host(void); int test_wolfSSL_X509_check_host_len(void); int test_wolfSSL_X509_check_email(void); +int test_wolfSSL_X509_check_email_san(void); int test_wolfSSL_X509(void); int test_wolfSSL_X509_get0_tbs_sigalg(void); int test_wolfSSL_X509_set_name(void); @@ -71,6 +72,7 @@ int test_wolfSSL_X509_cmp(void); TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_host), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_host_len), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_email), \ + TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_check_email_san), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_get0_tbs_sigalg), \ TEST_DECL_GROUP("ossl_x509", test_wolfSSL_X509_set_name), \