diff --git a/examples/client/client.c b/examples/client/client.c index 900c7bdfd91..4a60f3b6a18 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -155,7 +155,7 @@ static int quieter = 0; /* Print fewer messages. This is helpful with overly #ifdef HAVE_SESSION_TICKET #ifndef SESSION_TICKET_LEN -#define SESSION_TICKET_LEN 256 +#define SESSION_TICKET_LEN 2048 #endif static int sessionTicketCB(WOLFSSL* ssl, const unsigned char* ticket, int ticketSz, diff --git a/src/internal.c b/src/internal.c index af7a695f413..8a72a675d86 100644 --- a/src/internal.c +++ b/src/internal.c @@ -38542,6 +38542,11 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) if((ret=ALPN_Select(ssl))) goto out; #endif + #if defined(HAVE_SESSION_TICKET) && \ + (defined(HAVE_SNI) || defined(HAVE_ALPN)) + if((ret=VerifyTicketBinding(ssl))) + goto out; + #endif i += totalExtSz; #else @@ -39323,6 +39328,77 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) return ret; } +#ifdef HAVE_SNI + /* Hash server-selected SNI; zeros dst when none. */ + static int TicketSniHash(WOLFSSL* ssl, byte* dst) + { + char* name = NULL; + word16 nameLen; + + nameLen = TLSX_SNI_GetRequest(ssl->extensions, + WOLFSSL_SNI_HOST_NAME, + (void**)&name, 0); + if (name != NULL && nameLen > 0) { + return wc_Hash(TICKET_BINDING_HASH_TYPE, (const byte*)name, + nameLen, dst, TICKET_BINDING_HASH_SZ); + } + + XMEMSET(dst, 0, TICKET_BINDING_HASH_SZ); + return 0; + } +#endif + +#ifdef HAVE_ALPN + /* Hash negotiated ALPN; zeros dst when none. */ + static int TicketAlpnHash(WOLFSSL* ssl, byte* dst) + { + char* proto = NULL; + word16 protoLen = 0; + + if (TLSX_ALPN_GetRequest(ssl->extensions, (void**)&proto, + &protoLen) == WOLFSSL_SUCCESS && + proto != NULL && protoLen > 0) { + return wc_Hash(TICKET_BINDING_HASH_TYPE, (const byte*)proto, + protoLen, dst, TICKET_BINDING_HASH_SZ); + } + + XMEMSET(dst, 0, TICKET_BINDING_HASH_SZ); + return 0; + } +#endif + +#if defined(HAVE_SNI) || defined(HAVE_ALPN) + /* Server-side: verify the SNI/ALPN bindings carried on a resumed + * session match what was negotiated for the current connection. + * Must be called after extension parsing and ALPN_Select. + * Returns 0 on match, WOLFSSL_FATAL_ERROR on mismatch. */ + int VerifyTicketBinding(WOLFSSL* ssl) + { + byte curHash[TICKET_BINDING_HASH_SZ]; + + if (!ssl->options.resuming || !ssl->options.useTicket) + return 0; + +#ifdef HAVE_SNI + if (TicketSniHash(ssl, curHash) != 0 || + XMEMCMP(curHash, ssl->session->sniHash, + TICKET_BINDING_HASH_SZ) != 0) { + WOLFSSL_MSG("Ticket SNI mismatch"); + return WOLFSSL_FATAL_ERROR; + } +#endif +#ifdef HAVE_ALPN + if (TicketAlpnHash(ssl, curHash) != 0 || + XMEMCMP(curHash, ssl->session->alpnHash, + TICKET_BINDING_HASH_SZ) != 0) { + WOLFSSL_MSG("Ticket ALPN mismatch"); + return WOLFSSL_FATAL_ERROR; + } +#endif + return 0; + } +#endif + /* create a new session ticket, 0 on success * Do any kind of setup in SetupTicket */ int CreateTicket(WOLFSSL* ssl) @@ -39421,6 +39497,18 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) it->sessionCtxSz = ssl->sessionCtxSz; XMEMCPY(it->sessionCtx, ssl->sessionCtx, ID_LEN); #endif +#ifdef HAVE_SNI + ret = TicketSniHash(ssl, it->sniHash); + if (ret != 0) + goto error; + XMEMCPY(ssl->session->sniHash, it->sniHash, TICKET_BINDING_HASH_SZ); +#endif +#ifdef HAVE_ALPN + ret = TicketAlpnHash(ssl, it->alpnHash); + if (ret != 0) + goto error; + XMEMCPY(ssl->session->alpnHash, it->alpnHash, TICKET_BINDING_HASH_SZ); +#endif #if defined(OPENSSL_ALL) && defined(KEEP_PEER_CERT) && \ !defined(NO_CERT_IN_TICKET) @@ -39776,6 +39864,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) ssl->sessionCtxSz) != 0)) return WOLFSSL_FATAL_ERROR; #endif + /* SNI/ALPN binding is verified after ALPN_Select via + * VerifyTicketBinding(). */ return 0; } #endif /* WOLFSSL_SLT13 */ @@ -39787,36 +39877,54 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) word16 peerCertLen = 0; ato16(it->peerCertLen, &peerCertLen); - if (peerCertLen > 0 && peerCertLen <= MAX_TICKET_PEER_CERT_SZ) { + /* Clear any peer cert state that may have been copied from the session + * cache by wolfSSL_DupSession before we got here. */ + FreeX509(&ssl->peerCert); + InitX509(&ssl->peerCert, 0, ssl->heap); #ifdef SESSION_CERTS - /* Clear existing chain and add the peer certificate */ - ssl->session->chain.count = 0; - AddSessionCertToChain(&ssl->session->chain, - it->peerCert, peerCertLen); + ssl->session->chain.count = 0; #endif - /* Also decode into ssl->peerCert for direct access */ - { - int ret; - DecodedCert* dCert; - - dCert = (DecodedCert*)XMALLOC(sizeof(DecodedCert), ssl->heap, - DYNAMIC_TYPE_DCERT); - if (dCert != NULL) { - InitDecodedCert(dCert, it->peerCert, peerCertLen, ssl->heap); - ret = ParseCertRelative(dCert, CERT_TYPE, 0, NULL, NULL); - if (ret == 0) { + + if (peerCertLen > 0 && peerCertLen <= MAX_TICKET_PEER_CERT_SZ) { + int ret; + DecodedCert* dCert; + + dCert = (DecodedCert*)XMALLOC(sizeof(DecodedCert), ssl->heap, + DYNAMIC_TYPE_DCERT); + if (dCert != NULL) { + int verify = ssl->options.verifyPeer ? VERIFY : NO_VERIFY; + InitDecodedCert(dCert, it->peerCert, peerCertLen, ssl->heap); + /* Re-verify against the current trust store so that CA + * removal since ticket issue is enforced. */ + ret = ParseCertRelative(dCert, CERT_TYPE, verify, + SSL_CM(ssl), NULL); + #ifdef HAVE_OCSP + /* ParseCertRelative does not check revocation status. + * Run OCSP if the CertManager has it enabled. */ + if (ret == 0 && SSL_CM(ssl)->ocspEnabled) { + ret = CheckCertOCSP_ex(SSL_CM(ssl)->ocsp, dCert, ssl); + } + #endif + #ifdef HAVE_CRL + if (ret == 0 && SSL_CM(ssl)->crlEnabled) { + ret = CheckCertCRL(SSL_CM(ssl)->crl, dCert); + } + #endif + if (ret == 0) { + #ifdef SESSION_CERTS + AddSessionCertToChain(&ssl->session->chain, + it->peerCert, peerCertLen); + #endif + FreeX509(&ssl->peerCert); + InitX509(&ssl->peerCert, 0, ssl->heap); + ret = CopyDecodedToX509(&ssl->peerCert, dCert); + if (ret != 0) { FreeX509(&ssl->peerCert); InitX509(&ssl->peerCert, 0, ssl->heap); - ret = CopyDecodedToX509(&ssl->peerCert, dCert); - if (ret != 0) { - /* Failed to copy - clear peerCert */ - FreeX509(&ssl->peerCert); - InitX509(&ssl->peerCert, 0, ssl->heap); - } } - FreeDecodedCert(dCert); - XFREE(dCert, ssl->heap, DYNAMIC_TYPE_DCERT); } + FreeDecodedCert(dCert); + XFREE(dCert, ssl->heap, DYNAMIC_TYPE_DCERT); } } } @@ -39853,6 +39961,14 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) } } #endif + /* Carry the ticket bindings on the session for the deferred + * VerifyTicketBinding() check. */ +#ifdef HAVE_SNI + XMEMCPY(ssl->session->sniHash, it->sniHash, TICKET_BINDING_HASH_SZ); +#endif +#ifdef HAVE_ALPN + XMEMCPY(ssl->session->alpnHash, it->alpnHash, TICKET_BINDING_HASH_SZ); +#endif if (!IsAtLeastTLSv1_3(ssl->version)) { if (ssl->arrays == NULL) @@ -39962,6 +40078,12 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) it->sessionCtxSz = sess->sessionCtxSz; XMEMCPY(it->sessionCtx, sess->sessionCtx, sess->sessionCtxSz); #endif +#ifdef HAVE_SNI + XMEMCPY(it->sniHash, sess->sniHash, TICKET_BINDING_HASH_SZ); +#endif +#ifdef HAVE_ALPN + XMEMCPY(it->alpnHash, sess->alpnHash, TICKET_BINDING_HASH_SZ); +#endif #if defined(OPENSSL_ALL) && defined(KEEP_PEER_CERT) && \ defined(SESSION_CERTS) && !defined(NO_CERT_IN_TICKET) /* Store peer certificate from session chain */ @@ -40206,6 +40328,8 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) goto cleanup; } + /* SNI/ALPN binding is verified after ALPN_Select via + * VerifyTicketBinding(). */ DoClientTicketFinalize(ssl, it, NULL); cleanup: diff --git a/src/ssl_sess.c b/src/ssl_sess.c index d1d4acf20d9..000a7dcfd9f 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -3069,6 +3069,7 @@ WOLFSSL_SESSION* wolfSSL_d2i_SSL_SESSION(WOLFSSL_SESSION** sess, (void)idx; if (sess != NULL) { + wolfSSL_FreeSession(NULL, *sess); *sess = s; } diff --git a/src/tls13.c b/src/tls13.c index 701dce71562..73ed5183743 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -7632,6 +7632,10 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, * select the ALPN protocol, if so requested */ if ((ret = ALPN_Select(ssl)) != 0) goto exit_dch; +#endif +#if defined(HAVE_SESSION_TICKET) && (defined(HAVE_SNI) || defined(HAVE_ALPN)) + if ((ret = VerifyTicketBinding(ssl)) != 0) + goto exit_dch; #endif } /* case TLS_ASYNC_BEGIN */ FALL_THROUGH; diff --git a/src/x509_str.c b/src/x509_str.c index 09870cb4382..abdb42b4e21 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -706,23 +706,13 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) /* We found our issuer in the non-trusted cert list, add it * to the CM and verify the current cert against it */ #ifndef WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE - /* RFC 5280 6.1.3(k): a non-self-issued intermediate must have - * basicConstraints CA:TRUE to be used as a signing authority. - * Reject CA:FALSE intermediates here; the verify_cb (if any) - * may override. Define WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE - * to restore the legacy permissive behavior. - */ + /* RFC 5280 4.2.1.9: reject non-CA issuer. verify_cb may + * suppress the INVALID_CA error to keep building the chain, + * but the leaf signature must still be verified against the + * issuer below - never skip X509StoreVerifyCert. */ if (!issuer->isCa) { - /* error depth is current depth + 1. The compat alias - * X509_V_ERR_INVALID_CA (= 79) lives in wolfssl/openssl/x509.h - * which is not always pulled into this translation unit - * (e.g. some linuxkm build chains). Define a local fallback - * so callers reading X509_STORE_CTX_get_error() see the - * OpenSSL-compatible value. */ - #ifndef X509_V_ERR_INVALID_CA - #define X509_V_ERR_INVALID_CA 79 - #endif - SetupStoreCtxError_ex(ctx, X509_V_ERR_INVALID_CA, + /* error depth is current depth + 1 */ + SetupStoreCtxError_ex(ctx, WOLFSSL_X509_V_ERR_INVALID_CA, (ctx->chain) ? (int)(ctx->chain->num + 1) : 1); #if defined(OPENSSL_ALL) || defined(WOLFSSL_QT) if (ctx->store->verify_cb) { @@ -738,28 +728,25 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) ret = WOLFSSL_FAILURE; goto exit; } - } else + } #endif - { - ret = X509StoreAddCa(ctx->store, issuer, - WOLFSSL_TEMP_CA); - if (ret != WOLFSSL_SUCCESS) { - X509VerifyCertSetupRetry(ctx, certs, failedCerts, - &depth, origDepth); - continue; - } - added = 1; - ret = X509StoreVerifyCert(ctx); - if (ret != WOLFSSL_SUCCESS) { - if ((origDepth - depth) <= 1) - added = 0; - X509VerifyCertSetupRetry(ctx, certs, failedCerts, - &depth, origDepth); - continue; - } - /* Add it to the current chain and look at the issuer cert next */ - wolfSSL_sk_X509_push(ctx->chain, ctx->current_cert); + ret = X509StoreAddCa(ctx->store, issuer, WOLFSSL_TEMP_CA); + if (ret != WOLFSSL_SUCCESS) { + X509VerifyCertSetupRetry(ctx, certs, failedCerts, + &depth, origDepth); + continue; } + added = 1; + ret = X509StoreVerifyCert(ctx); + if (ret != WOLFSSL_SUCCESS) { + if ((origDepth - depth) <= 1) + added = 0; + X509VerifyCertSetupRetry(ctx, certs, failedCerts, + &depth, origDepth); + continue; + } + /* Add it to the current chain and look at the issuer cert next */ + wolfSSL_sk_X509_push(ctx->chain, ctx->current_cert); ctx->current_cert = issuer; } else if (ret == WC_NO_ERR_TRACE(WOLFSSL_FAILURE)) { diff --git a/tests/api.c b/tests/api.c index 389cf8e5180..f175b287b80 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30232,9 +30232,11 @@ static int error_test(void) {11, 11}, {17, 15}, {19, 19}, + {24, 24}, {27, 26 }, {61, 30}, {63, 63}, + {78, 65}, #endif { -9, WC_SPAN1_FIRST_E + 1 }, { -300, -300 }, diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 64be65084f3..36e4ec72c1e 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -969,7 +969,7 @@ int test_DecodeAltNames_length_underflow(void) 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, /* SAN extension: correct SEQUENCE length 0x06 */ 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x08, 0x30, 0x06, 0x82, - 0x04, 0x61, 0x2a, 0x00, 0x2a, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, + 0x04, 0x61, 0x2a, 0x62, 0x2a, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0x92, 0x6a, 0x1e, 0x52, 0x3a, 0x1a, 0x57, 0x9f, 0xc9, 0x82, 0x9a, 0xce, 0xc8, 0xc0, 0xa9, 0x51, 0x9d, 0x2f, 0xc7, 0x72, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, @@ -1025,6 +1025,16 @@ int test_DecodeAltNames_length_underflow(void) WC_NO_ERR_TRACE(ASN_PARSE_E)); wc_FreeDecodedCert(&cert); + /* NUL in dNSName SAN must be rejected per RFC 5280 4.2.1.6. */ + XMEMCPY(bad_san_cert, good_san_cert, sizeof(good_san_cert)); + bad_san_cert[SAN_SEQ_LEN_OFFSET + 5] = 0x00; + + wc_InitDecodedCert(&cert, bad_san_cert, (word32)sizeof(bad_san_cert), + NULL); + ExpectIntEQ(wc_ParseCert(&cert, CERT_TYPE, NO_VERIFY, NULL), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + wc_FreeDecodedCert(&cert); + #endif /* !NO_CERTS && !NO_RSA && !NO_ASN */ return EXPECT_RESULT(); } diff --git a/tests/api/test_ossl_x509.c b/tests/api/test_ossl_x509.c index 1244a0869fd..e1cf3aa0f41 100644 --- a/tests/api/test_ossl_x509.c +++ b/tests/api/test_ossl_x509.c @@ -1144,7 +1144,7 @@ int test_wolfSSL_X509_bad_altname(void) 0xf5, 0xe5, 0x09, 0x02, 0x01, 0x03, 0xa3, 0x61, 0x30, 0x5f, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x08, 0x30, 0x06, 0x82, - 0x04, 0x61, 0x2a, 0x00, 0x2a, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, + 0x04, 0x61, 0x2a, 0x62, 0x2a, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0x92, 0x6a, 0x1e, 0x52, 0x3a, 0x1a, 0x57, 0x9f, 0xc9, 0x82, 0x9a, 0xce, 0xc8, 0xc0, 0xa9, 0x51, 0x9d, 0x2f, 0xc7, 0x72, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, @@ -1183,8 +1183,7 @@ int test_wolfSSL_X509_bad_altname(void) ExpectNotNull(x509 = wolfSSL_X509_load_certificate_buffer( malformed_alt_name_cert, certSize, SSL_FILETYPE_ASN1)); - /* malformed_alt_name_cert has a malformed alternative - * name of "a*\0*". Ensure that it does not match "aaaaa" */ + /* SAN "a*b*" must not match "aaaaa" under any wildcard flag. */ ExpectIntNE(wolfSSL_X509_check_host(x509, name, nameLen, WOLFSSL_ALWAYS_CHECK_SUBJECT, NULL), 1); diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 81872bc985c..9afd09fa8c6 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -5222,6 +5222,11 @@ int test_tls13_empty_record_limit(void) return EXPECT_RESULT(); } +/* Test that a TLS 1.3 NewSessionTicket with a ticket shorter than ID_LEN + * (32 bytes) does not cause an unsigned integer underflow / OOB read in + * SetTicket. Uses a full memio handshake, then injects a crafted + * NewSessionTicket with a 5-byte ticket into the client's read path. */ + int test_tls13_short_session_ticket(void) { EXPECT_DECLS; @@ -5732,3 +5737,113 @@ int test_tls13_serverhello_bad_cipher_suites(void) #endif return EXPECT_RESULT(); } + +/* Verify that a peer certificate restored from a session ticket is re-verified + * against the current trust store. After CA removal, the cert must not be + * installed into ssl->peerCert even though the ticket itself decrypts fine. */ +int test_tls13_ticket_peer_cert_reverify(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(OPENSSL_ALL) && defined(KEEP_PEER_CERT) && \ + !defined(NO_CERT_IN_TICKET) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_RSA) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + WOLFSSL_X509 *peer = NULL; + char readBuf[64]; + + /* --- Step 1: mTLS handshake, obtain a session ticket --- */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* Set up CTXs manually so we can configure mTLS before SSL creation */ + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, caCertFile, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + wolfSSL_SetIORecv(ctx_c, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_c, test_memio_write_cb); + + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_s, svrCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + /* Server trusts both its own CA and the client CA for mTLS */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, caCertFile, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, + "certs/client-ca.pem", 0), WOLFSSL_SUCCESS); + wolfSSL_CTX_set_verify(ctx_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + wolfSSL_SetIORecv(ctx_s, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_s, test_memio_write_cb); + + /* Create SSL objects from fully-configured CTXs */ + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Drain post-handshake NewSessionTicket */ + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + /* Peer cert should be available after initial handshake */ + ExpectNotNull(peer = wolfSSL_get_peer_certificate(ssl_s)); + wolfSSL_X509_free(peer); + peer = NULL; + + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* --- Step 2: remove the client CA from the server trust store --- */ + ExpectIntEQ(wolfSSL_CTX_UnloadCAs(ctx_s), WOLFSSL_SUCCESS); + /* Re-load only the server's own CA so TLS works, but not the client CA */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, caCertFile, 0), + WOLFSSL_SUCCESS); + + /* --- Step 3: resume with the old ticket --- */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + /* Resumption handshake succeeds (the ticket master secret is fine) */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The session should have been resumed via PSK. */ + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 1); + /* But the peer cert must NOT be restored because the issuing CA is + * no longer in the trust store. Check the peerCert directly rather + * than wolfSSL_get_peer_certificate which has a session-chain + * fallback that may see stale cache state. */ + ExpectIntEQ(ssl_s->peerCert.issuer.sz, 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index d508465e9a0..e7a3bb775b2 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -66,6 +66,7 @@ int test_tls13_cert_with_extern_psk_requires_key_share(void); int test_tls13_cert_with_extern_psk_rejects_resumption(void); int test_tls13_cert_with_extern_psk_sh_missing_key_share(void); int test_tls13_cert_with_extern_psk_sh_confirms_resumption(void); +int test_tls13_ticket_peer_cert_reverify(void); #define TEST_TLS13_DECLS \ TEST_DECL_GROUP("tls13", test_tls13_apis), \ @@ -109,6 +110,7 @@ int test_tls13_cert_with_extern_psk_sh_confirms_resumption(void); TEST_DECL_GROUP("tls13", test_tls13_cert_with_extern_psk_requires_key_share), \ TEST_DECL_GROUP("tls13", test_tls13_cert_with_extern_psk_rejects_resumption), \ TEST_DECL_GROUP("tls13", test_tls13_cert_with_extern_psk_sh_missing_key_share), \ - TEST_DECL_GROUP("tls13", test_tls13_cert_with_extern_psk_sh_confirms_resumption) + TEST_DECL_GROUP("tls13", test_tls13_cert_with_extern_psk_sh_confirms_resumption), \ + TEST_DECL_GROUP("tls13", test_tls13_ticket_peer_cert_reverify) #endif /* WOLFCRYPT_TEST_TLS13_H */ diff --git a/tests/test-fails.conf b/tests/test-fails.conf index 955a6c67ba5..74530e3e0e6 100644 --- a/tests/test-fails.conf +++ b/tests/test-fails.conf @@ -14,21 +14,6 @@ -m -x -# server bad certificate alternate name has null --v 3 --l ECDHE-RSA-AES128-GCM-SHA256 --k ./certs/server-key.pem --c ./certs/test/server-badaltnull.pem --d - -# client bad certificate alternate name has null --v 3 --l ECDHE-RSA-AES128-GCM-SHA256 --h localhost --A ./certs/test/server-badaltnull.pem --m --x - # server nomatch common name -v 3 -l ECDHE-RSA-AES128-GCM-SHA256 diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 6a7c6d4f676..d01420d769f 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18338,6 +18338,19 @@ static int DecodeOtherName(DecodedCert* cert, const byte* input, * @return ASN_UNKNOWN_OID_E when the OID cannot be verified. * @return MEMORY_E when dynamic memory allocation fails. */ +/* Reject IA5String SAN content that cannot legally appear in + * dNSName / rfc822Name / URI per RFC 5280 4.2.1.6. Currently just NUL. */ +static int DecodeGeneralNameCheckChars(const byte* input, int len) +{ + int i; + for (i = 0; i < len; i++) { + if (input[i] == 0) { + return ASN_PARSE_E; + } + } + return 0; +} + static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag, int len, DecodedCert* cert) { @@ -18346,6 +18359,10 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag, /* GeneralName choice: dnsName */ if (tag == (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE)) { + ret = DecodeGeneralNameCheckChars(input + idx, len); + if (ret != 0) { + return ret; + } ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len, ASN_DNS_TYPE, &cert->altNames); if (ret == 0) { @@ -18373,6 +18390,10 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag, } /* GeneralName choice: rfc822Name */ else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE)) { + ret = DecodeGeneralNameCheckChars(input + idx, len); + if (ret != 0) { + return ret; + } ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len, ASN_RFC822_TYPE, &cert->altEmailNames); if (ret == 0) { @@ -18381,6 +18402,10 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag, } /* GeneralName choice: uniformResourceIdentifier */ else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE)) { + ret = DecodeGeneralNameCheckChars(input + idx, len); + if (ret != 0) { + return ret; + } WOLFSSL_MSG("\tPutting URI into list but not using"); #if !defined(WOLFSSL_NO_ASN_STRICT) && !defined(WOLFSSL_FPKI) diff --git a/wolfcrypt/src/asn_orig.c b/wolfcrypt/src/asn_orig.c index 4572cfaa29e..5bf43c9008d 100644 --- a/wolfcrypt/src/asn_orig.c +++ b/wolfcrypt/src/asn_orig.c @@ -3200,6 +3200,19 @@ static int DecodeConstructedOtherName(DecodedCert* cert, const byte* input, return ret; } +/* Reject IA5String SAN content that cannot legally appear in + * dNSName / rfc822Name / URI per RFC 5280 4.2.1.6. Currently just NUL. */ +static int DecodeGeneralNameCheckChars(const byte* input, int len) +{ + int i; + for (i = 0; i < len; i++) { + if (input[i] == 0) { + return ASN_PARSE_E; + } + } + return 0; +} + static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert) { word32 idx = 0; @@ -3259,6 +3272,13 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert) } length -= (int)(idx - lenStartIdx); + if ((word32)strLen + idx > sz) { + return BUFFER_E; + } + if (DecodeGeneralNameCheckChars(&input[idx], strLen) != 0) { + return ASN_PARSE_E; + } + dnsEntry = AltNameNew(cert->heap); if (dnsEntry == NULL) { WOLFSSL_MSG("\tOut of Memory"); @@ -3344,6 +3364,13 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert) } length -= (int)(idx - lenStartIdx); + if ((word32)strLen + idx > sz) { + return BUFFER_E; + } + if (DecodeGeneralNameCheckChars(&input[idx], strLen) != 0) { + return ASN_PARSE_E; + } + emailEntry = AltNameNew(cert->heap); if (emailEntry == NULL) { WOLFSSL_MSG("\tOut of Memory"); @@ -3389,6 +3416,10 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert) return BUFFER_E; } + if (DecodeGeneralNameCheckChars(&input[idx], strLen) != 0) { + return ASN_PARSE_E; + } + #if !defined(WOLFSSL_NO_ASN_STRICT) && !defined(WOLFSSL_FPKI) /* Verify RFC 5280 Sec 4.2.1.6 rule: "The name MUST NOT be a relative URI" diff --git a/wolfssl/internal.h b/wolfssl/internal.h index f48de685cc2..256d3d76c22 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3508,6 +3508,24 @@ WOLFSSL_LOCAL int TLSX_AddEmptyRenegotiationInfo(TLSX** extensions, void* heap); #ifndef MAX_TICKET_PEER_CERT_SZ #define MAX_TICKET_PEER_CERT_SZ 2048 #endif +#if defined(HAVE_SNI) || defined(HAVE_ALPN) +/* Hash algorithm used for SNI/ALPN binding in session tickets. + * Pick the best available at compile time. */ +#ifndef TICKET_BINDING_HASH_TYPE + #if !defined(NO_SHA256) + #define TICKET_BINDING_HASH_TYPE WC_HASH_TYPE_SHA256 + #define TICKET_BINDING_HASH_SZ WC_SHA256_DIGEST_SIZE + #elif defined(WOLFSSL_SHA384) + #define TICKET_BINDING_HASH_TYPE WC_HASH_TYPE_SHA384 + #define TICKET_BINDING_HASH_SZ WC_SHA384_DIGEST_SIZE + #elif !defined(NO_SHA) + #define TICKET_BINDING_HASH_TYPE WC_HASH_TYPE_SHA + #define TICKET_BINDING_HASH_SZ WC_SHA_DIGEST_SIZE + #else + #error "No hash algorithm available for ticket binding" + #endif +#endif +#endif /* Our ticket format. All members need to be a byte or array of byte to * avoid alignment issues */ @@ -3530,6 +3548,14 @@ typedef struct InternalTicket { #ifdef WOLFSSL_TICKET_HAVE_ID byte id[ID_LEN]; #endif +#ifdef HAVE_SNI + byte sniHash[TICKET_BINDING_HASH_SZ]; /* digest of server name + * at ticket issue */ +#endif +#ifdef HAVE_ALPN + byte alpnHash[TICKET_BINDING_HASH_SZ]; /* digest of negotiated + * ALPN at issue */ +#endif #ifdef OPENSSL_EXTRA byte sessionCtxSz; /* sessionCtx length */ byte sessionCtx[ID_LEN]; /* app specific context id */ @@ -4771,6 +4797,12 @@ struct WOLFSSL_SESSION { byte* ticket; word16 ticketLen; word16 ticketLenAlloc; /* is dynamic */ +#ifdef HAVE_SNI + byte sniHash[TICKET_BINDING_HASH_SZ]; /* SNI at issue */ +#endif +#ifdef HAVE_ALPN + byte alpnHash[TICKET_BINDING_HASH_SZ]; /* ALPN at issue */ +#endif #endif #ifdef SESSION_CERTS @@ -6777,6 +6809,9 @@ WOLFSSL_LOCAL int DoClientTicket_ex(const WOLFSSL* ssl, PreSharedKey* psk, #endif WOLFSSL_LOCAL int DoClientTicket(WOLFSSL* ssl, const byte* input, word32 len); +#if defined(HAVE_SNI) || defined(HAVE_ALPN) +WOLFSSL_LOCAL int VerifyTicketBinding(WOLFSSL* ssl); +#endif #endif /* HAVE_SESSION_TICKET */ WOLFSSL_LOCAL int SendData(WOLFSSL* ssl, const void* data, size_t sz); #ifdef WOLFSSL_THREADED_CRYPT diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 597bf6a1d92..89c1fb331b2 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -2701,13 +2701,13 @@ enum { WOLFSSL_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE = 21, WOLFSSL_X509_V_ERR_CERT_CHAIN_TOO_LONG = 22, WOLFSSL_X509_V_ERR_CERT_REVOKED = 23, - WOLFSSL_X509_V_ERR_INVALID_CA = 24, WOLFSSL_X509_V_ERR_PATH_LENGTH_EXCEEDED = 25, WOLFSSL_X509_V_ERR_CERT_REJECTED = 28, WOLFSSL_X509_V_ERR_SUBJECT_ISSUER_MISMATCH = 29, - WOLFSSL_X509_V_ERR_HOSTNAME_MISMATCH = 62, - WOLFSSL_X509_V_ERR_IP_ADDRESS_MISMATCH = 64, - WC_OSSL_V509_V_ERR_MAX = 65, + WOLFSSL_X509_V_ERR_HOSTNAME_MISMATCH = 62, + WOLFSSL_X509_V_ERR_IP_ADDRESS_MISMATCH = 64, + WOLFSSL_X509_V_ERR_INVALID_CA = 79, + WC_OSSL_V509_V_ERR_MAX = 80, #ifdef HAVE_OCSP /* OCSP Flags */