diff --git a/examples/client/client.c b/examples/client/client.c index dfcfe5942ba..267b5309297 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 c10b89d6a67..ff0757e9cb4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -38468,6 +38468,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 @@ -39249,6 +39254,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) @@ -39347,6 +39423,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) @@ -39702,6 +39790,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 */ @@ -39713,36 +39803,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); } } } @@ -39779,6 +39887,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) @@ -39888,6 +40004,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 */ @@ -40132,6 +40254,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 ec790575057..146471350db 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 fba9a05cadc..2091a2563d7 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -7555,6 +7555,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 294a5a2eb29..5450080df80 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -705,14 +705,14 @@ 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 */ - #if defined(OPENSSL_ALL) || defined(WOLFSSL_QT) - /* OpenSSL doesn't allow the cert as CA if it is not CA:TRUE for - * intermediate certs. - */ + /* 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 */ - SetupStoreCtxError_ex(ctx, X509_V_ERR_INVALID_CA, + 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) { ret = ctx->store->verify_cb(0, ctx); if (ret != WOLFSSL_SUCCESS) { @@ -720,32 +720,30 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) goto exit; } } - else { + else + #endif + { 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 05a7688d7ff..740b2da2d1f 100644 --- a/tests/api.c +++ b/tests/api.c @@ -28238,9 +28238,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 ce8546dc247..32854851f6a 100644 --- a/tests/api/test_ossl_x509.c +++ b/tests/api/test_ossl_x509.c @@ -1136,7 +1136,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, @@ -1175,8 +1175,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 ba12b7e3689..400286d6604 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -4579,10 +4579,6 @@ int test_tls13_pqc_hybrid_truncated_keyshare(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_empty_record_limit(void) { EXPECT_DECLS; @@ -4754,6 +4750,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; @@ -5264,3 +5265,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 c8b42fc56c6..113eb13a08a 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -60,6 +60,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), \ @@ -97,6 +98,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 9a3be56616f..2d5f0c650b7 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -18026,6 +18026,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) { @@ -18034,6 +18047,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) { @@ -18061,6 +18078,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) { @@ -18069,6 +18090,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 d6568aa5d11..a154f21ed04 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 96606ba4f97..0572d35648d 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3498,6 +3498,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 */ @@ -3520,6 +3538,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 */ @@ -4761,6 +4787,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 @@ -6758,6 +6790,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 236515157b4..dba3afa315d 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -2684,13 +2684,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 */