[Security] Cherry Pick Spiffe Verification (#40515)

Cherry pick #40476 to 1.75

RELEASE NOTES:
* Adds support for SPIFFE Bundle Maps in as roots of trust per [gRFC
A87](https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md)
and https://github.com/grpc/proposal/pull/506
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cd7aad2..42cf9eb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1768,6 +1768,10 @@
     add_dependencies(buildtests_cxx socket_utils_test)
   endif()
   add_dependencies(buildtests_cxx sorted_pack_test)
+  add_dependencies(buildtests_cxx spiffe_bundle_map_end2end_test)
+  if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
+    add_dependencies(buildtests_cxx spiffe_ssl_transport_security_test)
+  endif()
   add_dependencies(buildtests_cxx spiffe_utils_test)
   add_dependencies(buildtests_cxx spinlock_test)
   if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
@@ -1951,6 +1955,9 @@
   if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     add_dependencies(buildtests_cxx xds_security_end2end_test)
   endif()
+  if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
+    add_dependencies(buildtests_cxx xds_security_spiffe_end2end_test)
+  endif()
   add_dependencies(buildtests_cxx xds_stats_watcher_test)
   if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     add_dependencies(buildtests_cxx xds_wrr_end2end_test)
@@ -29770,6 +29777,127 @@
 endif()
 if(gRPC_BUILD_TESTS)
 
+add_executable(spiffe_bundle_map_end2end_test
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/api/http.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/api/http.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/api/http.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/api/http.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/validate/validate.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/validate/validate.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/validate/validate.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/validate/validate.grpc.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.grpc.pb.cc
+  ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.pb.h
+  ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.grpc.pb.h
+  test/cpp/end2end/spiffe_bundle_map_end2end_test.cc
+  test/cpp/end2end/test_service_impl.cc
+)
+if(WIN32 AND MSVC)
+  if(BUILD_SHARED_LIBS)
+    target_compile_definitions(spiffe_bundle_map_end2end_test
+    PRIVATE
+      "GPR_DLL_IMPORTS"
+      "GRPC_DLL_IMPORTS"
+      "GRPCXX_DLL_IMPORTS"
+    )
+  endif()
+endif()
+target_compile_features(spiffe_bundle_map_end2end_test PUBLIC cxx_std_17)
+target_include_directories(spiffe_bundle_map_end2end_test
+  PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/include
+    ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
+    ${_gRPC_RE2_INCLUDE_DIR}
+    ${_gRPC_SSL_INCLUDE_DIR}
+    ${_gRPC_UPB_GENERATED_DIR}
+    ${_gRPC_UPB_GRPC_GENERATED_DIR}
+    ${_gRPC_UPB_INCLUDE_DIR}
+    ${_gRPC_XXHASH_INCLUDE_DIR}
+    ${_gRPC_ZLIB_INCLUDE_DIR}
+    third_party/googletest/googletest/include
+    third_party/googletest/googletest
+    third_party/googletest/googlemock/include
+    third_party/googletest/googlemock
+    ${_gRPC_PROTO_GENS_DIR}
+)
+
+target_link_libraries(spiffe_bundle_map_end2end_test
+  ${_gRPC_ALLTARGETS_LIBRARIES}
+  gtest
+  grpc++_test_util
+)
+
+
+endif()
+if(gRPC_BUILD_TESTS)
+if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
+
+  add_executable(spiffe_ssl_transport_security_test
+    test/core/tsi/spiffe_ssl_transport_security_test.cc
+    test/core/tsi/transport_security_test_lib.cc
+  )
+  if(WIN32 AND MSVC)
+    if(BUILD_SHARED_LIBS)
+      target_compile_definitions(spiffe_ssl_transport_security_test
+      PRIVATE
+        "GPR_DLL_IMPORTS"
+        "GRPC_DLL_IMPORTS"
+      )
+    endif()
+  endif()
+  target_compile_features(spiffe_ssl_transport_security_test PUBLIC cxx_std_17)
+  target_include_directories(spiffe_ssl_transport_security_test
+    PRIVATE
+      ${CMAKE_CURRENT_SOURCE_DIR}
+      ${CMAKE_CURRENT_SOURCE_DIR}/include
+      ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
+      ${_gRPC_RE2_INCLUDE_DIR}
+      ${_gRPC_SSL_INCLUDE_DIR}
+      ${_gRPC_UPB_GENERATED_DIR}
+      ${_gRPC_UPB_GRPC_GENERATED_DIR}
+      ${_gRPC_UPB_INCLUDE_DIR}
+      ${_gRPC_XXHASH_INCLUDE_DIR}
+      ${_gRPC_ZLIB_INCLUDE_DIR}
+      third_party/googletest/googletest/include
+      third_party/googletest/googletest
+      third_party/googletest/googlemock/include
+      third_party/googletest/googlemock
+      ${_gRPC_PROTO_GENS_DIR}
+  )
+
+  target_link_libraries(spiffe_ssl_transport_security_test
+    ${_gRPC_ALLTARGETS_LIBRARIES}
+    gtest
+    grpc_test_util
+  )
+
+
+endif()
+endif()
+if(gRPC_BUILD_TESTS)
+
 add_executable(spiffe_utils_test
   test/core/credentials/transport/tls/spiffe_utils_test.cc
 )
@@ -46930,6 +47058,562 @@
 endif()
 endif()
 if(gRPC_BUILD_TESTS)
+if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
+
+  add_executable(xds_security_spiffe_end2end_test
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/duplicate/echo_duplicate.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/duplicate/echo_duplicate.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/duplicate/echo_duplicate.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/duplicate/echo_duplicate.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/echo_messages.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/simple_messages.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/ads.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/ads.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/ads.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/ads.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/discovery.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/discovery.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/discovery.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/discovery.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/lrs.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/lrs.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/lrs.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/xds/v3/lrs.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/deprecation.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/deprecation.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/deprecation.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/deprecation.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/resource.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/resource.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/resource.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/annotations/resource.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/accesslog/v3/accesslog.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/accesslog/v3/accesslog.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/accesslog/v3/accesslog.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/accesslog/v3/accesslog.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/circuit_breaker.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/circuit_breaker.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/circuit_breaker.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/circuit_breaker.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/cluster.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/cluster.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/cluster.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/cluster.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/filter.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/filter.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/filter.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/filter.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/outlier_detection.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/outlier_detection.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/outlier_detection.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/cluster/v3/outlier_detection.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/address.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/address.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/address.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/address.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/backoff.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/backoff.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/backoff.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/backoff.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/base.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/base.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/base.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/base.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/config_source.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/config_source.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/config_source.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/config_source.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/event_service_config.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/event_service_config.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/event_service_config.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/event_service_config.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/extension.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/extension.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/extension.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/extension.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_method_list.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_method_list.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_method_list.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_method_list.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_service.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_service.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_service.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/grpc_service.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/health_check.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/health_check.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/health_check.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/health_check.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_service.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_service.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_service.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_service.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_uri.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_uri.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_uri.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/http_uri.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/protocol.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/protocol.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/protocol.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/protocol.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/proxy_protocol.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/proxy_protocol.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/proxy_protocol.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/proxy_protocol.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/resolver.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/resolver.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/resolver.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/resolver.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_cmsg_headers.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_cmsg_headers.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_cmsg_headers.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_cmsg_headers.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_option.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_option.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_option.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/socket_option.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/substitution_format_string.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/substitution_format_string.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/substitution_format_string.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/substitution_format_string.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/udp_socket_config.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/udp_socket_config.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/udp_socket_config.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/core/v3/udp_socket_config.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint_components.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint_components.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint_components.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/endpoint_components.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/load_report.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/load_report.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/load_report.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/endpoint/v3/load_report.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/api_listener.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/api_listener.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/api_listener.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/api_listener.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener_components.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener_components.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener_components.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/listener_components.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/quic_config.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/quic_config.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/quic_config.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/quic_config.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/udp_listener_config.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/udp_listener_config.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/udp_listener_config.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/listener/v3/udp_listener_config.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/rbac/v3/rbac.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/rbac/v3/rbac.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/rbac/v3/rbac.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/rbac/v3/rbac.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route_components.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route_components.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route_components.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/route_components.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/scoped_route.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/scoped_route.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/scoped_route.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/route/v3/scoped_route.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/datadog.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/datadog.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/datadog.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/datadog.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/dynamic_ot.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/dynamic_ot.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/dynamic_ot.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/dynamic_ot.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/http_tracer.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/http_tracer.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/http_tracer.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/http_tracer.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/lightstep.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/lightstep.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/lightstep.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/lightstep.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/opentelemetry.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/opentelemetry.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/opentelemetry.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/opentelemetry.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/service.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/service.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/service.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/service.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/skywalking.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/skywalking.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/skywalking.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/skywalking.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/trace.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/trace.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/trace.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/trace.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/xray.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/xray.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/xray.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/xray.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/zipkin.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/zipkin.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/zipkin.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/config/trace/v3/zipkin.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/data/accesslog/v3/accesslog.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/data/accesslog/v3/accesslog.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/data/accesslog/v3/accesslog.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/data/accesslog/v3/accesslog.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/clusters/aggregate/v3/cluster.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/clusters/aggregate/v3/cluster.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/clusters/aggregate/v3/cluster.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/clusters/aggregate/v3/cluster.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/rbac/v3/rbac.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/rbac/v3/rbac.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/rbac/v3/rbac.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/rbac/v3/rbac.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/router/v3/router.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/router/v3/router.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/router/v3/router.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/http/router/v3/router.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/cert.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/cert.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/cert.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/cert.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/common.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/common.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/common.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/common.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/secret.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/secret.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/secret.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/secret.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/cookie.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/cookie.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/cookie.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/cookie.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/path_transformation.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/path_transformation.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/path_transformation.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/http/v3/path_transformation.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/address.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/address.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/address.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/address.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/filter_state.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/filter_state.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/filter_state.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/filter_state.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/http_inputs.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/http_inputs.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/http_inputs.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/http_inputs.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/metadata.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/metadata.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/metadata.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/metadata.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/node.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/node.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/node.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/node.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/number.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/number.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/number.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/number.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/path.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/path.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/path.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/path.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/regex.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/regex.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/regex.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/regex.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/status_code_input.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/status_code_input.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/status_code_input.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/status_code_input.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/string.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/string.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/string.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/string.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/struct.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/struct.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/struct.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/struct.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/value.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/value.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/value.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/matcher/v3/value.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/metadata/v3/metadata.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/metadata/v3/metadata.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/metadata/v3/metadata.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/metadata/v3/metadata.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/tracing/v3/custom_tag.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/tracing/v3/custom_tag.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/tracing/v3/custom_tag.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/tracing/v3/custom_tag.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/hash_policy.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/hash_policy.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/hash_policy.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/hash_policy.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http_status.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http_status.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http_status.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/http_status.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/percent.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/percent.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/percent.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/percent.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/range.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/range.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/range.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/range.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_strategy.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_strategy.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_strategy.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_strategy.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_unit.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_unit.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_unit.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/ratelimit_unit.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/semantic_version.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/semantic_version.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/semantic_version.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/semantic_version.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/token_bucket.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/token_bucket.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/token_bucket.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/envoy/type/v3/token_bucket.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/annotations.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/checked.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/checked.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/checked.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/checked.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/syntax.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/syntax.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/syntax.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/expr/v1alpha1/syntax.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/http.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/http.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/http.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/http.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/httpbody.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/httpbody.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/api/httpbody.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/api/httpbody.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/google/rpc/status.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/validate/validate.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/validate/validate.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/validate/validate.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/validate/validate.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/migrate.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/migrate.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/migrate.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/migrate.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/security.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/security.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/security.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/security.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/sensitive.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/sensitive.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/sensitive.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/sensitive.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/status.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/status.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/status.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/status.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/versioning.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/versioning.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/versioning.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/udpa/annotations/versioning.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/migrate.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/migrate.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/migrate.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/migrate.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/security.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/security.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/security.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/security.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/sensitive.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/sensitive.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/sensitive.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/sensitive.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/status.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/status.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/status.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/status.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/versioning.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/versioning.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/versioning.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/annotations/v3/versioning.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/authority.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/authority.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/authority.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/authority.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/cidr.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/cidr.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/cidr.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/cidr.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/collection_entry.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/collection_entry.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/collection_entry.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/collection_entry.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/context_params.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/context_params.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/context_params.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/context_params.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/extension.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/extension.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/extension.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/extension.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_locator.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_locator.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_locator.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_locator.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_name.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_name.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_name.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/core/v3/resource_name.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/data/orca/v3/orca_load_report.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/cel.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/cel.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/cel.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/cel.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/domain.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/domain.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/domain.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/domain.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/http_inputs.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/http_inputs.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/http_inputs.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/http_inputs.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/ip.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/ip.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/ip.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/ip.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/matcher.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/matcher.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/matcher.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/matcher.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/range.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/range.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/range.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/range.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/regex.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/regex.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/regex.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/regex.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/string.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/string.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/string.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/matcher/v3/string.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/cel.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/cel.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/cel.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/cel.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/range.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/range.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/range.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/range.grpc.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/typed_struct.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/typed_struct.grpc.pb.cc
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/typed_struct.pb.h
+    ${_gRPC_PROTO_GENS_DIR}/xds/type/v3/typed_struct.grpc.pb.h
+    test/core/test_util/audit_logging_utils.cc
+    test/cpp/end2end/test_service_impl.cc
+    test/cpp/end2end/xds/xds_end2end_test_lib.cc
+    test/cpp/end2end/xds/xds_security_spiffe_end2end_test.cc
+    test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
+    test/cpp/util/tls_test_utils.cc
+  )
+  if(WIN32 AND MSVC)
+    if(BUILD_SHARED_LIBS)
+      target_compile_definitions(xds_security_spiffe_end2end_test
+      PRIVATE
+        "GPR_DLL_IMPORTS"
+        "GRPC_DLL_IMPORTS"
+        "GRPCXX_DLL_IMPORTS"
+      )
+    endif()
+  endif()
+  target_compile_features(xds_security_spiffe_end2end_test PUBLIC cxx_std_17)
+  target_include_directories(xds_security_spiffe_end2end_test
+    PRIVATE
+      ${CMAKE_CURRENT_SOURCE_DIR}
+      ${CMAKE_CURRENT_SOURCE_DIR}/include
+      ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
+      ${_gRPC_RE2_INCLUDE_DIR}
+      ${_gRPC_SSL_INCLUDE_DIR}
+      ${_gRPC_UPB_GENERATED_DIR}
+      ${_gRPC_UPB_GRPC_GENERATED_DIR}
+      ${_gRPC_UPB_INCLUDE_DIR}
+      ${_gRPC_XXHASH_INCLUDE_DIR}
+      ${_gRPC_ZLIB_INCLUDE_DIR}
+      third_party/googletest/googletest/include
+      third_party/googletest/googletest
+      third_party/googletest/googlemock/include
+      third_party/googletest/googlemock
+      ${_gRPC_PROTO_GENS_DIR}
+  )
+
+  target_link_libraries(xds_security_spiffe_end2end_test
+    ${_gRPC_ALLTARGETS_LIBRARIES}
+    gtest
+    grpc++_test_config
+    grpc++_test_util
+  )
+
+
+endif()
+endif()
+if(gRPC_BUILD_TESTS)
 
 add_executable(xds_stats_watcher_test
   ${_gRPC_PROTO_GENS_DIR}/src/proto/grpc/testing/empty.pb.cc
diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml
index dd34653..9d42cfe 100644
--- a/build_autogenerated.yaml
+++ b/build_autogenerated.yaml
@@ -18949,6 +18949,42 @@
   deps:
   - gtest
   uses_polling: false
+- name: spiffe_bundle_map_end2end_test
+  gtest: true
+  build: test
+  language: c++
+  headers:
+  - test/cpp/end2end/test_service_impl.h
+  src:
+  - src/proto/grpc/testing/echo.proto
+  - src/proto/grpc/testing/echo_messages.proto
+  - src/proto/grpc/testing/simple_messages.proto
+  - third_party/googleapis/google/api/annotations.proto
+  - third_party/googleapis/google/api/http.proto
+  - third_party/googleapis/google/rpc/status.proto
+  - third_party/protoc-gen-validate/validate/validate.proto
+  - third_party/xds/xds/data/orca/v3/orca_load_report.proto
+  - test/cpp/end2end/spiffe_bundle_map_end2end_test.cc
+  - test/cpp/end2end/test_service_impl.cc
+  deps:
+  - gtest
+  - grpc++_test_util
+- name: spiffe_ssl_transport_security_test
+  gtest: true
+  build: test
+  language: c++
+  headers:
+  - test/core/tsi/transport_security_test_lib.h
+  src:
+  - test/core/tsi/spiffe_ssl_transport_security_test.cc
+  - test/core/tsi/transport_security_test_lib.cc
+  deps:
+  - gtest
+  - grpc_test_util
+  platforms:
+  - linux
+  - posix
+  - mac
 - name: spiffe_utils_test
   gtest: true
   build: test
@@ -24100,6 +24136,162 @@
   - linux
   - posix
   - mac
+- name: xds_security_spiffe_end2end_test
+  gtest: true
+  build: test
+  language: c++
+  headers:
+  - test/core/test_util/audit_logging_utils.h
+  - test/core/test_util/scoped_env_var.h
+  - test/cpp/end2end/counted_service.h
+  - test/cpp/end2end/test_service_impl.h
+  - test/cpp/end2end/xds/xds_end2end_test_lib.h
+  - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
+  - test/cpp/util/tls_test_utils.h
+  - third_party/protoc-gen-validate/validate/validate.h
+  src:
+  - src/proto/grpc/testing/duplicate/echo_duplicate.proto
+  - src/proto/grpc/testing/echo.proto
+  - src/proto/grpc/testing/echo_messages.proto
+  - src/proto/grpc/testing/simple_messages.proto
+  - src/proto/grpc/testing/xds/v3/ads.proto
+  - src/proto/grpc/testing/xds/v3/discovery.proto
+  - src/proto/grpc/testing/xds/v3/lrs.proto
+  - third_party/envoy-api/envoy/annotations/deprecation.proto
+  - third_party/envoy-api/envoy/annotations/resource.proto
+  - third_party/envoy-api/envoy/config/accesslog/v3/accesslog.proto
+  - third_party/envoy-api/envoy/config/cluster/v3/circuit_breaker.proto
+  - third_party/envoy-api/envoy/config/cluster/v3/cluster.proto
+  - third_party/envoy-api/envoy/config/cluster/v3/filter.proto
+  - third_party/envoy-api/envoy/config/cluster/v3/outlier_detection.proto
+  - third_party/envoy-api/envoy/config/core/v3/address.proto
+  - third_party/envoy-api/envoy/config/core/v3/backoff.proto
+  - third_party/envoy-api/envoy/config/core/v3/base.proto
+  - third_party/envoy-api/envoy/config/core/v3/config_source.proto
+  - third_party/envoy-api/envoy/config/core/v3/event_service_config.proto
+  - third_party/envoy-api/envoy/config/core/v3/extension.proto
+  - third_party/envoy-api/envoy/config/core/v3/grpc_method_list.proto
+  - third_party/envoy-api/envoy/config/core/v3/grpc_service.proto
+  - third_party/envoy-api/envoy/config/core/v3/health_check.proto
+  - third_party/envoy-api/envoy/config/core/v3/http_service.proto
+  - third_party/envoy-api/envoy/config/core/v3/http_uri.proto
+  - third_party/envoy-api/envoy/config/core/v3/protocol.proto
+  - third_party/envoy-api/envoy/config/core/v3/proxy_protocol.proto
+  - third_party/envoy-api/envoy/config/core/v3/resolver.proto
+  - third_party/envoy-api/envoy/config/core/v3/socket_cmsg_headers.proto
+  - third_party/envoy-api/envoy/config/core/v3/socket_option.proto
+  - third_party/envoy-api/envoy/config/core/v3/substitution_format_string.proto
+  - third_party/envoy-api/envoy/config/core/v3/udp_socket_config.proto
+  - third_party/envoy-api/envoy/config/endpoint/v3/endpoint.proto
+  - third_party/envoy-api/envoy/config/endpoint/v3/endpoint_components.proto
+  - third_party/envoy-api/envoy/config/endpoint/v3/load_report.proto
+  - third_party/envoy-api/envoy/config/listener/v3/api_listener.proto
+  - third_party/envoy-api/envoy/config/listener/v3/listener.proto
+  - third_party/envoy-api/envoy/config/listener/v3/listener_components.proto
+  - third_party/envoy-api/envoy/config/listener/v3/quic_config.proto
+  - third_party/envoy-api/envoy/config/listener/v3/udp_listener_config.proto
+  - third_party/envoy-api/envoy/config/rbac/v3/rbac.proto
+  - third_party/envoy-api/envoy/config/route/v3/route.proto
+  - third_party/envoy-api/envoy/config/route/v3/route_components.proto
+  - third_party/envoy-api/envoy/config/route/v3/scoped_route.proto
+  - third_party/envoy-api/envoy/config/trace/v3/datadog.proto
+  - third_party/envoy-api/envoy/config/trace/v3/dynamic_ot.proto
+  - third_party/envoy-api/envoy/config/trace/v3/http_tracer.proto
+  - third_party/envoy-api/envoy/config/trace/v3/lightstep.proto
+  - third_party/envoy-api/envoy/config/trace/v3/opentelemetry.proto
+  - third_party/envoy-api/envoy/config/trace/v3/service.proto
+  - third_party/envoy-api/envoy/config/trace/v3/skywalking.proto
+  - third_party/envoy-api/envoy/config/trace/v3/trace.proto
+  - third_party/envoy-api/envoy/config/trace/v3/xray.proto
+  - third_party/envoy-api/envoy/config/trace/v3/zipkin.proto
+  - third_party/envoy-api/envoy/data/accesslog/v3/accesslog.proto
+  - third_party/envoy-api/envoy/extensions/clusters/aggregate/v3/cluster.proto
+  - third_party/envoy-api/envoy/extensions/filters/http/rbac/v3/rbac.proto
+  - third_party/envoy-api/envoy/extensions/filters/http/router/v3/router.proto
+  - third_party/envoy-api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
+  - third_party/envoy-api/envoy/extensions/transport_sockets/tls/v3/cert.proto
+  - third_party/envoy-api/envoy/extensions/transport_sockets/tls/v3/common.proto
+  - third_party/envoy-api/envoy/extensions/transport_sockets/tls/v3/secret.proto
+  - third_party/envoy-api/envoy/extensions/transport_sockets/tls/v3/tls.proto
+  - third_party/envoy-api/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto
+  - third_party/envoy-api/envoy/type/http/v3/cookie.proto
+  - third_party/envoy-api/envoy/type/http/v3/path_transformation.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/address.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/filter_state.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/http_inputs.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/metadata.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/node.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/number.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/path.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/regex.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/status_code_input.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/string.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/struct.proto
+  - third_party/envoy-api/envoy/type/matcher/v3/value.proto
+  - third_party/envoy-api/envoy/type/metadata/v3/metadata.proto
+  - third_party/envoy-api/envoy/type/tracing/v3/custom_tag.proto
+  - third_party/envoy-api/envoy/type/v3/hash_policy.proto
+  - third_party/envoy-api/envoy/type/v3/http.proto
+  - third_party/envoy-api/envoy/type/v3/http_status.proto
+  - third_party/envoy-api/envoy/type/v3/percent.proto
+  - third_party/envoy-api/envoy/type/v3/range.proto
+  - third_party/envoy-api/envoy/type/v3/ratelimit_strategy.proto
+  - third_party/envoy-api/envoy/type/v3/ratelimit_unit.proto
+  - third_party/envoy-api/envoy/type/v3/semantic_version.proto
+  - third_party/envoy-api/envoy/type/v3/token_bucket.proto
+  - third_party/googleapis/google/api/annotations.proto
+  - third_party/googleapis/google/api/expr/v1alpha1/checked.proto
+  - third_party/googleapis/google/api/expr/v1alpha1/syntax.proto
+  - third_party/googleapis/google/api/http.proto
+  - third_party/googleapis/google/api/httpbody.proto
+  - third_party/googleapis/google/rpc/status.proto
+  - third_party/protoc-gen-validate/validate/validate.proto
+  - third_party/xds/udpa/annotations/migrate.proto
+  - third_party/xds/udpa/annotations/security.proto
+  - third_party/xds/udpa/annotations/sensitive.proto
+  - third_party/xds/udpa/annotations/status.proto
+  - third_party/xds/udpa/annotations/versioning.proto
+  - third_party/xds/xds/annotations/v3/migrate.proto
+  - third_party/xds/xds/annotations/v3/security.proto
+  - third_party/xds/xds/annotations/v3/sensitive.proto
+  - third_party/xds/xds/annotations/v3/status.proto
+  - third_party/xds/xds/annotations/v3/versioning.proto
+  - third_party/xds/xds/core/v3/authority.proto
+  - third_party/xds/xds/core/v3/cidr.proto
+  - third_party/xds/xds/core/v3/collection_entry.proto
+  - third_party/xds/xds/core/v3/context_params.proto
+  - third_party/xds/xds/core/v3/extension.proto
+  - third_party/xds/xds/core/v3/resource.proto
+  - third_party/xds/xds/core/v3/resource_locator.proto
+  - third_party/xds/xds/core/v3/resource_name.proto
+  - third_party/xds/xds/data/orca/v3/orca_load_report.proto
+  - third_party/xds/xds/type/matcher/v3/cel.proto
+  - third_party/xds/xds/type/matcher/v3/domain.proto
+  - third_party/xds/xds/type/matcher/v3/http_inputs.proto
+  - third_party/xds/xds/type/matcher/v3/ip.proto
+  - third_party/xds/xds/type/matcher/v3/matcher.proto
+  - third_party/xds/xds/type/matcher/v3/range.proto
+  - third_party/xds/xds/type/matcher/v3/regex.proto
+  - third_party/xds/xds/type/matcher/v3/string.proto
+  - third_party/xds/xds/type/v3/cel.proto
+  - third_party/xds/xds/type/v3/range.proto
+  - third_party/xds/xds/type/v3/typed_struct.proto
+  - test/core/test_util/audit_logging_utils.cc
+  - test/cpp/end2end/test_service_impl.cc
+  - test/cpp/end2end/xds/xds_end2end_test_lib.cc
+  - test/cpp/end2end/xds/xds_security_spiffe_end2end_test.cc
+  - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
+  - test/cpp/util/tls_test_utils.cc
+  deps:
+  - gtest
+  - grpc++_test_config
+  - grpc++_test_util
+  platforms:
+  - linux
+  - posix
+  - mac
 - name: xds_stats_watcher_test
   gtest: true
   build: test
diff --git a/include/grpc/credentials.h b/include/grpc/credentials.h
index f4a614d..eb6c06b 100644
--- a/include/grpc/credentials.h
+++ b/include/grpc/credentials.h
@@ -656,6 +656,11 @@
  *   be null if no identity credentials are needed.
  * - root_cert_path is the file path to the root certificate bundle. This
  *   may be null if no root certs are needed.
+ * - spiffe_bundle_map_path is the file path to the SPIFFE Bundle Map. If
+ * configured, this will be used to find the roots of trust for a given SPIFFE
+ * domain during verification. See
+ *   https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md for
+ * more details on SPIFFE verification.
  * - refresh_interval_sec is the refreshing interval that we will check the
  *   files for updates.
  * It does not take ownership of parameters.
@@ -663,7 +668,8 @@
 GRPCAPI grpc_tls_certificate_provider*
 grpc_tls_certificate_provider_file_watcher_create(
     const char* private_key_path, const char* identity_certificate_path,
-    const char* root_cert_path, unsigned int refresh_interval_sec);
+    const char* root_cert_path, const char* spiffe_bundle_map_path,
+    unsigned int refresh_interval_sec);
 
 /**
  * EXPERIMENTAL API - Subject to change
diff --git a/include/grpcpp/security/tls_certificate_provider.h b/include/grpcpp/security/tls_certificate_provider.h
index 7912e4c..d85785c 100644
--- a/include/grpcpp/security/tls_certificate_provider.h
+++ b/include/grpcpp/security/tls_certificate_provider.h
@@ -109,19 +109,27 @@
   FileWatcherCertificateProvider(const std::string& private_key_path,
                                  const std::string& identity_certificate_path,
                                  const std::string& root_cert_path,
+                                 const std::string& spiffe_bundle_map_path,
                                  unsigned int refresh_interval_sec);
   // Constructor to get credential updates from identity file paths only.
   FileWatcherCertificateProvider(const std::string& private_key_path,
                                  const std::string& identity_certificate_path,
                                  unsigned int refresh_interval_sec)
       : FileWatcherCertificateProvider(private_key_path,
-                                       identity_certificate_path, "",
+                                       identity_certificate_path, "", "",
                                        refresh_interval_sec) {}
   // Constructor to get credential updates from root file path only.
   FileWatcherCertificateProvider(const std::string& root_cert_path,
                                  unsigned int refresh_interval_sec)
-      : FileWatcherCertificateProvider("", "", root_cert_path,
+      : FileWatcherCertificateProvider("", "", root_cert_path, "",
                                        refresh_interval_sec) {}
+  FileWatcherCertificateProvider(const std::string& private_key_path,
+                                 const std::string& identity_certificate_path,
+                                 const std::string& root_cert_path,
+                                 unsigned int refresh_interval_sec)
+      : FileWatcherCertificateProvider(
+            private_key_path, identity_certificate_path, root_cert_path, "",
+            refresh_interval_sec) {}
 
   ~FileWatcherCertificateProvider() override;
 
diff --git a/src/core/credentials/transport/tls/grpc_tls_certificate_provider.cc b/src/core/credentials/transport/tls/grpc_tls_certificate_provider.cc
index 940c6a0..2855c1e 100644
--- a/src/core/credentials/transport/tls/grpc_tls_certificate_provider.cc
+++ b/src/core/credentials/transport/tls/grpc_tls_certificate_provider.cc
@@ -509,17 +509,18 @@
       std::move(root_cert_core), std::move(identity_pairs_core));
 }
 
-// TODO(gtcooke94): Add a parameter to set the spiffe_bundle_map_path
 grpc_tls_certificate_provider*
 grpc_tls_certificate_provider_file_watcher_create(
     const char* private_key_path, const char* identity_certificate_path,
-    const char* root_cert_path, unsigned int refresh_interval_sec) {
+    const char* root_cert_path, const char* spiffe_bundle_map_path,
+    unsigned int refresh_interval_sec) {
   grpc_core::ExecCtx exec_ctx;
   return new grpc_core::FileWatcherCertificateProvider(
       private_key_path == nullptr ? "" : private_key_path,
       identity_certificate_path == nullptr ? "" : identity_certificate_path,
       root_cert_path == nullptr ? "" : root_cert_path,
-      /*spiffe_bundle_map_path=*/"", refresh_interval_sec);
+      spiffe_bundle_map_path == nullptr ? "" : spiffe_bundle_map_path,
+      refresh_interval_sec);
 }
 
 void grpc_tls_certificate_provider_release(
diff --git a/src/core/credentials/transport/tls/spiffe_utils.cc b/src/core/credentials/transport/tls/spiffe_utils.cc
index b12d7dc..4d9b16d 100644
--- a/src/core/credentials/transport/tls/spiffe_utils.cc
+++ b/src/core/credentials/transport/tls/spiffe_utils.cc
@@ -130,6 +130,11 @@
 
 }  // namespace
 
+std::string AddPemBlockWrapping(absl::string_view spiffe_bundle_root) {
+  return absl::StrCat(kCertificatePrefix, spiffe_bundle_root,
+                      kCertificateSuffix);
+}
+
 absl::StatusOr<SpiffeId> SpiffeId::FromString(absl::string_view input) {
   GRPC_RETURN_IF_ERROR(DoInitialUriValidation(input));
   if (!absl::StartsWithIgnoreCase(input, kSpiffePrefix)) {
@@ -202,8 +207,7 @@
     }
     if (!x5c->empty()) {
       ValidationErrors::ScopedField field(errors, "[0]");
-      std::string pem_cert =
-          absl::StrCat(kCertificatePrefix, (*x5c)[0], kCertificateSuffix);
+      std::string pem_cert = AddPemBlockWrapping((*x5c)[0]);
       auto certs = ParsePemCertificateChain(pem_cert);
       if (!certs.ok()) {
         errors->AddError(certs.status().ToString());
@@ -234,9 +238,53 @@
   for (size_t i = 0; i < keys->size(); ++i) {
     roots_.emplace_back((*keys)[i].GetRoot());
   }
+  ValidationErrors::ScopedField field(errors, "keys");
+  absl::Status status = CreateX509Stack();
+  if (!status.ok()) {
+    errors->AddError(status.ToString());
+  }
 }
 
-absl::Span<const std::string> SpiffeBundle::GetRoots() { return roots_; }
+SpiffeBundle::~SpiffeBundle() {
+  if (root_stack_ != nullptr) {
+    sk_X509_pop_free(*root_stack_, X509_free);
+  }
+}
+
+SpiffeBundle::SpiffeBundle(const SpiffeBundle& other) {
+  roots_ = other.roots_;
+  if (other.root_stack_ != nullptr) {
+    root_stack_ =
+        std::make_unique<STACK_OF(X509)*>(sk_X509_dup(*other.root_stack_));
+    for (size_t i = 0; i < sk_X509_num(*root_stack_); i++) {
+      X509* x = sk_X509_value(*root_stack_, i);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+      CHECK(X509_up_ref(x));
+#else
+      CRYPTO_add(&x->references, 1, CRYPTO_LOCK_X509);
+#endif
+    }
+  }
+}
+
+SpiffeBundle& SpiffeBundle::operator=(const SpiffeBundle& other) {
+  if (this != &other) {
+    roots_ = other.roots_;
+    if (other.root_stack_ != nullptr) {
+      root_stack_ =
+          std::make_unique<STACK_OF(X509)*>(sk_X509_dup(*other.root_stack_));
+      for (size_t i = 0; i < sk_X509_num(*root_stack_); i++) {
+        X509* x = sk_X509_value(*root_stack_, i);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+        CHECK(X509_up_ref(x));
+#else
+        CRYPTO_add(&x->references, 1, CRYPTO_LOCK_X509);
+#endif
+      }
+    }
+  }
+  return *this;
+}
 
 const JsonLoaderInterface* SpiffeBundleMap::JsonLoader(const JsonArgs&) {
   static const auto* kLoader =
@@ -246,6 +294,38 @@
   return kLoader;
 }
 
+absl::Span<const std::string> SpiffeBundle::GetRoots() { return roots_; }
+
+absl::StatusOr<STACK_OF(X509) *> SpiffeBundle::GetRootStack() {
+  if (root_stack_ == nullptr) {
+    return absl::FailedPreconditionError(
+        "root_stack_ has not been initialized");
+  }
+  return *root_stack_;
+}
+
+absl::Status SpiffeBundle::CreateX509Stack() {
+  root_stack_ = std::make_unique<STACK_OF(X509)*>(sk_X509_new_null());
+  absl::Status status = absl::OkStatus();
+  for (const auto& pem_cert : roots_) {
+    auto cert = ParsePemCertificateChain(AddPemBlockWrapping(pem_cert));
+    if (!cert.status().ok()) {
+      status = cert.status();
+      break;
+    }
+    if (cert->size() != 1) {
+      status = absl::InvalidArgumentError("Got a malformed root certificate.");
+      break;
+    }
+    sk_X509_push(*root_stack_, (*cert)[0]);
+  }
+  // If there was an error parsing we don't want a partially filled root stack.
+  if (!status.ok()) {
+    sk_X509_pop_free(*root_stack_, X509_free);
+  }
+  return status;
+}
+
 void SpiffeBundleMap::JsonPostLoad(const Json&, const JsonArgs&,
                                    ValidationErrors* errors) {
   {
@@ -279,4 +359,13 @@
       "No spiffe bundle found for trust domain %s", trust_domain));
 }
 
-}  // namespace grpc_core
\ No newline at end of file
+absl::StatusOr<STACK_OF(X509) *> SpiffeBundleMap::GetRootStack(
+    absl::string_view trust_domain) {
+  if (auto it = bundles_.find(trust_domain); it != bundles_.end()) {
+    return it->second.GetRootStack();
+  }
+  return absl::NotFoundError(absl::StrFormat(
+      "No spiffe bundle found for trust domain %s", trust_domain));
+}
+
+}  // namespace grpc_core
diff --git a/src/core/credentials/transport/tls/spiffe_utils.h b/src/core/credentials/transport/tls/spiffe_utils.h
index 742fe4f..1c4f011 100644
--- a/src/core/credentials/transport/tls/spiffe_utils.h
+++ b/src/core/credentials/transport/tls/spiffe_utils.h
@@ -19,6 +19,9 @@
 #ifndef GRPC_SRC_CORE_CREDENTIALS_TRANSPORT_TLS_SPIFFE_UTILS_H
 #define GRPC_SRC_CORE_CREDENTIALS_TRANSPORT_TLS_SPIFFE_UTILS_H
 
+#include <openssl/stack.h>
+#include <openssl/x509.h>
+
 #include <string>
 
 #include "absl/status/statusor.h"
@@ -28,6 +31,10 @@
 
 namespace grpc_core {
 
+// Adds the leading and trailing lines expected for a PEM formatted certificate
+// around the raw base64 certificate data stored in a SPIFFE bundle map.
+std::string AddPemBlockWrapping(absl::string_view spiffe_bundle_root);
+
 // A representation of a SPIFFE ID per the spec:
 // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#the-spiffe-identity-and-verifiable-identity-document
 class SpiffeId final {
@@ -72,8 +79,17 @@
 // domain.
 // https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#3-spiffe-bundles
 // https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md
+// Not thread-safe
 class SpiffeBundle final {
  public:
+  // Do not use - only exists to work with the JSON library.
+  // SpiffeBundles should be used by loading a SpiffeBundleMap via
+  // SpiffeBundleMap::FromFile
+  SpiffeBundle() = default;
+  ~SpiffeBundle();
+  SpiffeBundle(const SpiffeBundle& other);
+  SpiffeBundle& operator=(const SpiffeBundle& other);
+
   static const JsonLoaderInterface* JsonLoader(const JsonArgs&);
   void JsonPostLoad(const Json& json, const JsonArgs&,
                     ValidationErrors* errors);
@@ -81,7 +97,12 @@
   // Returns a vector of the roots in this SPIFFE Bundle.
   absl::Span<const std::string> GetRoots();
 
+  // The caller does not take ownership of the stack of roots.
+  // The caller MUST NOT mutate this value.
+  absl::StatusOr<STACK_OF(X509) *> GetRootStack();
+
   bool operator==(const SpiffeBundle& other) const {
+    // For our purposes SPIFFE Bundles are equal if their roots are the same.
     return roots_ == other.roots_;
   }
 
@@ -90,7 +111,11 @@
   }
 
  private:
+  // Constructs the `root_stack_` OpenSSL representation of the roots.
+  absl::Status CreateX509Stack();
+
   std::vector<std::string> roots_;
+  std::unique_ptr<STACK_OF(X509)*> root_stack_;
 };
 
 // A map of SPIFFE bundles keyed to trust domains. This functions as a map of a
@@ -99,6 +124,7 @@
 // https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md
 // https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md
 // Only configuring X509 roots is supported.
+// Not thread-safe
 class SpiffeBundleMap final {
  public:
   static const JsonLoaderInterface* JsonLoader(const JsonArgs&);
@@ -116,6 +142,9 @@
   absl::StatusOr<absl::Span<const std::string>> GetRoots(
       absl::string_view trust_domain);
 
+  // The caller does not take ownership of the stack of roots.
+  absl::StatusOr<STACK_OF(X509) *> GetRootStack(
+      const absl::string_view trust_domain);
   size_t size() const { return bundles_.size(); }
 
   bool operator==(const SpiffeBundleMap& other) const {
diff --git a/src/core/tsi/ssl_transport_security.cc b/src/core/tsi/ssl_transport_security.cc
index e763e51..8dd5b18 100644
--- a/src/core/tsi/ssl_transport_security.cc
+++ b/src/core/tsi/ssl_transport_security.cc
@@ -253,6 +253,7 @@
 static gpr_once g_init_openssl_once = GPR_ONCE_INIT;
 static int g_ssl_ctx_ex_factory_index = -1;
 static int g_ssl_ctx_ex_crl_provider_index = -1;
+static int g_ssl_ctx_ex_spiffe_bundle_map_index = -1;
 static const unsigned char kSslSessionIdContext[] = {'g', 'r', 'p', 'c'};
 static int g_ssl_ex_verified_root_cert_index = -1;
 #if !defined(OPENSSL_IS_BORINGSSL) && !defined(OPENSSL_NO_ENGINE)
@@ -343,6 +344,10 @@
       SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
   CHECK_NE(g_ssl_ctx_ex_crl_provider_index, -1);
 
+  g_ssl_ctx_ex_spiffe_bundle_map_index =
+      SSL_CTX_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
+  CHECK_NE(g_ssl_ctx_ex_spiffe_bundle_map_index, -1);
+
   g_ssl_ex_verified_root_cert_index = SSL_get_ex_new_index(
       0, nullptr, nullptr, nullptr, verified_root_cert_free);
   CHECK_NE(g_ssl_ex_verified_root_cert_index, -1);
@@ -1234,6 +1239,113 @@
   return 1;
 }
 
+static grpc_core::SpiffeBundleMap* GetSpiffeBundleMap(X509_STORE_CTX* ctx) {
+  CHECK(ctx != nullptr);
+  ERR_clear_error();
+  int ssl_index = SSL_get_ex_data_X509_STORE_CTX_idx();
+  if (ssl_index < 0) {
+    char err_str[256];
+    ERR_error_string_n(ERR_get_error(), err_str, sizeof(err_str));
+    GRPC_TRACE_LOG(tsi, INFO)
+        << "error getting the SSL index from the X509_STORE_CTX while getting "
+           "the SPIFFE Bundle Map: "
+        << err_str;
+    return nullptr;
+  }
+  SSL* ssl = static_cast<SSL*>(X509_STORE_CTX_get_ex_data(ctx, ssl_index));
+  if (ssl == nullptr) {
+    GRPC_TRACE_LOG(tsi, INFO)
+        << "error while fetching SPIFFE Bundle Map. SSL object is null";
+    return nullptr;
+  }
+  SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(ssl);
+  return static_cast<grpc_core::SpiffeBundleMap*>(
+      SSL_CTX_get_ex_data(ssl_ctx, g_ssl_ctx_ex_spiffe_bundle_map_index));
+}
+
+static absl::StatusOr<std::string> GetSpiffeUriFromCert(X509* cert) {
+  CHECK(cert != nullptr);
+  GENERAL_NAMES* subject_alt_names = static_cast<GENERAL_NAMES*>(
+      X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr));
+  int uri_count = 0;
+  absl::StatusOr<std::string> spiffe_uri = absl::InvalidArgumentError(
+      "spiffe: no SPIFFE ID found in leaf certificate.");
+  if (subject_alt_names != nullptr) {
+    size_t subject_alt_name_count = sk_GENERAL_NAME_num(subject_alt_names);
+    for (size_t i = 0; i < subject_alt_name_count; i++) {
+      GENERAL_NAME* subject_alt_name =
+          sk_GENERAL_NAME_value(subject_alt_names, TSI_SIZE_AS_SIZE(i));
+      if (subject_alt_name == nullptr) {
+        continue;
+      }
+      if (subject_alt_name->type == GEN_URI) {
+        uri_count++;
+        if (uri_count > 1) {
+          sk_GENERAL_NAME_pop_free(subject_alt_names, GENERAL_NAME_free);
+          return absl::InvalidArgumentError(
+              "spiffe: more than one SAN URI found while doing SPIFFE "
+              "validation. Must "
+              "have exactly one URI SAN that is the SPIFFE ID.");
+        }
+        spiffe_uri = grpc_core::ParseUriString(subject_alt_name);
+      }
+    }
+  }
+  sk_GENERAL_NAME_pop_free(subject_alt_names, GENERAL_NAME_free);
+  GRPC_RETURN_IF_ERROR(spiffe_uri.status());
+  if (spiffe_uri->empty()) {
+    return absl::InvalidArgumentError(
+        "spiffe: no URI SAN found in leaf certificate");
+  }
+  return spiffe_uri;
+}
+
+static absl::StatusOr<std::string> SpiffeTrustDomainFromCert(X509* cert) {
+  CHECK(cert != nullptr);
+  auto subject_name = GetSpiffeUriFromCert(cert);
+  GRPC_RETURN_IF_ERROR(subject_name.status());
+  auto spiffe_id = grpc_core::SpiffeId::FromString(*subject_name);
+  GRPC_RETURN_IF_ERROR(spiffe_id.status());
+  return std::string(spiffe_id->trust_domain());
+}
+
+// Fills ctx's trusted roots with the roots in the SPIFFE Bundle Map that
+// are associated with the to-be-verified leaf certificate's trust domain.
+// For more detail see
+// https://github.com/grpc/proposal/blob/master/A87-mtls-spiffe-support.md
+absl::Status ConfigureSpiffeRoots(
+    X509_STORE_CTX* ctx, grpc_core::SpiffeBundleMap* spiffe_bundle_map) {
+  CHECK(ctx != nullptr);
+  if (spiffe_bundle_map == nullptr) {
+    return absl::InvalidArgumentError(
+        "cannot configure spiffe roots with a nullptr spiffe_bundle_map.");
+  }
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+  X509* leaf_cert = X509_STORE_CTX_get0_cert(ctx);
+#else
+  X509* leaf_cert = ctx->cert;
+#endif
+  if (leaf_cert == nullptr) {
+    return absl::InvalidArgumentError(
+        "A SPIFFE bundle map was configured but the leaf cert is null");
+  }
+  absl::StatusOr<std::string> trust_domain =
+      SpiffeTrustDomainFromCert(leaf_cert);
+  GRPC_RETURN_IF_ERROR(trust_domain.status());
+  auto root_stack = spiffe_bundle_map->GetRootStack(*trust_domain);
+  GRPC_RETURN_IF_ERROR(root_stack.status());
+  if (*root_stack == nullptr) {
+    return absl::InvalidArgumentError(
+        "spiffe: root stack in the SPIFFE Bundle Map is nullptr.");
+  }
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+  X509_STORE_CTX_set0_trusted_stack(ctx, *root_stack);
+#else
+  X509_STORE_CTX_trusted_stack(ctx, *root_stack);
+#endif
+  return absl::OkStatus();
+}
+
 // The custom verification function to set in OpenSSL using
 // X509_set_cert_verify_callback. This calls the standard OpenSSL procedure
 // (X509_verify_cert), then also extracts the root certificate in the built
@@ -1241,12 +1353,24 @@
 // returns 1 on success, indicating a trusted chain to a root of trust was
 // found, 0 if a trusted chain could not be built.
 static int CustomVerificationFunction(X509_STORE_CTX* ctx, void* arg) {
+  CHECK(ctx != nullptr);
+  grpc_core::SpiffeBundleMap* spiffe_bundle_map = GetSpiffeBundleMap(ctx);
+  if (spiffe_bundle_map != nullptr) {
+    // If a SPIFFE Bundle Map is configured, we'll use
+    // X509_STORE_CTX_set0_trusted_stack to then configure these as the roots
+    // for verification.
+    absl::Status status = ConfigureSpiffeRoots(ctx, spiffe_bundle_map);
+    if (!status.ok()) {
+      VLOG(2) << "Failed to configure SPIFFE roots: " << status;
+      return -1;
+    }
+  }
   int ret = X509_verify_cert(ctx);
   if (ret <= 0) {
     VLOG(2) << "Failed to verify cert chain.";
     // Verification failed. We shouldn't expect to have a verified chain, so
-    // there is no need to attempt to extract the root cert from it, check for
-    // revocation, or check anything else.
+    // there is no need to attempt to extract the root cert from it, check
+    // for revocation, or check anything else.
     return ret;
   }
   grpc_core::experimental::CrlProvider* provider = GetCrlProvider(ctx);
@@ -1260,9 +1384,9 @@
   return RootCertExtractCallback(ctx, arg);
 }
 
-// Sets the min and max TLS version of |ssl_context| to |min_tls_version| and
-// |max_tls_version|, respectively. Calling this method is a no-op when using
-// OpenSSL versions < 1.1.
+// Sets the min and max TLS version of |ssl_context| to |min_tls_version|
+// and |max_tls_version|, respectively. Calling this method is a no-op when
+// using OpenSSL versions < 1.1.
 static tsi_result tsi_set_min_and_max_tls_versions(
     SSL_CTX* ssl_context, tsi_tls_version min_tls_version,
     tsi_tls_version max_tls_version) {
@@ -2429,12 +2553,14 @@
                 ssl_context, pem_root_certs.c_str(), pem_root_certs.size(),
                 nullptr);
           },
-          [&](const grpc_core::SpiffeBundleMap&) {
-            // TODO(gtcooke94) - implement SPIFFE Bundle Map verification. Crash
-            // until this is done.
-            grpc_core::Crash(
-                "spiffe bundle maps were configured but are not fully "
-                "implemented.");
+          [&](const grpc_core::SpiffeBundleMap& spiffe_bundle_map) {
+            X509_STORE* cert_store = SSL_CTX_get_cert_store(ssl_context);
+            X509_STORE_set_flags(cert_store, X509_V_FLAG_PARTIAL_CHAIN |
+                                                 X509_V_FLAG_TRUSTED_FIRST);
+            const void* p = &spiffe_bundle_map;
+            void* map = const_cast<void*>(p);
+            SSL_CTX_set_ex_data(ssl_context,
+                                g_ssl_ctx_ex_spiffe_bundle_map_index, map);
           });
       X509_STORE* cert_store = SSL_CTX_get_cert_store(ssl_context);
 #if OPENSSL_VERSION_NUMBER >= 0x10100000
@@ -2657,12 +2783,15 @@
                 SSL_CTX_set_client_CA_list(impl->ssl_contexts[i], root_names);
               }
             },
-            [&](const grpc_core::SpiffeBundleMap&) {
-              // TODO(gtcooke94) - implement SPIFFE Bundle Map verification.
-              // Crash until this is done.
-              grpc_core::Crash(
-                  "spiffe bundle maps were configured but are not fully "
-                  "implemented.");
+            [&](const grpc_core::SpiffeBundleMap& spiffe_bundle_map) {
+              X509_STORE* cert_store =
+                  SSL_CTX_get_cert_store(impl->ssl_contexts[i]);
+              X509_STORE_set_flags(cert_store, X509_V_FLAG_PARTIAL_CHAIN |
+                                                   X509_V_FLAG_TRUSTED_FIRST);
+              const void* p = &spiffe_bundle_map;
+              void* map = const_cast<void*>(p);
+              SSL_CTX_set_ex_data(impl->ssl_contexts[i],
+                                  g_ssl_ctx_ex_spiffe_bundle_map_index, map);
             });
         if (result != TSI_OK) {
           break;
diff --git a/src/core/tsi/ssl_transport_security_utils.cc b/src/core/tsi/ssl_transport_security_utils.cc
index 5c3b828..65301cf 100644
--- a/src/core/tsi/ssl_transport_security_utils.cc
+++ b/src/core/tsi/ssl_transport_security_utils.cc
@@ -428,4 +428,25 @@
   return pkey;
 }
 
+absl::StatusOr<std::string> ParseUriString(GENERAL_NAME* subject_alt_name) {
+  if (subject_alt_name == nullptr || subject_alt_name->type != GEN_URI) {
+    return absl::InvalidArgumentError("Could not parse ASN1 string to UTF8");
+  }
+  // This shouldn't be a possible if statement to enter because if the type is
+  // GEN_URI it then by definition should have a d.uniformResourceIdentifier.
+  // But we can still keep it for safety.
+  if (subject_alt_name->d.uniformResourceIdentifier == nullptr) {
+    return absl::InvalidArgumentError("Could not parse ASN1 string to UTF8");
+  }
+  unsigned char* name = nullptr;
+  int name_size =
+      ASN1_STRING_to_UTF8(&name, subject_alt_name->d.uniformResourceIdentifier);
+  if (name_size < 0 || name == nullptr) {
+    OPENSSL_free(name);
+    return absl::InvalidArgumentError("Could not parse ASN1 string to UTF8");
+  }
+  std::string ret(reinterpret_cast<char const*>(name), name_size);
+  OPENSSL_free(name);
+  return ret;
+}
 }  // namespace grpc_core
diff --git a/src/core/tsi/ssl_transport_security_utils.h b/src/core/tsi/ssl_transport_security_utils.h
index 038b40d..d71847a 100644
--- a/src/core/tsi/ssl_transport_security_utils.h
+++ b/src/core/tsi/ssl_transport_security_utils.h
@@ -23,6 +23,7 @@
 #include <grpc/support/port_platform.h>
 #include <openssl/evp.h>
 #include <openssl/x509.h>
+#include <openssl/x509v3.h>
 
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
@@ -177,6 +178,9 @@
 // Returns an EVP_PKEY instance parsed from the non-empty PEM private key block
 // in private_key_pem. Caller takes ownership of the EVP_PKEY pointer.
 absl::StatusOr<EVP_PKEY*> ParsePemPrivateKey(absl::string_view private_key_pem);
+
+// Safely parses a URI from OpenSSL's GENERAL_NAME to a string representation.
+absl::StatusOr<std::string> ParseUriString(GENERAL_NAME* subject_alt_name);
 }  // namespace grpc_core
 
 #endif  // GRPC_SRC_CORE_TSI_SSL_TRANSPORT_SECURITY_UTILS_H
diff --git a/src/cpp/common/tls_certificate_provider.cc b/src/cpp/common/tls_certificate_provider.cc
index da0d3b1..5c6123a 100644
--- a/src/cpp/common/tls_certificate_provider.cc
+++ b/src/cpp/common/tls_certificate_provider.cc
@@ -55,10 +55,13 @@
 FileWatcherCertificateProvider::FileWatcherCertificateProvider(
     const std::string& private_key_path,
     const std::string& identity_certificate_path,
-    const std::string& root_cert_path, unsigned int refresh_interval_sec) {
+    const std::string& root_cert_path,
+    const std::string& spiffe_bundle_map_path,
+    unsigned int refresh_interval_sec) {
   c_provider_ = grpc_tls_certificate_provider_file_watcher_create(
       private_key_path.c_str(), identity_certificate_path.c_str(),
-      root_cert_path.c_str(), refresh_interval_sec);
+      root_cert_path.c_str(), spiffe_bundle_map_path.c_str(),
+      refresh_interval_sec);
   CHECK_NE(c_provider_, nullptr);
 };
 
diff --git a/src/ruby/ext/grpc/rb_grpc_imports.generated.h b/src/ruby/ext/grpc/rb_grpc_imports.generated.h
index 6f84be3..0a4baa2 100644
--- a/src/ruby/ext/grpc/rb_grpc_imports.generated.h
+++ b/src/ruby/ext/grpc/rb_grpc_imports.generated.h
@@ -207,7 +207,7 @@
 typedef grpc_tls_certificate_provider*(*grpc_tls_certificate_provider_static_data_create_type)(const char* root_certificate, grpc_tls_identity_pairs* pem_key_cert_pairs);
 extern grpc_tls_certificate_provider_static_data_create_type grpc_tls_certificate_provider_static_data_create_import;
 #define grpc_tls_certificate_provider_static_data_create grpc_tls_certificate_provider_static_data_create_import
-typedef grpc_tls_certificate_provider*(*grpc_tls_certificate_provider_file_watcher_create_type)(const char* private_key_path, const char* identity_certificate_path, const char* root_cert_path, unsigned int refresh_interval_sec);
+typedef grpc_tls_certificate_provider*(*grpc_tls_certificate_provider_file_watcher_create_type)(const char* private_key_path, const char* identity_certificate_path, const char* root_cert_path, const char* spiffe_bundle_map_path, unsigned int refresh_interval_sec);
 extern grpc_tls_certificate_provider_file_watcher_create_type grpc_tls_certificate_provider_file_watcher_create_import;
 #define grpc_tls_certificate_provider_file_watcher_create grpc_tls_certificate_provider_file_watcher_create_import
 typedef void(*grpc_tls_certificate_provider_release_type)(grpc_tls_certificate_provider* provider);
diff --git a/test/core/credentials/transport/tls/spiffe_utils_test.cc b/test/core/credentials/transport/tls/spiffe_utils_test.cc
index 8c981b0..87bb9d6 100644
--- a/test/core/credentials/transport/tls/spiffe_utils_test.cc
+++ b/test/core/credentials/transport/tls/spiffe_utils_test.cc
@@ -68,6 +68,37 @@
   }
   return cert;
 }
+
+bool CompareX509(const X509* a, const X509* b) {
+  if (a == b) return true;  // both null or same ptr
+  if (a == nullptr || b == nullptr) {
+    return false;  // One is null, the other is not, not equal
+  }
+  return X509_cmp(a, b) == 0;  // Uses internal comparison
+}
+
+// Function to compare two STACK_OF(X509)
+bool X509StacksEqual(const STACK_OF(X509) * stack1,
+                     const STACK_OF(X509) * stack2) {
+  if (stack1 == stack2) return true;  // both null or same ptr
+  if (stack1 == nullptr || stack2 == nullptr) return false;
+
+  size_t num1 = sk_X509_num(stack1);
+  size_t num2 = sk_X509_num(stack2);
+
+  if (num1 != num2) {
+    return false;  // Different number of elements
+  }
+
+  for (size_t i = 0; i < num1; ++i) {
+    X509* cert1 = sk_X509_value(stack1, i);
+    X509* cert2 = sk_X509_value(stack2, i);
+    if (!CompareX509(cert1, cert2)) {
+      return false;  // Certificates at this index are not equal
+    }
+  }
+  return true;  // All certificates are equal
+}
 }  // namespace
 
 TEST(SpiffeId, EmptyFails) {
@@ -426,8 +457,13 @@
         "spiffe_cert.pem");
     ASSERT_TRUE(expected_certificate.ok()) << expected_certificate.status();
     EXPECT_EQ(X509_cmp(*certificate, *expected_certificate), 0);
+    STACK_OF(X509)* expected_stack = sk_X509_new_null();
+    sk_X509_push(expected_stack, *expected_certificate);
+    auto actual_root_stack = bundle_map->GetRootStack("example.com");
+    ASSERT_TRUE(actual_root_stack.ok());
+    EXPECT_TRUE(X509StacksEqual(*actual_root_stack, expected_stack));
+    sk_X509_pop_free(expected_stack, X509_free);
     X509_free(*certificate);
-    X509_free(*expected_certificate);
   }
   {
     // check the test.example.com bundle
@@ -441,8 +477,13 @@
         "server1_spiffe.pem");
     ASSERT_TRUE(expected_certificate.ok()) << expected_certificate.status();
     EXPECT_EQ(X509_cmp(*certificate, *expected_certificate), 0);
+    STACK_OF(X509)* expected_stack = sk_X509_new_null();
+    sk_X509_push(expected_stack, *expected_certificate);
+    auto actual_root_stack = bundle_map->GetRootStack("test.example.com");
+    ASSERT_TRUE(actual_root_stack.ok());
+    EXPECT_TRUE(X509StacksEqual(*actual_root_stack, expected_stack));
+    sk_X509_pop_free(expected_stack, X509_free);
     X509_free(*certificate);
-    X509_free(*expected_certificate);
   }
 }
 
@@ -459,30 +500,39 @@
   auto roots = bundle_map->GetRoots("example.com");
   ASSERT_TRUE(roots.ok());
   EXPECT_EQ(roots->size(), 2);
-  {
-    // Check the first root
-    auto certificate = ReadCertificate((*roots)[0]);
-    ASSERT_TRUE(certificate.ok()) << certificate.status();
-    auto expected_certificate = ReadCertificateFromFile(
-        "test/core/credentials/transport/tls/test_data/spiffe/"
-        "spiffe_cert.pem");
-    ASSERT_TRUE(expected_certificate.ok()) << expected_certificate.status();
-    EXPECT_EQ(X509_cmp(*certificate, *expected_certificate), 0);
-    X509_free(*certificate);
-    X509_free(*expected_certificate);
-  }
-  {
-    // Check the second root
-    auto certificate = ReadCertificate((*roots)[1]);
-    ASSERT_TRUE(certificate.ok()) << certificate.status();
-    auto expected_certificate = ReadCertificateFromFile(
-        "test/core/credentials/transport/tls/test_data/spiffe/test_bundles/"
-        "server1_spiffe.pem");
-    ASSERT_TRUE(expected_certificate.ok()) << expected_certificate.status();
-    EXPECT_EQ(X509_cmp(*certificate, *expected_certificate), 0);
-    X509_free(*certificate);
-    X509_free(*expected_certificate);
-  }
+  // Check the first root
+  auto certificate = ReadCertificate((*roots)[0]);
+  ASSERT_TRUE(certificate.ok()) << certificate.status();
+  auto expected_certificate = ReadCertificateFromFile(
+      "test/core/credentials/transport/tls/test_data/spiffe/"
+      "spiffe_cert.pem");
+  ASSERT_TRUE(expected_certificate.ok()) << expected_certificate.status();
+  EXPECT_EQ(X509_cmp(*certificate, *expected_certificate), 0);
+  // Check the second root
+  auto certificate2 = ReadCertificate((*roots)[1]);
+  ASSERT_TRUE(certificate.ok()) << certificate.status();
+  auto expected_certificate2 = ReadCertificateFromFile(
+      "test/core/credentials/transport/tls/test_data/spiffe/test_bundles/"
+      "server1_spiffe.pem");
+  ASSERT_TRUE(expected_certificate2.ok()) << expected_certificate2.status();
+  EXPECT_EQ(X509_cmp(*certificate2, *expected_certificate2), 0);
+
+  STACK_OF(X509)* expected_stack = sk_X509_new_null();
+  sk_X509_push(expected_stack, *expected_certificate);
+  sk_X509_push(expected_stack, *expected_certificate2);
+  auto actual_root_stack = bundle_map->GetRootStack("example.com");
+  ASSERT_TRUE(actual_root_stack.ok());
+  EXPECT_TRUE(X509StacksEqual(*actual_root_stack, expected_stack));
+  sk_X509_pop_free(expected_stack, X509_free);
+  X509_free(*certificate);
+  X509_free(*certificate2);
+}
+
+TEST(SpiffeBundle, BundleRootToPem) {
+  const absl::string_view base = "foo";
+  EXPECT_EQ(AddPemBlockWrapping(base),
+            absl::StrCat("-----BEGIN CERTIFICATE-----\n", base,
+                         "\n-----END CERTIFICATE-----"));
 }
 
 }  // namespace testing
diff --git a/test/core/end2end/fixtures/h2_tls_common.h b/test/core/end2end/fixtures/h2_tls_common.h
index cb95b77..71d43bf 100644
--- a/test/core/end2end/fixtures/h2_tls_common.h
+++ b/test/core/end2end/fixtures/h2_tls_common.h
@@ -104,9 +104,11 @@
       }
       case SecurityPrimitives::ProviderType::FILE_PROVIDER: {
         client_provider_ = grpc_tls_certificate_provider_file_watcher_create(
-            SERVER_KEY_PATH, SERVER_CERT_PATH, CA_CERT_PATH, 1);
+            SERVER_KEY_PATH, SERVER_CERT_PATH, CA_CERT_PATH,
+            /*spiffe_bundle_map_path=*/"", 1);
         server_provider_ = grpc_tls_certificate_provider_file_watcher_create(
-            SERVER_KEY_PATH, SERVER_CERT_PATH, CA_CERT_PATH, 1);
+            SERVER_KEY_PATH, SERVER_CERT_PATH, CA_CERT_PATH,
+            /*spiffe_bundle_map_path=*/"", 1);
         break;
       }
     }
diff --git a/test/core/tsi/BUILD b/test/core/tsi/BUILD
index 8537542..352bdb2 100644
--- a/test/core/tsi/BUILD
+++ b/test/core/tsi/BUILD
@@ -165,6 +165,46 @@
 )
 
 grpc_cc_test(
+    name = "spiffe_ssl_transport_security_test",
+    srcs = ["spiffe_ssl_transport_security_test.cc"],
+    data = [
+        "//test/core/tsi/test_creds/crl_data:ca.pem",
+        "//test/core/tsi/test_creds/crl_data:valid.key",
+        "//test/core/tsi/test_creds/crl_data:valid.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffebundle.json",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate.cnf",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_gen.sh",
+        "//test/core/tsi/test_creds/spiffe_end2end:invalid_utf8_san.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:invalid_utf8_san_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_and_intermediate_chain.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:multi_san.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:multi_san_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffebundle.json",
+    ],
+    external_deps = [
+        "absl/log:check",
+        "gtest",
+    ],
+    tags = ["no_windows"],
+    deps = [
+        ":transport_security_test_lib",
+        "//:gpr",
+        "//:grpc",
+        "//:tsi",
+        "//test/core/test_util:grpc_test_util",
+    ],
+)
+
+grpc_cc_test(
     name = "transport_security_test",
     srcs = ["transport_security_test.cc"],
     external_deps = [
diff --git a/test/core/tsi/spiffe_ssl_transport_security_test.cc b/test/core/tsi/spiffe_ssl_transport_security_test.cc
new file mode 100644
index 0000000..93e75bb
--- /dev/null
+++ b/test/core/tsi/spiffe_ssl_transport_security_test.cc
@@ -0,0 +1,445 @@
+// Copyright 2025 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <grpc/grpc.h>
+#include <grpc/support/alloc.h>
+#include <grpc/support/string_util.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <memory>
+#include <utility>
+
+#include "absl/log/check.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/core/credentials/transport/security_connector.h"
+#include "src/core/tsi/ssl_transport_security.h"
+#include "src/core/tsi/transport_security.h"
+#include "src/core/tsi/transport_security_interface.h"
+#include "src/core/util/crash.h"
+#include "test/core/test_util/test_config.h"
+#include "test/core/test_util/tls_utils.h"
+#include "test/core/tsi/transport_security_test_lib.h"
+
+extern "C" {
+#include <openssl/crypto.h>
+#include <openssl/pem.h>
+}
+
+namespace {
+constexpr absl::string_view kCaPemPath =
+    "test/core/tsi/test_creds/spiffe_end2end/ca.pem";
+constexpr absl::string_view kClientKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client.key";
+constexpr absl::string_view kClientCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem";
+constexpr absl::string_view kServerKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server.key";
+constexpr absl::string_view kServerCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem";
+constexpr absl::string_view kServerChainKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.key";
+constexpr absl::string_view kServerChainCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/leaf_and_intermediate_chain.pem";
+constexpr absl::string_view kClientSpiffeBundleMapPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json";
+constexpr absl::string_view kServerSpiffeBundleMapPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json";
+
+constexpr absl::string_view kNonSpiffeKeyPath =
+    "test/core/tsi/test_creds/crl_data/valid.key";
+constexpr absl::string_view kNonSpiffeCertPath =
+    "test/core/tsi/test_creds/crl_data/valid.pem";
+constexpr absl::string_view kMultiSanKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/multi_san.key";
+constexpr absl::string_view kMultiSanCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/multi_san_spiffe.pem";
+constexpr absl::string_view kInvalidUtf8SanKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san.key";
+constexpr absl::string_view kInvalidUtf8SanCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san_spiffe.pem";
+
+class SpiffeSslTransportSecurityTest
+    : public testing::TestWithParam<tsi_tls_version> {
+ protected:
+  // A tsi_test_fixture implementation.
+  class SslTsiTestFixture {
+   public:
+    SslTsiTestFixture(
+        absl::string_view server_key_path, absl::string_view server_cert_path,
+        absl::string_view client_key_path, absl::string_view client_cert_path,
+        absl::string_view server_spiffe_bundle_map_path,
+        absl::string_view client_spiffe_bundle_map_path,
+        std::optional<absl::string_view> ca_path, bool expect_server_success,
+        bool expect_client_success_1_2, bool expect_client_success_1_3) {
+      tsi_test_fixture_init(&base_);
+      base_.test_unused_bytes = true;
+      base_.vtable = &kVtable;
+      server_key_ = grpc_core::testing::GetFileContents(server_key_path.data());
+      server_cert_ =
+          grpc_core::testing::GetFileContents(server_cert_path.data());
+      client_key_ = grpc_core::testing::GetFileContents(client_key_path.data());
+      client_cert_ =
+          grpc_core::testing::GetFileContents(client_cert_path.data());
+      // We set this and it shouldn't matter if we set spiffe bundles
+      if (ca_path.has_value()) {
+        ca_certificates_ = grpc_core::testing::GetFileContents(ca_path->data());
+      }
+      if (!server_spiffe_bundle_map_path.empty()) {
+        auto server_map =
+            grpc_core::SpiffeBundleMap::FromFile(server_spiffe_bundle_map_path);
+        CHECK(server_map.ok());
+        server_spiffe_bundle_map_ = std::make_shared<RootCertInfo>(*server_map);
+      }
+      if (!client_spiffe_bundle_map_path.empty()) {
+        auto client_map =
+            grpc_core::SpiffeBundleMap::FromFile(client_spiffe_bundle_map_path);
+        CHECK(client_map.ok());
+        client_spiffe_bundle_map_ = std::make_shared<RootCertInfo>(*client_map);
+      }
+      expect_server_success_ = expect_server_success;
+      expect_client_success_1_2_ = expect_client_success_1_2;
+      expect_client_success_1_3_ = expect_client_success_1_3;
+
+      server_pem_key_cert_pairs_ = static_cast<tsi_ssl_pem_key_cert_pair*>(
+          gpr_malloc(sizeof(tsi_ssl_pem_key_cert_pair)));
+      server_pem_key_cert_pairs_[0].private_key = server_key_.c_str();
+      server_pem_key_cert_pairs_[0].cert_chain = server_cert_.c_str();
+      client_pem_key_cert_pairs_ = static_cast<tsi_ssl_pem_key_cert_pair*>(
+          gpr_malloc(sizeof(tsi_ssl_pem_key_cert_pair)));
+      client_pem_key_cert_pairs_[0].private_key = client_key_.c_str();
+      client_pem_key_cert_pairs_[0].cert_chain = client_cert_.c_str();
+    }
+
+    void Run() {
+      tsi_test_do_handshake(&base_);
+      tsi_test_fixture_destroy(&base_);
+    }
+
+    ~SslTsiTestFixture() {
+      gpr_free(server_pem_key_cert_pairs_);
+      gpr_free(client_pem_key_cert_pairs_);
+
+      tsi_ssl_server_handshaker_factory_unref(server_handshaker_factory_);
+      tsi_ssl_client_handshaker_factory_unref(client_handshaker_factory_);
+    }
+
+   private:
+    static void SetupHandshakers(tsi_test_fixture* fixture) {
+      CHECK_NE(fixture, nullptr);
+      auto* self = reinterpret_cast<SslTsiTestFixture*>(fixture);
+      self->SetupHandshakers();
+    }
+
+    void SetupHandshakers() {
+      // Create client handshaker factory.
+      tsi_ssl_client_handshaker_options client_options;
+      client_options.pem_key_cert_pair = client_pem_key_cert_pairs_;
+      if (client_spiffe_bundle_map_ != nullptr) {
+        client_options.root_cert_info = client_spiffe_bundle_map_;
+      } else {
+        client_options.root_cert_info =
+            std::make_shared<RootCertInfo>(ca_certificates_);
+      }
+      client_options.min_tls_version = GetParam();
+      client_options.max_tls_version = GetParam();
+      EXPECT_EQ(tsi_create_ssl_client_handshaker_factory_with_options(
+                    &client_options, &client_handshaker_factory_),
+                TSI_OK);
+      // Create server handshaker factory.
+      tsi_ssl_server_handshaker_options server_options;
+      server_options.pem_key_cert_pairs = server_pem_key_cert_pairs_;
+      server_options.num_key_cert_pairs = 1;
+      if (server_spiffe_bundle_map_ != nullptr) {
+        server_options.root_cert_info = server_spiffe_bundle_map_;
+      } else {
+        server_options.root_cert_info =
+            std::make_shared<RootCertInfo>(ca_certificates_);
+      }
+      server_options.client_certificate_request =
+          TSI_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY;
+      server_options.session_ticket_key = nullptr;
+      server_options.session_ticket_key_size = 0;
+      server_options.min_tls_version = GetParam();
+      server_options.max_tls_version = GetParam();
+      EXPECT_EQ(tsi_create_ssl_server_handshaker_factory_with_options(
+                    &server_options, &server_handshaker_factory_),
+                TSI_OK);
+      // Create server and client handshakers.
+      EXPECT_EQ(tsi_ssl_client_handshaker_factory_create_handshaker(
+                    client_handshaker_factory_, nullptr, 0, 0,
+                    /*alpn_preferred_protocol_list=*/std::nullopt,
+                    &base_.client_handshaker),
+                TSI_OK);
+      EXPECT_EQ(tsi_ssl_server_handshaker_factory_create_handshaker(
+                    server_handshaker_factory_, 0, 0, &base_.server_handshaker),
+                TSI_OK);
+    }
+
+    static void CheckHandshakerPeers(tsi_test_fixture* fixture) {
+      CHECK_NE(fixture, nullptr);
+      auto* self = reinterpret_cast<SslTsiTestFixture*>(fixture);
+      self->CheckHandshakerPeers();
+    }
+
+    void CheckHandshakerPeers() {
+      bool expect_server_success = expect_server_success_;
+      bool expect_client_success = false;
+#if OPENSSL_VERSION_NUMBER >= 0x10100000
+      expect_client_success = GetParam() == tsi_tls_version::TSI_TLS1_2
+                                  ? expect_client_success_1_2_
+                                  : expect_client_success_1_3_;
+#else
+      //  If using OpenSSL version < 1.1, the CRL revocation won't
+      //  be enabled anyways, so we always expect the connection to
+      //  be successful.
+      expect_server_success = true;
+      expect_client_success = expect_server_success;
+#endif
+      tsi_peer peer;
+      if (expect_client_success) {
+        EXPECT_EQ(
+            tsi_handshaker_result_extract_peer(base_.client_result, &peer),
+            TSI_OK);
+        tsi_peer_destruct(&peer);
+      } else {
+        EXPECT_EQ(base_.client_result, nullptr);
+      }
+      if (expect_server_success) {
+        EXPECT_EQ(
+            tsi_handshaker_result_extract_peer(base_.server_result, &peer),
+            TSI_OK);
+        tsi_peer_destruct(&peer);
+      } else {
+        EXPECT_EQ(base_.server_result, nullptr);
+      }
+    }
+
+    static void Destruct(tsi_test_fixture* fixture) {
+      auto* self = reinterpret_cast<SslTsiTestFixture*>(fixture);
+      delete self;
+    }
+
+    static struct tsi_test_fixture_vtable kVtable;
+
+    tsi_test_fixture base_;
+    std::string ca_certificates_;
+    tsi_ssl_server_handshaker_factory* server_handshaker_factory_;
+    tsi_ssl_client_handshaker_factory* client_handshaker_factory_;
+    std::shared_ptr<RootCertInfo> server_spiffe_bundle_map_;
+    std::shared_ptr<RootCertInfo> client_spiffe_bundle_map_;
+
+    std::string server_key_;
+    std::string server_cert_;
+    std::string client_key_;
+    std::string client_cert_;
+    bool expect_server_success_;
+    bool expect_client_success_1_2_;
+    bool expect_client_success_1_3_;
+    tsi_ssl_pem_key_cert_pair* client_pem_key_cert_pairs_;
+    tsi_ssl_pem_key_cert_pair* server_pem_key_cert_pairs_;
+  };
+};
+
+struct tsi_test_fixture_vtable
+    SpiffeSslTransportSecurityTest::SslTsiTestFixture::kVtable = {
+        &SpiffeSslTransportSecurityTest::SslTsiTestFixture::SetupHandshakers,
+        &SpiffeSslTransportSecurityTest::SslTsiTestFixture::
+            CheckHandshakerPeers,
+        &SpiffeSslTransportSecurityTest::SslTsiTestFixture::Destruct};
+
+// Valid SPIFFE Bundles on both sides with the root configured for the
+// appropriate trust domain
+TEST_P(SpiffeSslTransportSecurityTest, MTLSSpiffe) {
+  auto* fixture = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kClientKeyPath, kClientCertPath,
+      kServerSpiffeBundleMapPath, kClientSpiffeBundleMapPath, std::nullopt,
+      /*expect_server_success=*/true,
+      /*expect_client_success_1_2=*/true, /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// Valid SPIFFE Bundles on both sides with the root configured for the
+// appropriate trust domain, and a certificate chain with an intermediate CA on
+// the server side
+TEST_P(SpiffeSslTransportSecurityTest, MTLSSpiffeChain) {
+  auto* fixture = new SslTsiTestFixture(
+      kServerChainKeyPath, kServerChainCertPath, kClientKeyPath,
+      kClientCertPath, kServerSpiffeBundleMapPath, kClientSpiffeBundleMapPath,
+      std::nullopt,
+      /*expect_server_success=*/true,
+      /*expect_client_success_1_2=*/true,
+      /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// Valid SPIFFE bundle on the client side, but the server side has a flat list
+// of CA certificates.
+TEST_P(SpiffeSslTransportSecurityTest, ClientSideSpiffeBundle) {
+  auto* fixture = new SslTsiTestFixture(kServerKeyPath, kServerCertPath,
+                                        kClientKeyPath, kClientCertPath, "",
+                                        kClientSpiffeBundleMapPath, kCaPemPath,
+                                        /*expect_server_success=*/true,
+                                        /*expect_client_success_1_2=*/true,
+                                        /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// Valid SPIFFE bundle on the server side, but the client side has a flat list
+// of CA certificates.
+TEST_P(SpiffeSslTransportSecurityTest, ServerSideSpiffeBundle) {
+  auto* fixture = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kClientKeyPath, kClientCertPath,
+      kServerSpiffeBundleMapPath, "", kCaPemPath,
+      /*expect_server_success=*/true,
+      /*expect_client_success_1_2=*/true,
+      /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// Valid SPIFFE bundle on the client side, but the server side has a SPIFFE
+// bundle that does not have a trust domain that will match the client leaf
+// certificate. When negotiating TLS 1.3, the client-side handshake succeeds
+// because server verification of the client certificate occurs after the
+// client-side handshake is complete.
+TEST_P(SpiffeSslTransportSecurityTest, MTLSSpiffeServerMismatchFail) {
+  auto* fixture = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kClientKeyPath, kClientCertPath,
+      kClientSpiffeBundleMapPath, kClientSpiffeBundleMapPath, std::nullopt,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// Valid SPIFFE bundle on the server side, but the client side has a SPIFFE
+// bundle that does not have a trust domain that will match the server leaf
+// certificate.
+TEST_P(SpiffeSslTransportSecurityTest, MTLSSpiffeClientMismatchFail) {
+  auto* fixture = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kClientKeyPath, kClientCertPath,
+      kServerSpiffeBundleMapPath, kServerSpiffeBundleMapPath, std::nullopt,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/false);
+  fixture->Run();
+}
+
+// The client side is configured with only a SPIFFE bundle, but the server leaf
+// certificate does not have a SPIFFE ID.
+TEST_P(SpiffeSslTransportSecurityTest, NonSpiffeServerCertFail) {
+  auto* fixture = new SslTsiTestFixture(
+      kNonSpiffeKeyPath, kNonSpiffeCertPath, kClientKeyPath, kClientCertPath,
+      kServerSpiffeBundleMapPath, kClientSpiffeBundleMapPath, std::nullopt,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/false);
+  fixture->Run();
+}
+
+// The server side is configured with only a SPIFFE bundle, but the client leaf
+// certificate does not have a SPIFFE ID. When negotiating TLS 1.3, the
+// client-side handshake succeeds because server verification of the client
+// certificate occurs after the client-side handshake is complete.
+TEST_P(SpiffeSslTransportSecurityTest, NonSpiffeClientCertFail) {
+  // TLS1.3 client will pass because it validates the server
+  auto* fixture = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kNonSpiffeKeyPath, kNonSpiffeCertPath,
+      kServerSpiffeBundleMapPath, kClientSpiffeBundleMapPath, std::nullopt,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/true);
+  fixture->Run();
+}
+
+// The server side is configued with a SPIFFE bundle, but the client side has a
+// certificate with multiple URI SANs which should fail SPIFFE verification. The
+// client's certificate is otherwise valid. This specific failure should show up
+// in logs. If SPIFFE verification is NOT done, we would expect this to pass -
+// it's a function of the SPIFFE spec to fail on multiple URI SANs. We verify
+// that the certificates used here would otherwise succeed when the root CA is
+// used directly rather than the SPIFFE Bundle Map, then that same setup fails
+// when a SPIFFE Bundle Map is used.
+TEST_P(SpiffeSslTransportSecurityTest, MultiSanSpiffeCertFails) {
+  // Passes because SPIFFE verification is not done, and this would be valid in
+  // that case.
+  auto* fixture_pass =
+      new SslTsiTestFixture(kServerKeyPath, kServerCertPath, kMultiSanKeyPath,
+                            kMultiSanCertPath, "", "", kCaPemPath,
+                            /*expect_server_success=*/true,
+                            /*expect_client_success_1_2=*/true,
+                            /*expect_client_success_1_3=*/true);
+  fixture_pass->Run();
+  // Should fail SPIFFE verification because of multiple URI SANs.
+  auto* fixture_fail = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kMultiSanKeyPath, kMultiSanCertPath,
+      kServerSpiffeBundleMapPath, "", kCaPemPath,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/true);
+  fixture_fail->Run();
+}
+
+// The server side is configued with a SPIFFE bundle, but the client side has a
+// certificate with multiple URI SANs which should fail SPIFFE verification. The
+// client's certificate is otherwise valid. This specific failure should show up
+// in logs. If SPIFFE verification is NOT done, we would expect this to pass -
+// it's a function of the SPIFFE spec to fail on multiple URI SANs. We verify
+// that the certificates used here would otherwise succeed when the root CA is
+// used directly rather than the SPIFFE Bundle Map, then that same setup fails
+// when a SPIFFE Bundle Map is used.
+TEST_P(SpiffeSslTransportSecurityTest, InvalidUTF8Fails) {
+  // Passes because SPIFFE verification is not done, and this would be valid in
+  // that case.
+  auto* fixture_pass = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kInvalidUtf8SanKeyPath,
+      kInvalidUtf8SanCertPath, "", "", kCaPemPath,
+      /*expect_server_success=*/true,
+      /*expect_client_success_1_2=*/true,
+      /*expect_client_success_1_3=*/true);
+  fixture_pass->Run();
+  // Should fail SPIFFE verification because of multiple URI SANs.
+  auto* fixture_fail = new SslTsiTestFixture(
+      kServerKeyPath, kServerCertPath, kInvalidUtf8SanKeyPath,
+      kInvalidUtf8SanCertPath, kServerSpiffeBundleMapPath, "", kCaPemPath,
+      /*expect_server_success=*/false,
+      /*expect_client_success_1_2=*/false,
+      /*expect_client_success_1_3=*/true);
+  fixture_fail->Run();
+}
+
+std::string TestNameSuffix(
+    const ::testing::TestParamInfo<tsi_tls_version>& version) {
+  if (version.param == tsi_tls_version::TSI_TLS1_2) return "TLS_1_2";
+  CHECK(version.param == tsi_tls_version::TSI_TLS1_3);
+  return "TLS_1_3";
+}
+
+INSTANTIATE_TEST_SUITE_P(TLSVersionsTest, SpiffeSslTransportSecurityTest,
+                         testing::Values(tsi_tls_version::TSI_TLS1_2,
+                                         tsi_tls_version::TSI_TLS1_3),
+                         &TestNameSuffix);
+
+}  // namespace
+
+int main(int argc, char** argv) {
+  grpc::testing::TestEnvironment env(&argc, argv);
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/test/core/tsi/ssl_transport_security_utils_test.cc b/test/core/tsi/ssl_transport_security_utils_test.cc
index f65f96b..b97706a 100644
--- a/test/core/tsi/ssl_transport_security_utils_test.cc
+++ b/test/core/tsi/ssl_transport_security_utils_test.cc
@@ -835,6 +835,60 @@
   EXPECT_NE(*pkey, nullptr);
   EVP_PKEY_free(*pkey);
 }
+
+TEST(ParseUriString, ValidUri) {
+  GENERAL_NAME* subject_alt_name = GENERAL_NAME_new();
+  ASN1_IA5STRING* uri = ASN1_IA5STRING_new();
+  ASN1_STRING_set(uri, "spiffe://foo.bar/path", -1);
+  GENERAL_NAME_set0_value(subject_alt_name, GEN_URI, uri);
+  absl::StatusOr<std::string> parsed_uri = ParseUriString(subject_alt_name);
+  ASSERT_EQ(parsed_uri.status(), absl::OkStatus());
+  EXPECT_EQ(*parsed_uri, "spiffe://foo.bar/path");
+  GENERAL_NAME_free(subject_alt_name);
+}
+
+TEST(ParseUriString, EmptyUri) {
+  GENERAL_NAME* subject_alt_name = GENERAL_NAME_new();
+  ASN1_IA5STRING* uri = ASN1_IA5STRING_new();
+  ASN1_STRING_set(uri, "", -1);
+  GENERAL_NAME_set0_value(subject_alt_name, GEN_URI, uri);
+  absl::StatusOr<std::string> parsed_uri = ParseUriString(subject_alt_name);
+  ASSERT_EQ(parsed_uri.status(), absl::OkStatus());
+  EXPECT_EQ(*parsed_uri, "");
+  GENERAL_NAME_free(subject_alt_name);
+}
+
+TEST(ParseUriString, InvalidUtf8) {
+  GENERAL_NAME* subject_alt_name = GENERAL_NAME_new();
+  ASN1_UTF8STRING* uri = ASN1_UTF8STRING_new();
+  // This sequence is invalid UTF8.
+  const unsigned char invalid_utf8[] = {0xc0};
+  ASN1_STRING_set(reinterpret_cast<ASN1_STRING*>(uri), invalid_utf8,
+                  sizeof(invalid_utf8));
+  GENERAL_NAME_set0_value(subject_alt_name, GEN_URI, uri);
+  absl::StatusOr<std::string> parsed_uri = ParseUriString(subject_alt_name);
+  EXPECT_EQ(parsed_uri.status().code(), absl::StatusCode::kInvalidArgument);
+  GENERAL_NAME_free(subject_alt_name);
+}
+
+TEST(ParseUriString, WrongType) {
+  GENERAL_NAME* subject_alt_name = GENERAL_NAME_new();
+  ASN1_UTF8STRING* other = ASN1_UTF8STRING_new();
+  ASN1_STRING_set(other, "foo", -1);
+  GENERAL_NAME_set0_value(subject_alt_name, GEN_DNS, other);
+  absl::StatusOr<std::string> parsed_uri = ParseUriString(subject_alt_name);
+  EXPECT_EQ(parsed_uri.status().code(), absl::StatusCode::kInvalidArgument);
+  GENERAL_NAME_free(subject_alt_name);
+}
+
+TEST(ParseUriString, DontSetASN1String) {
+  GENERAL_NAME* subject_alt_name = GENERAL_NAME_new();
+  ASN1_UTF8STRING* other = ASN1_UTF8STRING_new();
+  GENERAL_NAME_set0_value(subject_alt_name, GEN_DNS, other);
+  absl::StatusOr<std::string> parsed_uri = ParseUriString(subject_alt_name);
+  EXPECT_EQ(parsed_uri.status().code(), absl::StatusCode::kInvalidArgument);
+  GENERAL_NAME_free(subject_alt_name);
+}
 }  // namespace testing
 }  // namespace grpc_core
 
diff --git a/test/core/tsi/test_creds/spiffe_end2end/BUILD b/test/core/tsi/test_creds/spiffe_end2end/BUILD
new file mode 100644
index 0000000..1ec7688
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2025 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+licenses(["notice"])
+
+exports_files([
+    "ca.key",
+    "ca.pem",
+    "client.key",
+    "client_spiffebundle.json",
+    "client_spiffe.pem",
+    "intermediate_ca.key",
+    "intermediate_ca.pem",
+    "intermediate.cnf",
+    "intermediate_gen.sh",
+    "invalid_utf8_san.key",
+    "invalid_utf8_san_spiffe.pem",
+    "leaf_and_intermediate_chain.pem",
+    "leaf_signed_by_intermediate.key",
+    "leaf_signed_by_intermediate.pem",
+    "multi_san.key",
+    "multi_san_spiffe.pem",
+    "server.key",
+    "server_spiffebundle.json",
+    "server_spiffe.pem",
+])
diff --git a/test/core/tsi/test_creds/spiffe_end2end/README.md b/test/core/tsi/test_creds/spiffe_end2end/README.md
new file mode 100644
index 0000000..e2ca443
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/README.md
@@ -0,0 +1,38 @@
+All of the following files in this directory except `server_spiffebundle.json`
+and `client_spiffebundle.json` are generated with the `generate.sh` and
+`generate_intermediate.sh` script in this directory.
+
+These comprise a root trust certificate authority (CA) that signs two
+certificates - `client_spiffe.pem` and `server_spiffe.pem`. These are valid
+SPIFFE certificates (via the configuration in `spiffe-openssl.cnf`), and the
+`*_spiffebundle.json` files are SPIFFE Bundle Maps for the client and server
+respectively.
+
+The SPIFFE trust bundle map files (`*_spiffebundle.json`) are manually created
+for end to end testing. The `server_spiffebundle.json` contains the
+`foo.bar.com` trust domain (only this entry is used in e2e tests) matching URI
+SAN of `client_spiffe.pem`, and the CA certificate is `ca.pem`. The client
+`spiffebundle.json` file contains `example.com` trust domain matching the URI
+SAN of `server_spiffe.pem`, and the CA certificate there is also `ca.pem`.
+
+`leaf_and_intermediate_chain.pem` is a certificate chain whose leaf is a valid
+SPIFFE cert that is signed by an intermediate CA (`intermediate_ca.pem`). The
+intermediate CA is signed by the root CA (`ca.pem`). Thus, this setup yields a
+valid chain to the root of trust `ca.pem`.
+
+If updating these files, the `x5c` field in the json is the raw PEM CA
+certificate and can be copy pasted from the certificate file `ca.pem`. `n` and
+`e` are values from the public key attached to this certificate. `e` should
+*probably* be `AQAB` as it is the exponent. `n` can be fetched from the
+certificate by getting the RSA key from the cert and extracting the value. This
+can be done in golang with the following codeblock:
+
+```
+func(GetBase64ModulusFromPublicKey(key *rsa.PublicKey) string {
+    return base64.RawURLEncoding.EncodeToString(key.N.Bytes())
+}
+
+block, _ := pem.Decode(rawPemCert) cert, _ := x509.ParseCertificate(block.Bytes)
+publicKey := cert.PublicKey.(*rsa.PublicKey)
+fmt.Println(GetBase64ModulusFromPublicKey(publicKey))
+```
diff --git a/test/core/tsi/test_creds/spiffe_end2end/ca.key b/test/core/tsi/test_creds/spiffe_end2end/ca.key
new file mode 100644
index 0000000..bb60bf9
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/ca.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC19PImArmxbwgq
+o2QG2si8BU6E69Bvyqxz8Je3swxBIGwZ9uIobMSBLeTCYyXuf+o90Zf0kMwzmrAK
+eLEeky5W/j07zGXAtgUBPA7L1Uk0TxOdJXBCUvEm5Oc4GxubfO7F+pdJKZ+XkRVq
+bnStGe5qX6KNs5rcJfFUhewbtM0snGEIf2yhaA0mNNuGtlIm4VB7jmiyHNU5YTow
+ByVCVrV3/t2RI9+T4ya0AlkW93rU0M0qQauJ35LkJIXifbzrnLxmztEyb+mnVUB+
+GJgz01E4teWo/PJb1aNJ/ojf/UONsQ5IFRdza6RhaQB7C+Dxlnt/SJ3MMaxHgVyc
+YuJeVtJQuncGRSuQ2YrmW9b36HVnxa0xBDeSluUjv48hMRlLNaXaH4yuK6oc8TNJ
+ie++/ir6Kb4H+0RjcKMGqxZYfotU1obxa+5N3wzGSjUDUrhofzlfvqbp+NCwdFH+
+qczM4IZPL8YMMh6goKr9BRN9/xRIieotyH6rfKNcnkUgDp750U0cZ7P2eRUpldyc
+9hZS5AlF4cKQXgLIrv1LrZHkiIietetInUEBAa/PF2YHRLXUyI1PCSBKBu7wdwAU
+15J9dVFC9jkmOLYhoRdPfrobpWhs5+FfPJumSoiusdGXd7x4l313xi2V02YXz5mR
+GbT2lCb6aJPweuziiEBZn+5KV++DkQIDAQABAoICAADWyAO4mVHKyGYpOdNT36do
+4HZZAir2+CCTv9z8wLnrF2cF36+xvNp+an5PoYdjoxL+HP5EVT4fFWLQ22VQw5N5
+uEKAoZDGBXUpNJ/UKO1rFWA8U+iHFmDYsxlzX7lrjSkBk4fOw1rJ12HcFZo1580V
+J/GO1TmVcf0Z2jLX9Q6+TD2xTzsWFIMWp/PPMP5FqokXeBQij3LluW4Wsbf2qEBu
+/a2g5HjL6QhBgxqci6eX7b3b7sbHjLp66G+kM6n4JIFE4+Z1+sypdVBYoUESRjlB
+9qIF5Gb53QTgLYgn93AKD/hGtJ7jUcrESliAoCgWbdkYDm/vkStkX6vWdXmX01fl
+hfv6c1eYTkDhOboVZL0p0DxZwHU4Ci6UF0PflrHmospKo12bLoa2dea+5LfQVgB0
+icijZ1ZF4oePzFOtIdDYmNDfTkBe7q52HKNIPbLytXTFv9gHj/XpRdDUDgNzS7TO
+wXP1k4zY96H9YEgXNr/Ze7IQt711/Szj/bIPfuCyJTSMRh40wPW+1TzGkdQepBDO
+lHk1uVs185lJd3TryUGhfXu9X49hXOpvq1nhOy3UW/FoDPLGckOoQGe9PikP3zQC
+6poTttdmfmTy7D3vniZUjDmbztyml3twM50+KGhGJTbjEKDqs9dASbn5B69LbnOA
+kq3SzgJOGgTiTT8EFwjzAoIBAQD8LjTqiEB8TY9qQjihtcidDWBA7TcBjIQ/Wm4p
+3ff0NdGPa6/qaCPX7769wBW5/fJfoJu9ZhebWfLLYacK2Dz1BYn5EqoAf4mF4C9S
+ZDY4NO3tl8t902bjlQs+ehojyzOOVHxuATZ4mBrCFUYuFn1YPXR0qIvvXsvhRM4k
+LCf9lGl1ldUpWNbdxTxUl7NZVuZKevwBvfsGB9SPgnvDCgg0Mh4pBGUDP0WZqwaS
+frvB2iP4hVH5e/lUl2XxtlJIil8Eb1rGH2O6CmbvkSc6Sceg67/E0N6u4w9i+g1G
+uu+FFdPQSve3jUrOrhQjO7tqjh6p8J858H7CvoJo7Uojk8yjAoIBAQC4tnUCvlns
+VJQ9SVqabVuIWPJvgDf/EVHncVFwJZtdsFRchs+QFtUWyJRD6mRtHtBdyIpsQCBb
+YxPYbMoQClpEC7VNC9Y6RDKVxRM+A1cQ1JiP4PQTyzYIVfoU/RbWuixXHOvGOYsb
+LwjGZ7WQtaG8Pyqt8P97OIqZzeuQK5y+ZuixDZpBsy3xhYyQcC1oowLLgf7ip1QU
+ueEF/fi3OZCd7oIbZX10EYr/QINnlyYstRxnHz2AOO4kilQEt4Yq2YKxo/GiXVp+
+HlDD53xl/1HfT1sV9BnnoRQksmt61LO3qz6hI7TgQtFt4+Z3TsXB4siWoFtmfGRo
+njuShRAdjt47AoIBADo2EFDzEq+e17QXhSsO4zgdllJmb4QUA5L8NOHFMy9UHQOW
+QA1D7MIzvTvf0yQT0pbwZALcRia+JKV97Sk7sRZ+KNMt08A5sG0Oyt2h9EIVNbba
+i0aIPf/ar7XESbRpe4UnS3G5JfHAaqzSbFjHKDfuN6uWHHcs0rM1RsBi0PhgAdf6
+tjHNKAwZM0vzmJrOzGn06acdS4yg0hgubpMfBbXGu9+L/CU7vb0zFVlLl9uvlw4O
++FU2oN8983JQO74y8qv7aGDDbinLrOcwAnYaAdLLmIC+FikYkLywK9Bf5lo6J6K1
+K2lbXCFJXaykZSa7l6hyus6NDTW29DN0UuDThTECggEBAIlRU0G74zv3UNETBGFD
+pUWC4pCT4I0mAh6uNp3Jx2vqF31F127GGpo6eUbYLNqWis79tDJvpd5qlKlTjxBH
+sf1eOuVRRGzCoFyJSxnR/9sKQXTNBobkjoG58RXDZUQy2Z3VaU5PHyQRXlOpa+ht
+rxQ0XXU8caHYUemvlUzzry45cQmXFDN9kk04PFxwmMFuVjiWprc3MCHMYgZDXbBR
+Ajr2whC97XrpQ80o18eeLYoVBk1/gwgcyqWtlHKBQSP/2dJYD93lys1xmhdZszhc
+jDAYXZ2/f7acIE57ub1x/T0w+HlxAxmpcupf/8h4bjoKb5bTFb/l//tqjt0JQQbI
+XFsCggEBALd+S5rIRYz3YpXCnt+uT6ELbD7RfDtoGdWtLavm3vQb3yZCXGwlHesP
+ud1xXZ5w1Ky3GSqvXJ4F+UiVd+DLEoq6u0Y8adkKmNpwElJffG7+/9ZgDgAUQDhr
+i4yqH5wYHs2tSdc0hHEqmLjn0uylX+y4pWojHD1l0w7KrepUzj9SNVrOyzl8D7D2
+xXFNb+rSY4B9ZwPofYqxGxM6GGo47GFFOGqFsCtwp8lcbSMgzx0j36Ibg9uPmoeM
+53ea7Fbqv+fWVzhu+6r7BE7mp0rYBWPMRg/euciYl2X3FBRjhqt1/GENDwejYkFS
+qLeaSJiKSNSUyCl91Qu0eb4FbE5qLwI=
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/ca.pem b/test/core/tsi/test_creds/spiffe_end2end/ca.pem
new file mode 100644
index 0000000..c086690
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/ca.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFlTCCA32gAwIBAgIUCD73sHXu5IURolSTiJ127a/xUO4wDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTAzMDcxOTM0NDZaFw0yNjAzMDcxOTM0NDZaMFoxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIDAJWQTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRsw
+GQYDVQQDDBJmb28uYmFyLmhvby5jYS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQC19PImArmxbwgqo2QG2si8BU6E69Bvyqxz8Je3swxBIGwZ9uIo
+bMSBLeTCYyXuf+o90Zf0kMwzmrAKeLEeky5W/j07zGXAtgUBPA7L1Uk0TxOdJXBC
+UvEm5Oc4GxubfO7F+pdJKZ+XkRVqbnStGe5qX6KNs5rcJfFUhewbtM0snGEIf2yh
+aA0mNNuGtlIm4VB7jmiyHNU5YTowByVCVrV3/t2RI9+T4ya0AlkW93rU0M0qQauJ
+35LkJIXifbzrnLxmztEyb+mnVUB+GJgz01E4teWo/PJb1aNJ/ojf/UONsQ5IFRdz
+a6RhaQB7C+Dxlnt/SJ3MMaxHgVycYuJeVtJQuncGRSuQ2YrmW9b36HVnxa0xBDeS
+luUjv48hMRlLNaXaH4yuK6oc8TNJie++/ir6Kb4H+0RjcKMGqxZYfotU1obxa+5N
+3wzGSjUDUrhofzlfvqbp+NCwdFH+qczM4IZPL8YMMh6goKr9BRN9/xRIieotyH6r
+fKNcnkUgDp750U0cZ7P2eRUpldyc9hZS5AlF4cKQXgLIrv1LrZHkiIietetInUEB
+Aa/PF2YHRLXUyI1PCSBKBu7wdwAU15J9dVFC9jkmOLYhoRdPfrobpWhs5+FfPJum
+SoiusdGXd7x4l313xi2V02YXz5mRGbT2lCb6aJPweuziiEBZn+5KV++DkQIDAQAB
+o1MwUTAdBgNVHQ4EFgQUL/YTEXKZRjbOv82CwGMuBeNwErIwHwYDVR0jBBgwFoAU
+L/YTEXKZRjbOv82CwGMuBeNwErIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
+AQsFAAOCAgEAIYtNERVMj4dS/KgoXD3a6mF6KD+IuxDTlVbVTwDJMzdNW5UPB2qC
+iDKbirK8Eedh90r3qxKzBiQPE5AHDZrbWSBAxDhlpQqCXgx8Z+nCB38K9i8hbKM/
+ablm9DkeHiHohoP1kJEN2HtPz1K6OabbKbPtVGt8y+QTIlZDodPPzKPmQ6dUTm9t
+WlU1oIxl4pPVR6WXDr0qAmNRvdW7+8/Ai+gMDi6fKQJCe/r/meVrI9lSrnn8VQyO
+4xXvyolHEROTUiomlJ1QE9IWAM9LNFOuWFwQjayo2d6O+zHqjGUtbzBHFb8/BLJc
+QzogWYT0/+rEboJhR+/OGe0ntPPkudmLL0HTx0Q1aajnChMVlaOd2wucePZSa6Gq
+GEVA+lCqwbteqwJCnL7deVHo+UlORFgQwYir2CQbyN9Yd4MtQT7VZuDiVC6tlGbj
+1ogrpOnW8n49jXrNPWuPz2hcPQb2gzYFGI9WRKIX4SjvvS3QcHUyigQkjAQ12Ldj
+5CXBYTjSmjcgsa2QfUY5qrXsHLz7e4uXD9XYnB/XEEfxQfQTFEy5CtzTrTxnT+cw
+RuBooIr6InjqdbdJ+UbkWcIY6w+c7ndFT849pUgtrojbVpm9ZzsmHhAahR/+iHYH
++LGeHYIpq71o/YDgM4vV9z3sWic7u1YE9JULBdlaDw8Xik82zjvw4IA=
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/client.key b/test/core/tsi/test_creds/spiffe_end2end/client.key
new file mode 100644
index 0000000..d3f694a
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/client.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/Y4KWLto/B3f3
+YRlwJXn2l+hlre80L36k0dNmyvZW6nY0zZfXGJBHV3UlIT3YB+vIsePu3KtpfJak
+xjBuyL65LtZphxkR+1mfL6ucVqgroI6b7WFt/Zo6vjElTbJGMaNcprItlLNjBxKw
+d0elYWS4q/TkhUw9cPfywE+FCNy3OPLPbI677lD1C29AlI8ToTHfjY1PqJ+9dS2j
+xg7qAskWUsOTjfXO8SiQbF15DkD2hZDg9I2/BBC9YhB5xUOCywb5U9Ucomp7Q9aH
+J3OkvsN1n0Un7GHkEn1iyXaa61vh94uk7M1e/ewvBdmC+JUgLaU0tKdg7dFIgc4g
+KF4ewvITAgMBAAECggEAGNHb1oQe83uReDhUZzHE/lC257DFy5u5LcC0TKlXhvAD
+AlgDL/bUhxv1aipEbeahEjbXm1tss1Jf6uB02XkZGsTTM348pxNUjQtOkwi96GJ6
+/Aet0ejIfoO5td+8ldPMHrDTfIvDM71wFAhz9yOJP1W1fYk32OP0k7uS1GrQzMXQ
+6y5pA+Mvn1lXb9zsufkrvyp4F+QGUiXnPNEPXC+9u5pNqY0OkNvudn3cfU6t2jJO
+lUies9xiX5GatWQ4kp/i26HoTGvefuM3mnNmm37dSt/5zalfrECC9e3mKkba1StY
+XPavOtXkjA1X51w5MqVL1wEBYQCQTlr+KSATR44y2QKBgQD98MID26fNYSrNuTxN
+I9YHGqFmwB3qe2jOec9dqmOtdh8ji+UEX1z10Sm6Vrdk75LwwCp066LBxyYVTJo2
+PgN6Dn0ARHEF0j4GgJiYWKhOJk+mwVHDENuQFSpGgP+mLXGGyo8irCz/HP6g5uww
+70h+QDPtNEccDHLAUaaA92DdfQKBgQDA8OEtKPtgWZBdTEvcKQbn49m1aihqjV/q
+kr8Nn28Y39i90vyGoO67muTuSAvDNgW6ne/W2Ic5VhSicIVyVHV0p3cUB2NGwdnp
+RvT+7wd22pVUMW9Lys4hAg1O4TtoO5Th/lRDXf28IAmxQPz7jc3alr2AEgaawrEH
+t/gyph5izwKBgAm/uHMVd7e2x0Hmnp4Hhiv3pcKgFYXu/vMOUhuycukx47glosEs
+OBFn2kZBFgFWATok5nxen3z4/ElNC46RiuigrcPY3M3waFkvjqwqmyuE5QHt4gmv
+F/7METdrtGTY0cbu99fI7GFO56edteubBBl0tYmhvCkjgTRI7i99T8axAoGADvGZ
+PzoxXiERwWgrZ/31fvhy4E48myQgDbcX/SIgJLkRvIIe9P55/yXcHWiPIKuKfg3F
+icpzjslQB9TXQSexgTuHSZHY6p4RFrRcPXKeDWKYtlCt4hzvIjdaeIWRaF1VBrwA
+Iseg43VHyubRsVomW4vZWZYL1OjHdWWWX7XmHJkCgYEA2LUeWNkdqABJ9Yta7kKu
+02gkOsjnQyyYIeFCteGSM9oejE4zDM+WSCpVCLzq8DcRK8M/xBhCIf+mVZRroHAz
+g5YpFrg5a7kuuTgQJv/6ju+Mbd+DGpsul/N2zIPFzy6T27gIzL8+QExUK0y1XkL/
+7yGUwV4OO2kuwh8YT+TiCts=
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem b/test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem
new file mode 100644
index 0000000..475b490
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEvjCCAqagAwIBAgIUBoJ396S7DO0kRqDUn4TB6zKKIIkwDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTAzMDcxOTM0NDZaFw0zNTAzMDUxOTM0NDZaMEwxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRMwEQYDVQQD
+DAp0ZXN0Y2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2OC
+li7aPwd392EZcCV59pfoZa3vNC9+pNHTZsr2Vup2NM2X1xiQR1d1JSE92AfryLHj
+7tyraXyWpMYwbsi+uS7WaYcZEftZny+rnFaoK6COm+1hbf2aOr4xJU2yRjGjXKay
+LZSzYwcSsHdHpWFkuKv05IVMPXD38sBPhQjctzjyz2yOu+5Q9QtvQJSPE6Ex342N
+T6ifvXUto8YO6gLJFlLDk431zvEokGxdeQ5A9oWQ4PSNvwQQvWIQecVDgssG+VPV
+HKJqe0PWhydzpL7DdZ9FJ+xh5BJ9Ysl2mutb4feLpOzNXv3sLwXZgviVIC2lNLSn
+YO3RSIHOICheHsLyEwIDAQABo4GJMIGGMEQGA1UdEQQ9MDuGOXNwaWZmZTovL2Zv
+by5iYXIuY29tLzllZWJjY2QyLTEyYmYtNDBhNi1iMjYyLTY1ZmUwNDg3ZDQ1MzAd
+BgNVHQ4EFgQUVHpZYzO6JCGBTQwzWbj/CVC6sTAwHwYDVR0jBBgwFoAUL/YTEXKZ
+RjbOv82CwGMuBeNwErIwDQYJKoZIhvcNAQELBQADggIBACa1C7Zn0rXXIXC9SI8G
+MPSTyebo2s/OZCQ2sKQEK90yL33Ae97beiBVgFlt2TTXeEW91iZMZMfn9BlRDYkZ
+j8qdcNbG1QQEco2ZT3Ti13psVoSIgX2yMbds5DFkalJhXCx21xxNHLVUtd1xRDhu
+JvFm+V4TNFi1ka0U5fNcxRAGeVkR7tzpvUy8La+aNHOOAEaOX4oCQKueDXhGJGcL
+EXMz6N/KyLq4otaKGO67D2q5or6hH1afgrIcdZV0Gb7Cf+nR18QNWvH0Nm1cXdkT
+OL/iKlDqbhLGznGazz6T1EWlbynGjupFWdXDIPjsgN0xrAJS7NWjkUSgWcA94J7Z
+JwtIsjgP0DWg7fQsYa4cR9rxS+gYtkiKZuRTONSGYxQLAxJEb5NCCXE6fBC6jfHZ
+xtsY1ExlA6f6t+E7mS0OkRqbb6KZqTFGgSyx99jDL46FUH3zqA4IOs8Q4loo/Zkh
+LNjeG/3MoO7RvyY9g58y+yf3ZOp92zoyKMSqCBsvdTMRP0hWGq4BqoW/3Sn3ujf1
+uf5KEE+peuYzEqK1mtuZQHe/+1Vm7h88/TlER7mijMN3/MFZzmor890vBbzqf7KQ
+Q2QCRwIISj3a0puskpZM13Jt0r8cM+npkk1evfgmpBjNLrOinIQgWy30Qlxt5fIh
+jYaOBLHtSFI01OHnivQ1dFWG
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json b/test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json
new file mode 100644
index 0000000..18f81da
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json
@@ -0,0 +1,18 @@
+{
+  "trust_domains": {
+    "example.com": {
+      "spiffe_sequence": 12035488,
+      "keys": [
+        {
+          "kty": "RSA",
+          "use": "x509-svid",
+          "x5c": [
+            "MIIFlTCCA32gAwIBAgIUCD73sHXu5IURolSTiJ127a/xUO4wDQYJKoZIhvcNAQELBQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAeFw0yNTAzMDcxOTM0NDZaFw0yNjAzMDcxOTM0NDZaMFoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRswGQYDVQQDDBJmb28uYmFyLmhvby5jYS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC19PImArmxbwgqo2QG2si8BU6E69Bvyqxz8Je3swxBIGwZ9uIobMSBLeTCYyXuf+o90Zf0kMwzmrAKeLEeky5W/j07zGXAtgUBPA7L1Uk0TxOdJXBCUvEm5Oc4GxubfO7F+pdJKZ+XkRVqbnStGe5qX6KNs5rcJfFUhewbtM0snGEIf2yhaA0mNNuGtlIm4VB7jmiyHNU5YTowByVCVrV3/t2RI9+T4ya0AlkW93rU0M0qQauJ35LkJIXifbzrnLxmztEyb+mnVUB+GJgz01E4teWo/PJb1aNJ/ojf/UONsQ5IFRdza6RhaQB7C+Dxlnt/SJ3MMaxHgVycYuJeVtJQuncGRSuQ2YrmW9b36HVnxa0xBDeSluUjv48hMRlLNaXaH4yuK6oc8TNJie++/ir6Kb4H+0RjcKMGqxZYfotU1obxa+5N3wzGSjUDUrhofzlfvqbp+NCwdFH+qczM4IZPL8YMMh6goKr9BRN9/xRIieotyH6rfKNcnkUgDp750U0cZ7P2eRUpldyc9hZS5AlF4cKQXgLIrv1LrZHkiIietetInUEBAa/PF2YHRLXUyI1PCSBKBu7wdwAU15J9dVFC9jkmOLYhoRdPfrobpWhs5+FfPJumSoiusdGXd7x4l313xi2V02YXz5mRGbT2lCb6aJPweuziiEBZn+5KV++DkQIDAQABo1MwUTAdBgNVHQ4EFgQUL/YTEXKZRjbOv82CwGMuBeNwErIwHwYDVR0jBBgwFoAUL/YTEXKZRjbOv82CwGMuBeNwErIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAIYtNERVMj4dS/KgoXD3a6mF6KD+IuxDTlVbVTwDJMzdNW5UPB2qCiDKbirK8Eedh90r3qxKzBiQPE5AHDZrbWSBAxDhlpQqCXgx8Z+nCB38K9i8hbKM/ablm9DkeHiHohoP1kJEN2HtPz1K6OabbKbPtVGt8y+QTIlZDodPPzKPmQ6dUTm9tWlU1oIxl4pPVR6WXDr0qAmNRvdW7+8/Ai+gMDi6fKQJCe/r/meVrI9lSrnn8VQyO4xXvyolHEROTUiomlJ1QE9IWAM9LNFOuWFwQjayo2d6O+zHqjGUtbzBHFb8/BLJcQzogWYT0/+rEboJhR+/OGe0ntPPkudmLL0HTx0Q1aajnChMVlaOd2wucePZSa6GqGEVA+lCqwbteqwJCnL7deVHo+UlORFgQwYir2CQbyN9Yd4MtQT7VZuDiVC6tlGbj1ogrpOnW8n49jXrNPWuPz2hcPQb2gzYFGI9WRKIX4SjvvS3QcHUyigQkjAQ12Ldj5CXBYTjSmjcgsa2QfUY5qrXsHLz7e4uXD9XYnB/XEEfxQfQTFEy5CtzTrTxnT+cwRuBooIr6InjqdbdJ+UbkWcIY6w+c7ndFT849pUgtrojbVpm9ZzsmHhAahR/+iHYH+LGeHYIpq71o/YDgM4vV9z3sWic7u1YE9JULBdlaDw8Xik82zjvw4IA="
+          ],
+          "n": "tfTyJgK5sW8IKqNkBtrIvAVOhOvQb8qsc_CXt7MMQSBsGfbiKGzEgS3kwmMl7n_qPdGX9JDMM5qwCnixHpMuVv49O8xlwLYFATwOy9VJNE8TnSVwQlLxJuTnOBsbm3zuxfqXSSmfl5EVam50rRnual-ijbOa3CXxVIXsG7TNLJxhCH9soWgNJjTbhrZSJuFQe45oshzVOWE6MAclQla1d_7dkSPfk-MmtAJZFvd61NDNKkGrid-S5CSF4n2865y8Zs7RMm_pp1VAfhiYM9NROLXlqPzyW9WjSf6I3_1DjbEOSBUXc2ukYWkAewvg8ZZ7f0idzDGsR4FcnGLiXlbSULp3BkUrkNmK5lvW9-h1Z8WtMQQ3kpblI7-PITEZSzWl2h-MriuqHPEzSYnvvv4q-im-B_tEY3CjBqsWWH6LVNaG8WvuTd8Mxko1A1K4aH85X76m6fjQsHRR_qnMzOCGTy_GDDIeoKCq_QUTff8USInqLch-q3yjXJ5FIA6e-dFNHGez9nkVKZXcnPYWUuQJReHCkF4CyK79S62R5IiInrXrSJ1BAQGvzxdmB0S11MiNTwkgSgbu8HcAFNeSfXVRQvY5Jji2IaEXT366G6VobOfhXzybpkqIrrHRl3e8eJd9d8YtldNmF8-ZkRm09pQm-miT8Hrs4ohAWZ_uSlfvg5E",
+          "e": "AQAB"
+        }
+      ]
+    }
+  }
+}
diff --git a/test/core/tsi/test_creds/spiffe_end2end/generate.sh b/test/core/tsi/test_creds/spiffe_end2end/generate.sh
new file mode 100755
index 0000000..44a33b7
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/generate.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Copyright 2025 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Generate client/server self signed CAs and certs.
+openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.pem -days 365 -nodes -subj "/C=US/ST=VA/O=Internet Widgits Pty Ltd/CN=foo.bar.hoo.ca.com"
+
+# The SPIFFE related extensions are listed in spiffe-openssl.cnf config. Both
+# client_spiffe.pem and server_spiffe.pem are generated in the same way with
+# original client.pem and server.pem but with using that config. Here are the
+# exact commands (we pass "-subj" as argument in this case):
+openssl genrsa -out client.key.rsa 2048
+openssl pkcs8 -topk8 -in client.key.rsa -out client.key -nocrypt
+openssl req -new -key client.key -out spiffe-cert.csr \
+ -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=testclient/ \
+ -config spiffe-openssl.cnf -reqexts spiffe_client_e2e
+openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \
+ -in spiffe-cert.csr -out client_spiffe.pem -extensions spiffe_client_e2e \
+  -extfile spiffe-openssl.cnf -days 3650 -sha256
+
+openssl genrsa -out server.key.rsa 2048
+openssl pkcs8 -topk8 -in server.key.rsa -out server.key -nocrypt
+openssl req -new -key server.key -out spiffe-cert.csr \
+ -subj "/C=US/ST=CA/L=SVL/O=gRPC/CN=*.test.google.com/" \
+ -config spiffe-openssl.cnf -reqexts spiffe_server_e2e
+openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \
+ -in spiffe-cert.csr -out server_spiffe.pem -extensions spiffe_server_e2e \
+  -extfile spiffe-openssl.cnf -days 3650 -sha256
+
+openssl genrsa -out multi_san.key.rsa 2048
+openssl pkcs8 -topk8 -in multi_san.key.rsa -out multi_san.key -nocrypt
+openssl req -new -key multi_san.key -out spiffe-cert.csr \
+ -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=testclient/ \
+ -config spiffe-openssl.cnf -reqexts spiffe_server_e2e
+openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \
+ -in spiffe-cert.csr -out multi_san_spiffe.pem -extensions spiffe_client_multi \
+  -extfile spiffe-openssl.cnf -days 3650 -sha256
+
+openssl genrsa -out invalid_utf8_san.key.rsa 2048
+openssl pkcs8 -topk8 -in invalid_utf8_san.key.rsa -out invalid_utf8_san.key -nocrypt
+openssl req -new -key invalid_utf8_san.key -out spiffe-cert.csr \
+ -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=testclient/ \
+ -config spiffe-openssl.cnf -reqexts spiffe_server_e2e
+openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \
+ -in spiffe-cert.csr -out invalid_utf8_san_spiffe.pem -extensions spiffe_client_non_utf8 \
+  -extfile spiffe-openssl.cnf -days 3650 -sha256
+
+rm ./*.rsa
+rm ./*.csr
+rm ./*.srl
diff --git a/test/core/tsi/test_creds/spiffe_end2end/intermediate.cnf b/test/core/tsi/test_creds/spiffe_end2end/intermediate.cnf
new file mode 100644
index 0000000..eeeb3af
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/intermediate.cnf
@@ -0,0 +1,38 @@
+[ca]
+default_ca = CA_intermediate
+
+[CA_intermediate]
+dir               = .
+certs             = $dir/certs
+crl_dir           = $dir/crl
+new_certs_dir     = $dir/newcerts
+database          = $dir/index.txt
+serial            = $dir/serial
+RANDFILE          = $dir/private/.rand
+private_key = $dir/intermediate_ca.key
+certificate = $dir/intermediate_ca.pem
+crl = $dir/intermediate.crl
+
+# For certificate revocation lists.
+crlnumber         = $dir/crlnumber
+crl               = $dir/crl/intermediate.crl
+crl_extensions    = crl_ext
+default_crl_days  = 3650
+
+default_md = sha256
+
+[req]
+distinguished_name = req_distinguished_name
+req_extensions = v3_req
+prompt = no
+
+[req_distinguished_name]
+CN = intermediatecert.example.com
+
+[crl_ext]
+authorityKeyIdentifier=keyid:always
+
+[v3_req]
+keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign, cRLSign
+extendedKeyUsage = clientAuth, serverAuth
+basicConstraints = critical, CA:true
diff --git a/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.key b/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.key
new file mode 100644
index 0000000..3e5fe81
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7XOGe2r5KNdo7
+ELwJ7GRAIVqxAkT9bB6+7rUyDhJnxWeI7E9vr+I4clDRYREoxnuXq6oGTICKlWiv
+vrxBASSdA0PpjLsMNnNU1n9XHXEBEa3YM7w85UBvf/8IdlpiyHgPkg+1DdvuToOa
+0Xhejnq/aCcNyErjjNN26k/06joO3+feYvsn1tToXomif4HUU+SE5xIGiuPHcDwJ
+tjk/Iugh+pNPdltOguxq2E7m8upQvwShe2A6azD1wV9HiyFtkf7tlp4f900MmRyb
+kHb7zXfgXBwlf7waa2RP3UDZbJAZZZkOFQmPcujNvkAz/ghaf0Uf2Ix/P0Zh69LU
+notPVa35AgMBAAECggEADMnb740UMHoJBX+Qqie7twHf0BWs0lRjK9qcvQgnWvRn
+KvogOOobKYmp+QSkWHFlxbY2onvtySfGD7rX3CMBFgN/Su1rZfvAdwG2CuLsT+ue
+P1PWrRxjv8iPhosXg8cVpNBuKIhD4vnycyqJigC1IaPxCHiLngkCdQssvnWPVWz2
+nXp0SKcDKJu/sU8Mp0UW490DIUi8iSQHkjbpXu+RExAPAjhe+vvEuExZFs8dN0Rp
+g3oj8l6pLkfJ/M4tfib7WiQxxtZWEEuEYsz3JDpxySFWR59SRZ2tYKv4k9TNbQXA
+qX8jqVGT+YubWKY4HtRxd2L3yy443ERQdwQecLsYIQKBgQDgPpRts1pz9UnrXnr8
+ee8nO4SSxsJxge4OX5/yX7Lm/JwOPyk6/nCH3LKqxOWMOZpOpgI3ithA1Qvb1JuM
+vDQo13z4Xi5RMJm8h3B5O0f9brw3L2h+hrImJfYuozj0icyFSbT/PjO1pGB/QAAF
+ie6G3JQLRlBFF0dclcqb4tpEEQKBgQDV5UAYKGz+sdDpzgarTmsyd0YVTRitdBMv
+S7eddG/gCBMaISXXXksYXX+Y74zbs2LCfkLz+i4j8BDoRFPfPlK/daCOMOriJ++Z
+LNBClmkBiwqlZXs9d1dEWmJis5GbJEiAneoNskAy6UspFMCLu4QJJHfB5aZsAphm
+4509tRqTaQKBgF8gDOTwVWmlXyMaZD+gFiRlahq9eBSgknTEedxeXk0AUc71Wi8t
+al0n8R7iAaJXeS7t2zjmjFAMUEiDyyyLVfERYHEXurw0SrMgHUVAMYy/odYjJUev
+Kflm1yT3lpydrAXKu54fK95dCZZRdvDijy3kemTCAiEc++e5n0Y7mG2RAoGAeMtN
+5ha2byPWeqOqaoYPzeFEB/WRMSuzRet514cRQzDsML51k2oh+LGcxK7MGqr05CfQ
+Ad7QveTud21W/GVC7/Mq6AJCM9Qf4J5JQsGUrZVrPrrP3YePFgABPgJxAW99Ln1/
+15pJ3TaZBqs8je0pBMy+gRcDZE4W6Uwz2cx0JLkCgYAn6/C71GXvLkjDdmRHS2zA
+K+gOTCb4YdGDCnv5Cez64pega6mUy+EIznJYpI3KwHbAsUFkYgNJ7VvUtVIu2z9U
+UHSBHilN/cMWRCgN3KXyYOHr50g0P+gei0bhiWddIh8+tC1J/627g2r2Jwowuarn
+1JLr0ZYgh3SXglOkgyvIKA==
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.pem b/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.pem
new file mode 100644
index 0000000..84cabe6
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/intermediate_ca.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEkzCCAnugAwIBAgIUa4ZkytupQZ5OwDMPw/vRY8deFc4wDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTAzMTgxNjE4NTFaFw0zNTAzMTYxNjE4NTFaMCcxJTAjBgNVBAMMHGludGVy
+bWVkaWF0ZWNlcnQuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQC7XOGe2r5KNdo7ELwJ7GRAIVqxAkT9bB6+7rUyDhJnxWeI7E9vr+I4
+clDRYREoxnuXq6oGTICKlWivvrxBASSdA0PpjLsMNnNU1n9XHXEBEa3YM7w85UBv
+f/8IdlpiyHgPkg+1DdvuToOa0Xhejnq/aCcNyErjjNN26k/06joO3+feYvsn1tTo
+Xomif4HUU+SE5xIGiuPHcDwJtjk/Iugh+pNPdltOguxq2E7m8upQvwShe2A6azD1
+wV9HiyFtkf7tlp4f900MmRybkHb7zXfgXBwlf7waa2RP3UDZbJAZZZkOFQmPcujN
+vkAz/ghaf0Uf2Ix/P0Zh69LUnotPVa35AgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQD
+AgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBTozd97no7w/qAJQZxNFwSYq6ydVjAfBgNVHSMEGDAWgBQv
+9hMRcplGNs6/zYLAYy4F43ASsjANBgkqhkiG9w0BAQsFAAOCAgEApXU0I4IWvGYo
+qpU0EdDcja7qAV3MaXQRk5hgViaSd+iAd3I04EMDeWomTVb3ZFSPBR2cwPy3vJ6Y
+cLx1gxRCH2gJNj85EjwQ44Fj4/gbSnx2rNVeTmzjyQ4bt6dzdaorGW6ydZgqfX3f
+n++wx9an7Q56NuBOLihInTMHOocVnizHrse5zXN3dXMZxBOU1EHCgKi9xAGMZRmR
+r+AYYXcWB6PRxxwCX7VG4STGYz+GRb4rx0hEl/oQatMqxatN1nAvXCwBASWp47UX
+3n3Tl0/jq0c7DSbL5i+VQridDaifRozBbOkDowS+JDbV1BzYQ/oo9C3kzjraRCXI
+ZJrO9pMNTyLHOXwC9LdlN/30Pe1j40jVo1CR29uONeqmUeJKHfPFBtucVE7ohqHu
+ESHuxZWb0VP7uVWaVIOQ9l8XZcLXtnmSQpHW+4aYbE4q9L0abZ312yRAtyOM/NSH
+wdJn8436jJnu1t8Va85M6lzQHY1E0gxuT9cw02PuS65odFseW7H9gGtlqlOzfVV2
+/psCLlAjVSRQ78zIH6y8DvWGMqabyznIAgZf/PVj9cydPA1he9We6V//Kdcv/Bb8
+RQyryujLBalUTyA9AtDGijp+NWkb1h1uyX0vhiIjJubEu6KKQ5YrZkLXjz+E4I20
+E5GmOYoJhLGkeoUjoTC2wuNFsLFJJHs=
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/intermediate_gen.sh b/test/core/tsi/test_creds/spiffe_end2end/intermediate_gen.sh
new file mode 100644
index 0000000..6fa8698
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/intermediate_gen.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Copyright 2025 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e
+
+# Meant to be run from testdata/spiffe_end2end/
+# Sets up an intermediate ca, generates certificates, then copies then up and deletes unnecessary files
+
+rm -rf intermediate_ca
+mkdir intermediate_ca
+cp intermediate.cnf intermediate_ca/
+cp spiffe-openssl.cnf intermediate_ca/
+pushd intermediate_ca
+
+# Generating the intermediate CA
+openssl genrsa -out temp.rsa 2048
+openssl pkcs8 -topk8 -in temp.rsa -out intermediate_ca.key -nocrypt
+rm temp.rsa
+openssl req -key intermediate_ca.key -new -out temp.csr -config intermediate.cnf
+openssl x509 -req -days 3650 -in temp.csr -CA "../ca.pem" -CAkey "../ca.key" -CAcreateserial -out intermediate_ca.pem -extfile intermediate.cnf  -extensions 'v3_req'
+
+# Generating the leaf and chain
+openssl genrsa -out temp.rsa 2048
+openssl pkcs8 -topk8 -in temp.rsa -out leaf_signed_by_intermediate.key -nocrypt
+openssl req -new -key leaf_signed_by_intermediate.key -out spiffe-cert.csr \
+ -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=testserver/ \
+ -config spiffe-openssl.cnf -reqexts spiffe_server_e2e
+openssl x509 -req -CA intermediate_ca.pem -CAkey intermediate_ca.key -CAcreateserial \
+ -in spiffe-cert.csr -out leaf_signed_by_intermediate.pem -extensions spiffe_server_e2e \
+  -extfile spiffe-openssl.cnf -days 3650 -sha256
+cat leaf_signed_by_intermediate.pem intermediate_ca.pem > leaf_and_intermediate_chain.pem
+
+popd
+
+# Copy files up to the higher directory
+cp "./intermediate_ca/leaf_signed_by_intermediate.key" ./
+cp "./intermediate_ca/leaf_signed_by_intermediate.pem" ./
+cp "./intermediate_ca/leaf_and_intermediate_chain.pem" ./
+cp "./intermediate_ca/intermediate_ca.key" ./
+cp "./intermediate_ca/intermediate_ca.pem" ./
+
+rm ca.srl
+rm -rf intermediate_ca
diff --git a/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san.key b/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san.key
new file mode 100644
index 0000000..53884a2
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsHcgPDkXqAKpD
+HdrTbsm5yXBXZPvNPc1ME5wgXdh1BWwSY6SIgmHcsdIHOP+6wZtp+NDZVcsVT+8M
+gTbE3a19dvJ6idU8n8vytuF5TwQ8L26QxeJtMTTkmn8M2zz3Jw8cb2UJnP5ZAyOa
+S3b77+NZ6UYHk8eoXx5InoRzRgf7h1IxaUHNu89CA7sprgMRI50WuUV+9hzxNpUG
+FtYpWODfj17QJUUfdJVlz/6Gr3tEEEybsKoDFmM0F2TVe9pueP+VwQ0QU93siu8G
+1g1YL0lrsCakQKnbjAJtFpSaVU1LPjr6WKaz8Y0e6kBmTNHpa0AG231FFYWgcuAH
+M9/ZrLmPAgMBAAECggEAEJe2Z60qlEqIH3xLUkLF88E1sVcs5zZkCkeBr6Ucbvuc
+1ZEBlZ9sK8iAxO17yddMUkUIbUtWDrHsC7qROx1b0lKbQbDz+6o0ESwMRJOXhu3T
+rOVG1Nv/i2JRiT3BfNopHMaoaOrXomEpF4Da8aLIuixA8g4PG25ADyQpBKvlrRvP
+26pSzi1yQqajCD3ilZzx+aBukboBZvz727iQw2ylQSj8BqLZFM7hrpWf6QtYeILc
+iqz9xMAwGV9OOavkpMIeVliAl6pSTG0AZpz1eQqcpJ4UKdmK26u0RlwjXidk2w05
+sbyxDQ4exsxb9pH2FtNBw5u+W1l2ZwGeD9660MlBcQKBgQDqLBd0ejhgokyixg99
+AUEilgrUoWfa+Kv+ZooiLrJ4ZWesvryWqf0ytOYn1J0W4otUIeZwY3W+pAMf2IC4
+Ftx4AcsiOoOIxDdKEQ0PiRM2tiY3hGxKMIk1Rszv52KvhybxPm8I7pDQ+ftAGZx7
+ss+/gzRl7Qy1Q5h0SIpK0n5xgwKBgQC8KONT6RmUD0TDWCfZogQLib2/ylOKTD0A
+G0cnDcyMtYvkU75uc9jaz7xv6XN+4uIz+gLvFCmQKLLL9VvM0geJbEyCXrtDn3s9
+Fai8FkIDBNiXM18p4/VFKikodJkwq5020CgQnM+u5db7pImS4Q6279vZOWjF+Ibo
+e8YUcJvWBQKBgFO7lF933HThU9nYTG18Z1gAorrJRCOOS6HZ9Iaof/SA/gNM6I5P
+FlRtTubrJYirVPqubIG/brjOWLeoaM5qjkbzDTmeBq5+yu5XDn6BmzczBpNPbJFS
+rZ05Rcp6yFTPVONqWbWt6GZxHKAdCFFK7PIj7jaL/sFJ/FeIILsEbWYVAoGBAKdM
+d6whI1QJK2vBE1Wnf48+vo8r95GM7eQNXPt1gn1N4n92kmik6pjLglHDMaUNWHE8
+h6VJuRbriOZeV2xLBtaCi5iTYE/lGm+atXffFnyWr3no/GFDxc3kicnluluTC2s0
+M5tpwiNRSxBeINCO+UVhCYogSG9V5aYKf3mnEnh9AoGAYi0UmjbiKZYWjVI7hNn6
+dqUwXJk8y3Bq0bsp1hLzn7OOChGhLR4hrrMLQHQDDlPHg8pK9UJnovGW80nOyNrw
+IPLHW8keC3FPwUaEQN05gSc3HQ3scY5d/E7nf2eNk3HHjEdRQS3meWIhH1P2dR1y
+M1AO/1HkIycWAliDtT/987c=
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san_spiffe.pem b/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san_spiffe.pem
new file mode 100644
index 0000000..419a93b
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/invalid_utf8_san_spiffe.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIErTCCApWgAwIBAgIUDZpLwN2bBsy5lvMkeCC7DgYl95kwDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTA2MjUyMDQ1MzhaFw0zNTA2MjMyMDQ1MzhaMEwxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRMwEQYDVQQD
+DAp0ZXN0Y2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArB3I
+Dw5F6gCqQx3a027JuclwV2T7zT3NTBOcIF3YdQVsEmOkiIJh3LHSBzj/usGbafjQ
+2VXLFU/vDIE2xN2tfXbyeonVPJ/L8rbheU8EPC9ukMXibTE05Jp/DNs89ycPHG9l
+CZz+WQMjmkt2++/jWelGB5PHqF8eSJ6Ec0YH+4dSMWlBzbvPQgO7Ka4DESOdFrlF
+fvYc8TaVBhbWKVjg349e0CVFH3SVZc/+hq97RBBMm7CqAxZjNBdk1Xvabnj/lcEN
+EFPd7IrvBtYNWC9Ja7AmpECp24wCbRaUmlVNSz46+lims/GNHupAZkzR6WtABtt9
+RRWFoHLgBzPf2ay5jwIDAQABo3kwdzA1BgNVHREELjAshipzcGlmZmU6Ly9mb28u
+YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMVx4YzAwHQYDVR0OBBYEFBwEmOn47l9h
+nK4rCTbox85Au0c5MB8GA1UdIwQYMBaAFC/2ExFymUY2zr/NgsBjLgXjcBKyMA0G
+CSqGSIb3DQEBCwUAA4ICAQCqgy3ZgqsPI2KO4+VBHU0hdpxR86fTKYslRbvC9i8G
+03I0+SJwI3YpXZpG1Mc3xwVYqRY8VuY9y0Ku2zKW5p7ako/wvcV1KhI/Ueq8qzCR
+vlL0Fse5DnXmwFZBS/hEVc97d0+ERXveyKBjaU+tC7KwHGfFefDUt+pyijYNALpK
+WpDwXhAEy9p6qmv2bjyMtVhj4nP/gG1jXozRwmdPOQcDe97trfQcq/n1zr9x5eUe
+qGD2iQhFBlgR+4Rf/w+M5kpPOQ8hz4dKySz5nF/u8+tvc32S9RivxHA4t5GaF5xg
+AOI3lSyoC6SJBt75ADzRLpbVYkharUTA5McitM1fIKeBtzJ5H3vqiHh2L1ESv/Ud
+2FjD1siLlrUjpffoUgtppr4bWSWXMxCiaKlWgqbMvgBaAIm1xZdkk3ZH36J4jQnZ
+/svRU5jpJpZu7K8/w2TmSj7l//16h+XmYOHE2BmVHliFqy2aBUi1YYiNhJi+xDdL
+mrJzn4KEgVY7fGmD3iP0oM5+gh0cKDdZ4zi/KgYFhyt8o+qLTe3EXq1pwnOXQn1p
+2LNymeGg0A69rhYot6AOgq6PecXFVvY7RLkrOqfex86Lk8aUl5+prcNRvlOgdmoh
+x1NAfGm0Ae4Bd7/t0WuZaEYtUtY+qfVX68rLP58jZXQlN+XwuDqX4bl1UWkfGCjc
+fw==
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/leaf_and_intermediate_chain.pem b/test/core/tsi/test_creds/spiffe_end2end/leaf_and_intermediate_chain.pem
new file mode 100644
index 0000000..a1cd901
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/leaf_and_intermediate_chain.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDvjCCAqagAwIBAgIUSuuNuIcMCQ1UZrOtCL689eeZx9EwDQYJKoZIhvcNAQEL
+BQAwJzElMCMGA1UEAwwcaW50ZXJtZWRpYXRlY2VydC5leGFtcGxlLmNvbTAeFw0y
+NTAzMTgxNjE4NTFaFw0zNTAzMTYxNjE4NTFaMEwxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRMwEQYDVQQDDAp0
+ZXN0c2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAue2BY6B4
+JNz5SnYr0b4IUsXfpSxqXzpx3CymRzNiPL+ZYERQd+5PPbwyj/K0ZUPNcdGtgj0i
+jCSpKl6fGNx11vY/Z5BS7DN9Z6Fqf8/sBr5BIko33UafdqB/s1avrIzFNRD1n6Fu
+X1EwlQpXwjiTMLswuf6X6mJVJ2VhA/xtyO4S6fTeK2jUlFn4MYHVczJBDTiMCUPX
+1Z/9SXkcwZbQTbRaWvd6c0OEWZKSWxSzWG6d4qOmq9C0Tse39cmjuDpeBvtiGik4
+pYrTmmswiQ8lgLbOY1/YGjvOD+qlxXkX9Y/cYjdygrQoY7mqnjMcUFvWK/OW0XCB
+CJ1jeSflrYUd4QIDAQABo4G8MIG5MHcGA1UdEQRwMG6CECoudGVzdC5nb29nbGUu
+ZnKCGHdhdGVyem9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29t
+hwTAqAEDhiZzcGlmZmU6Ly9leGFtcGxlLmNvbS93b3JrbG9hZC85ZWViY2NkMjAd
+BgNVHQ4EFgQUsMpVfuYdrW5+i5mFW/hCaOZiKKcwHwYDVR0jBBgwFoAU6M3fe56O
+8P6gCUGcTRcEmKusnVYwDQYJKoZIhvcNAQELBQADggEBABReEQ2cIaZv5wZKOjFS
+5b5QfpkbR29JTQQxrkiqdXcnonX+Q/dCH+tnzUp9LCCIsVjp6is4pdKTvPNg1ZPT
+WB0KEFpVG9LAOmfcgu9pwIsKUCS384PeVhffMKXsKWfgfRGP1E7XeEIdSgpwPKzK
+Defa7kwTG276xre/36WOAu9RZ32s0ltXyvHtTi0pfaGuOmryX+XeHtVpxQSjhdDB
+tpLvolS+7cRsvKNKIL5X/789IKbdShMgpwPDDe6Z3riBjGPBydnA40IofRc8y3jL
+0TKiBiKYOz8nj6DLAiVPqQ2vo5NzihNoqgu/HpkhSG26OAJq2NDYurMsT+Myiuq4
+YkE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkzCCAnugAwIBAgIUa4ZkytupQZ5OwDMPw/vRY8deFc4wDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTAzMTgxNjE4NTFaFw0zNTAzMTYxNjE4NTFaMCcxJTAjBgNVBAMMHGludGVy
+bWVkaWF0ZWNlcnQuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQC7XOGe2r5KNdo7ELwJ7GRAIVqxAkT9bB6+7rUyDhJnxWeI7E9vr+I4
+clDRYREoxnuXq6oGTICKlWivvrxBASSdA0PpjLsMNnNU1n9XHXEBEa3YM7w85UBv
+f/8IdlpiyHgPkg+1DdvuToOa0Xhejnq/aCcNyErjjNN26k/06joO3+feYvsn1tTo
+Xomif4HUU+SE5xIGiuPHcDwJtjk/Iugh+pNPdltOguxq2E7m8upQvwShe2A6azD1
+wV9HiyFtkf7tlp4f900MmRybkHb7zXfgXBwlf7waa2RP3UDZbJAZZZkOFQmPcujN
+vkAz/ghaf0Uf2Ix/P0Zh69LUnotPVa35AgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQD
+AgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBTozd97no7w/qAJQZxNFwSYq6ydVjAfBgNVHSMEGDAWgBQv
+9hMRcplGNs6/zYLAYy4F43ASsjANBgkqhkiG9w0BAQsFAAOCAgEApXU0I4IWvGYo
+qpU0EdDcja7qAV3MaXQRk5hgViaSd+iAd3I04EMDeWomTVb3ZFSPBR2cwPy3vJ6Y
+cLx1gxRCH2gJNj85EjwQ44Fj4/gbSnx2rNVeTmzjyQ4bt6dzdaorGW6ydZgqfX3f
+n++wx9an7Q56NuBOLihInTMHOocVnizHrse5zXN3dXMZxBOU1EHCgKi9xAGMZRmR
+r+AYYXcWB6PRxxwCX7VG4STGYz+GRb4rx0hEl/oQatMqxatN1nAvXCwBASWp47UX
+3n3Tl0/jq0c7DSbL5i+VQridDaifRozBbOkDowS+JDbV1BzYQ/oo9C3kzjraRCXI
+ZJrO9pMNTyLHOXwC9LdlN/30Pe1j40jVo1CR29uONeqmUeJKHfPFBtucVE7ohqHu
+ESHuxZWb0VP7uVWaVIOQ9l8XZcLXtnmSQpHW+4aYbE4q9L0abZ312yRAtyOM/NSH
+wdJn8436jJnu1t8Va85M6lzQHY1E0gxuT9cw02PuS65odFseW7H9gGtlqlOzfVV2
+/psCLlAjVSRQ78zIH6y8DvWGMqabyznIAgZf/PVj9cydPA1he9We6V//Kdcv/Bb8
+RQyryujLBalUTyA9AtDGijp+NWkb1h1uyX0vhiIjJubEu6KKQ5YrZkLXjz+E4I20
+E5GmOYoJhLGkeoUjoTC2wuNFsLFJJHs=
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.key b/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.key
new file mode 100644
index 0000000..23c07a7
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC57YFjoHgk3PlK
+divRvghSxd+lLGpfOnHcLKZHM2I8v5lgRFB37k89vDKP8rRlQ81x0a2CPSKMJKkq
+Xp8Y3HXW9j9nkFLsM31noWp/z+wGvkEiSjfdRp92oH+zVq+sjMU1EPWfoW5fUTCV
+ClfCOJMwuzC5/pfqYlUnZWED/G3I7hLp9N4raNSUWfgxgdVzMkENOIwJQ9fVn/1J
+eRzBltBNtFpa93pzQ4RZkpJbFLNYbp3io6ar0LROx7f1yaO4Ol4G+2IaKTilitOa
+azCJDyWAts5jX9gaO84P6qXFeRf1j9xiN3KCtChjuaqeMxxQW9Yr85bRcIEInWN5
+J+WthR3hAgMBAAECggEACGVwarbpIAyK1TUQkeVnl7HUAX/3yoa/lDBfvq7ESp8i
+KnPRULYuRcjfPWsJ4JoTD7cW418WhZ7E9SruWzt+9pL6nmcZCI3DdbPnVN/nTi9m
+rnlMrCljm4JAXBFfx+54PCcQuYmLLAKlC5D60TR3+KsWZmrX8kuoIeohC0KZtFlw
+xHC1fz6k9qbStDW8UBWUY8I1N2Zh9U5kYGNG9j5rLYzs7zJ23xWmiGOwywGlLSii
+2i9vC+XwQATFmgFyVM9F9W4P//ElOADDXDhBU8R3UXXoK5+s+Bx12gyTeBfSLgy1
+I5/ee2rFJdlGUsOvEO39npHDCeFDKe0MjeJY8FcSUQKBgQDcXJYnxM6ULY3T9jrf
+rSiVi4S7Rewq7KdFDKo32Lqr85VJ3+nqOtLjdyTnHpsRYQSBDbMluMjImfcWxLym
+e/3ldNdRR4W6YSdL8kvnKvxsCYR/XC5wihfgKLY3yU7iydrJjFdQZlmFLbCAom7U
+P26OJXo8STCD9BURFKlzm24W0QKBgQDX/0lBWuWNhF8h65WK7Lp8zj2LzZFfNOB2
+YysKKteYxDmyV7dKZogG9KDDbDpiylDhg58i8c949/73diCGbZHRMCFFeTO6SOjx
++2Z0iLgMs1IxqLCLhv/H/nfyNii+/G80JDHKR0d0xFQRfAzIVbBaxjL6JiI9aJdl
+Y+yJYHZ6EQKBgQCTtPftVk1GI7bSolTaQ35VpzxkLz1blF+WuCOM5ZhS1CZ4az+n
+AqEDWYSB6xD0ODzFqIIxwcfMNu/Z8iulyKzqVkRRtghcYuztSk84d6bVYBfRKt/y
+DY5tcbyabjSTbcxalticCs7spzCNONPjL1WSsGpb6I89k6lfVqMy27eSUQKBgQCG
+rFdidmg2Klieb6LX8e38ryLUriF7uR5S8lX9iuTODVrkBaj0rKUXoSMzdaGZwwdW
+9JgeU6LGi+nfJTn2Vw7Z3SaBiKZl+du0NMmW0z/eO1h+Oe2JsWx9p/3leTtCiWZU
+nlSlCHhXJ6o7FJtrtXG1x2o1ad1jaHks8Hak2Q5F4QKBgQCQpMwv662qJai06bpQ
+Q1A3rzn/LbDIPZmkadRE9+Lv1KDHp3uhkxX53DK7K560adwUYurzfsyp3VV763h4
+73Lk/z6wOTjmXNO3b/+asKx9z063XZc0vUxct7Del5+C28s026XfUOy30IMaIQqr
+ei17qa7SQn3GwetSxblr+IQKHg==
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.pem b/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.pem
new file mode 100644
index 0000000..e9c9f06
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDvjCCAqagAwIBAgIUSuuNuIcMCQ1UZrOtCL689eeZx9EwDQYJKoZIhvcNAQEL
+BQAwJzElMCMGA1UEAwwcaW50ZXJtZWRpYXRlY2VydC5leGFtcGxlLmNvbTAeFw0y
+NTAzMTgxNjE4NTFaFw0zNTAzMTYxNjE4NTFaMEwxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRMwEQYDVQQDDAp0
+ZXN0c2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAue2BY6B4
+JNz5SnYr0b4IUsXfpSxqXzpx3CymRzNiPL+ZYERQd+5PPbwyj/K0ZUPNcdGtgj0i
+jCSpKl6fGNx11vY/Z5BS7DN9Z6Fqf8/sBr5BIko33UafdqB/s1avrIzFNRD1n6Fu
+X1EwlQpXwjiTMLswuf6X6mJVJ2VhA/xtyO4S6fTeK2jUlFn4MYHVczJBDTiMCUPX
+1Z/9SXkcwZbQTbRaWvd6c0OEWZKSWxSzWG6d4qOmq9C0Tse39cmjuDpeBvtiGik4
+pYrTmmswiQ8lgLbOY1/YGjvOD+qlxXkX9Y/cYjdygrQoY7mqnjMcUFvWK/OW0XCB
+CJ1jeSflrYUd4QIDAQABo4G8MIG5MHcGA1UdEQRwMG6CECoudGVzdC5nb29nbGUu
+ZnKCGHdhdGVyem9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29t
+hwTAqAEDhiZzcGlmZmU6Ly9leGFtcGxlLmNvbS93b3JrbG9hZC85ZWViY2NkMjAd
+BgNVHQ4EFgQUsMpVfuYdrW5+i5mFW/hCaOZiKKcwHwYDVR0jBBgwFoAU6M3fe56O
+8P6gCUGcTRcEmKusnVYwDQYJKoZIhvcNAQELBQADggEBABReEQ2cIaZv5wZKOjFS
+5b5QfpkbR29JTQQxrkiqdXcnonX+Q/dCH+tnzUp9LCCIsVjp6is4pdKTvPNg1ZPT
+WB0KEFpVG9LAOmfcgu9pwIsKUCS384PeVhffMKXsKWfgfRGP1E7XeEIdSgpwPKzK
+Defa7kwTG276xre/36WOAu9RZ32s0ltXyvHtTi0pfaGuOmryX+XeHtVpxQSjhdDB
+tpLvolS+7cRsvKNKIL5X/789IKbdShMgpwPDDe6Z3riBjGPBydnA40IofRc8y3jL
+0TKiBiKYOz8nj6DLAiVPqQ2vo5NzihNoqgu/HpkhSG26OAJq2NDYurMsT+Myiuq4
+YkE=
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/multi_san.key b/test/core/tsi/test_creds/spiffe_end2end/multi_san.key
new file mode 100644
index 0000000..3e6f46f
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/multi_san.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5Ri7ktF7OC/w1
+EH37GBcrUWQ9bIcThAeGWMSvOGsu/BxU3H7nVFxDi901wlrii+Asj3HnhQQVFxtW
++zGbNhC2ksQHHJCPFYqSyaeQt4GroQ8I5itiHnrRDXB8z1fJDLa6ddmkQORlgpxz
+7kH1ak/pkXM05vq9YifDXrYDgWl0awTbkPNFbUZ/yWcmUJ8qdtuz21WDN491NNtf
+Folpoicm3iT6vVspUUA+2PAwB2DDzma5avXzJ/OkE1QUl1Eko7V0JKkYjon6m2Ix
+nDge81Er4JLhz5KQEbOdbfLPNjtxTMzD0mJLRm5sTqtqqgRWi7ieVumcUOljRdtP
+mrZuAsAJAgMBAAECgf9EHqnfLPIuj/mLCSVIzPK32pUggGappkGeA8VNJ1FBVTCV
+gvyl0T6bFzGeMnd4FDW4LIWUujNmDbjfojv69UXv2sZ4VuhPs3vfvIT/kHj8wZrl
+irsouEwv0OErApEkOqh0ph//ZFyVdhBQD6nwscXv+qqBKOvH9e7oxknlwA8l3hLk
+DkZw6VhHm0TZZK2Zo0zZ4i3P0pKdv1P374F4Gk08HAV8X8Mi2btYnGTDjCDBHvv7
+Chxr9RAqRU4wQFAxt8QkP/AKzxC9iSdziQHmXwaKpz/GVS98p//FcwM7h9rNR2/9
+aW1VY6CFd3x7DUl+a2Iekgt4y+TU6MzWt62OsSECgYEA5mk8gMxitxKSd5R9Jvbm
+KaLXEf2G95PbkyyrR1N3L+v8SvuwTlZH+TWV8UfiZFCF1FDos1lkNW/TO8oTv8SP
+nMKvjJ2NnTb0V14mTmg4EkcBDCZFQ4uc6kMB+eCpfqFPyhfGUdj7k9WuTrVBTH8c
+/GLUk+QajRVZ44cSfCzGO6ECgYEAzdmrXmxLZcUPXddrXQV9P4Z832D39jgw+hiL
+SSUr7dAIQGi6Gg45kqfG7u2+m8HR87RCGIyo7SCkzqC2jyTWq0OaSvFS597qTdYB
+ag90ZN5EYzslyODZdOWcfxEb+BVRsV3sVSI37CuepD7+mR6a7RU3xiK3PFadwWSI
+LqDIa2kCgYAyF573wXeUpY0CQl4TIopXcvefCadWG+cY0BVMpHC9Emj/REjIPynW
+nDfFBQPLG1Z9Gjn9A+otU+pwjTZN8sXmJs1TnM006MWmWOmrEi1ei+FGauuUC75J
+j9fWI2m2Bgv6u8B5+tp3AXWr1uwjXV9H35SnsmezVHj+Zlc2d+WBAQKBgQCLIfx6
+LagoeTERtHCUxwkIztXIBbqTaRdxcvVx0vYGHv30ytQ+mC7BuTT0eSzEChTmK4gh
+H7Ft1QqPvoCl77hIucCntsTahAjJ40PBdACvzKtchrXbOF7CbA+CswW8gGxe/aSn
+RiGWN0VarllXFSCR7i1sdjpzRfgKpvCE05ejQQKBgQCSxGbweGR4eZefTqYd/4vn
+WJapI9siN95SKg0e5ENRYVBvPS+tRZH6PjCep/DNlU3IMaAmCIH7j8vdV326OXDc
+VfGCUcbxAmPDo9bqmEc/0ceC4IrWTn0ejGOZIYefHVSiHQogHO/dn/GgMydqLtcs
+OXw/IVWtzvL4iUN8V3m5Ng==
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/multi_san_spiffe.pem b/test/core/tsi/test_creds/spiffe_end2end/multi_san_spiffe.pem
new file mode 100644
index 0000000..a99eaf3
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/multi_san_spiffe.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIE0zCCArugAwIBAgIUeFNyqxRU+RxnKwrjen0Zil6qZJswDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTA2MjUxOTUwMThaFw0zNTA2MjMxOTUwMThaMEwxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRMwEQYDVQQD
+DAp0ZXN0Y2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuUYu
+5LRezgv8NRB9+xgXK1FkPWyHE4QHhljErzhrLvwcVNx+51RcQ4vdNcJa4ovgLI9x
+54UEFRcbVvsxmzYQtpLEBxyQjxWKksmnkLeBq6EPCOYrYh560Q1wfM9XyQy2unXZ
+pEDkZYKcc+5B9WpP6ZFzNOb6vWInw162A4FpdGsE25DzRW1Gf8lnJlCfKnbbs9tV
+gzePdTTbXxaJaaInJt4k+r1bKVFAPtjwMAdgw85muWr18yfzpBNUFJdRJKO1dCSp
+GI6J+ptiMZw4HvNRK+CS4c+SkBGznW3yzzY7cUzMw9JiS0ZubE6raqoEVou4nlbp
+nFDpY0XbT5q2bgLACQIDAQABo4GeMIGbMFkGA1UdEQRSMFCGJnNwaWZmZTovL2Zv
+by5iYXIuY29tL2NsaWVudC93b3JrbG9hZC8xhiZzcGlmZmU6Ly9mb28uYmFyLmNv
+bS9jbGllbnQvd29ya2xvYWQvMjAdBgNVHQ4EFgQUZzYg1h15LWXJsHFO2X2DqskW
+SfwwHwYDVR0jBBgwFoAUL/YTEXKZRjbOv82CwGMuBeNwErIwDQYJKoZIhvcNAQEL
+BQADggIBAAHXGB9qEt/q/6JClvcbvt19l1C7YabLJ8HBYo9zIPnjDE306AKK9umK
+/ZJ4BovfMcR3xbeHrem3h4nIuS6s84vAhKTKVHIZZl9ix4NrkPPtriQLsRxRxRoi
+Ivk5BgylVj8TxMLMfoOinzrxXX7nQ+Yw6nBLlpo8atPS7qRXpene2p8814Eqk2UA
+svicIlGFm2YnizjluYyICf8PuVXFxwFDRMvrTicKD8i/WiFPKAlLsaH0WdL36hRt
+PnA7Pd7Ska7S7xgcYeJ2RQ2bMUzKtRL67tXHIrUKd/5l4kb843TIoA11JNO9vT+J
+iyAYpJErAHK3mczS7GGMDr5SIOuhZlLJV1XA3p7eqhlw5B2W77255WCIuHX3Mhm3
+4gq/ObivSwMnR/ZPV0PRDKoqMy5TmeZHa8TUC455cHm9HXnF6owpanLoI+tdV7tN
+Em0p0x4u2jXqTFbacLX3KENodm+4d6ESHGucARBo6d4b693qyNj7nxDUYmRB70W4
+aX5JVTJ/J6sN3Df741VvJX5sjNID3Z7B/ZFR+VQIpp6GqdhrQT1W6JuMymLAGQRi
+CJeeFKukArX35hwaXtWziPqp2QDOHa8d20GBMJSdVRhoG2LkAwxzzA15F2UEj+de
+AVXizkiJ2+uRtAs4uHmVV8oSl1L2UAMFTTvKX//r7XSj/TkR3x4G
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/server.key b/test/core/tsi/test_creds/spiffe_end2end/server.key
new file mode 100644
index 0000000..6bc006c
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpfq2SvTp7uxmk
+ux2D9fM3yBOum3YjRx3HLplC0GkBOHCwvsvw03ipQk8DJOSz0enun6SaLEhOZJ3Z
+9Y24qEeMEeWfm2rWrCzULmf2qH9lmJ1fYdj3V/v8x+ulgUWiSRB9LiwJzSFt0uTK
+L+PZZLjfkze8SFB6Zn6NBnTKzWbLs/ZxjiJcin8YLMoAnS5FfEVMfaDwE3RO9kED
+Jt0YG8bf6p8Tv773aOrDW2WszxWSUwoVMe2ByHajf6vC7/oyh1lrq0QIUy0AnXst
+3DKPolenY+Qt+17XP9HZf52cLVolzuhjw+yjEKMxbwHtXewayMHkoV5ySck5VrC9
+iF34jyTfAgMBAAECggEABxYYw63NqCamZ9C65y19QbPHvuEZC1iJ0i4MklKMUVS+
+wWT4udTdkpYHH6aHOU1pbmSWSSvYyg5C2z388t92PlDxPL8OtHL2XNij2BgrW2AK
+FFbO72hqTKM/IuhItQRBADrR9IbzAthyYFGoQQqJAC/zgJe1ZairWdIPR/tWdOW7
+1+ctVWIMCKpTpomuoI1oK7PJos6ciIQ5yCWPhMVyQ4EInG4YHF4r2agxWyaF5MLT
+gu/qH7tKvkCgo+jjofEntCrw7QYqW8HSFLGm5wxus4/vxoyrwRAgwSotm13tpbg8
+ODleK3u8S50zXuVk/+smskHv1QMGVAxuVFi7ZpPHAQKBgQDU6KpvYOZPR5+XdQNn
+BBGbkgaw0qQ7E+1w3pbcaEpglcLQRQn2fjwUOc7YWV8SpMXIneqfhvGLPJXKExXK
+coCdjyPAy77zAt7HOVJwAfYaLaoyxnAxnFIgTCLUOchvysWuvauYOkBF5d1GCbXK
+v5XUqbLiWnQoLPESubm/IJEEfwKBgQDLzJ/wmEn8vCZk6saEJoFkfG43oJaGrDOT
+kKE5pgLSNOtBB6ML4xliU1rwbHz+gpmQKBpb1zQedSyB9rd5G+NzPFQ14RoTsR10
+71S6o8jacIJxo6vZWrS3yOJSBcEhU2K8yFRGgI3cy53JtT5gZIckyINnovdTZpR7
+7vdhPngvoQKBgQC4FH4flGEsVJnIYrRRYjQzFLtSMJ0sMxuNBfbblBQSlAl+9uIZ
+S6V4O076wbKwtOQIg3iy+wJmRzifIYcqpXtMPtFDxSVQiL/C8m2zSA7XLXpFMksY
+qbZcFFfctCT3exMV9QmisWKWzJpXDtgt9x19ZEe0604Y2lqIvSm69VZM0wKBgHHG
+I8gijLOOqnSOaRMfl/7sRG/DtCUs/4IzLP7NAiguKOPeCpU5TFOLK2qrdkBz0p0a
+9lQtWUhjq9xGSvlOq3UKygxmRbOWnlZIwmmLxDbGttwQPoESVW+As3CNC9u5/JZd
+1EajwA7ykX0pNNCFbrkHQ+zYmvCxhNWHfA2K3XfhAoGAUtwFJFNRCYIGiFVrQ19n
+nwCt4+3zYT8wTL1EbyWa0pKhADFSwnwdhJ1ZNev1hKu663+iQO5KdlBeoicicpOA
+hNkrw5Al4pvJBp+WV6pqCLtMbuJUBYAWOpUipGnnJsEYTC9SxDnIqh+6Qc7tsEYv
+xDykqFvrNxQXLahWavYW8Lg=
+-----END PRIVATE KEY-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem b/test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem
new file mode 100644
index 0000000..f607d7c
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE+DCCAuCgAwIBAgIUBoJ396S7DO0kRqDUn4TB6zKKIIowDQYJKoZIhvcNAQEL
+BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAe
+Fw0yNTAzMDcxOTM0NDZaFw0zNTAzMDUxOTM0NDZaMFMxCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIDAJDQTEMMAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRowGAYDVQQD
+DBEqLnRlc3QuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAKl+rZK9Onu7GaS7HYP18zfIE66bdiNHHccumULQaQE4cLC+y/DTeKlCTwMk
+5LPR6e6fpJosSE5kndn1jbioR4wR5Z+batasLNQuZ/aof2WYnV9h2PdX+/zH66WB
+RaJJEH0uLAnNIW3S5Mov49lkuN+TN7xIUHpmfo0GdMrNZsuz9nGOIlyKfxgsygCd
+LkV8RUx9oPATdE72QQMm3Rgbxt/qnxO/vvdo6sNbZazPFZJTChUx7YHIdqN/q8Lv
++jKHWWurRAhTLQCdey3cMo+iV6dj5C37Xtc/0dl/nZwtWiXO6GPD7KMQozFvAe1d
+7BrIweShXnJJyTlWsL2IXfiPJN8CAwEAAaOBvDCBuTB3BgNVHREEcDBughAqLnRl
+c3QuZ29vZ2xlLmZyghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55
+b3V0dWJlLmNvbYcEwKgBA4Ymc3BpZmZlOi8vZXhhbXBsZS5jb20vd29ya2xvYWQv
+OWVlYmNjZDIwHQYDVR0OBBYEFDJn3wGxfbvBrS/dTLOScNtTMi9EMB8GA1UdIwQY
+MBaAFC/2ExFymUY2zr/NgsBjLgXjcBKyMA0GCSqGSIb3DQEBCwUAA4ICAQCaWp4p
+qmG5il353PrAedmk7kk5+YigeX/uUBYCdOm/j/M4IpJpsmkfFStYIhJAq3H2KViQ
+x7MNRLd15Z2fcdx9qWJ7aHq9fp9W7L1dINbiFD0KTp9c9oD2HpXnldy8r1EIDDaQ
+VxIlJDd3rUsebLT4oPZ13HQDp5clJ44WdcybqFgbvcJoxhXYMlBXOKwsLlm+abDO
+0XWEfubO/irsrsByUmaoGc/0Adf2O+wgDo9xSB4d2b601K02dYpl71NNelRvBrB5
+FubExuAef+4xgha/eMn+qcRBhH2jv7Fjc1HuKH8srukcRJHblQy96reJj2bcDhp/
+bPJ2xBzc13UZ2do9A209qGRCHONd5VH5NY++484XlFf12jJUEQiUCT7jOReFbUbR
+FkiHbyapVEmqnI8RifPbRIov/XzCqkaCZuMW+64cr6eBpuoi8YV+xV37zHYhT2bf
+Wpk83uyuvw6qZApQPLc2MQ6MpRv3740S/0+lKjNoG5xi2loNCMTGCgtt7EQT93cZ
+z/picgGigC1qIPonukQsdf+cCD3eC0Kd45VEj8bon4MxmjbROUdVzVwG4wmptF7l
+jB3GjnqHfEJl1RuDsLFEtBSqjESQTeeOuNMcPKSfNVbePLf4CxuRmIXumpwAxicw
+aeSaOwEPybd7LBd8qJHSRgfKMMgKnpZgzV4SZQ==
+-----END CERTIFICATE-----
diff --git a/test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json b/test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json
new file mode 100644
index 0000000..928d15b
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json
@@ -0,0 +1,18 @@
+{
+    "trust_domains": {
+        "foo.bar.com": {
+            "spiffe_sequence": 12035488,
+            "keys": [
+                {
+                    "kty": "RSA",
+                    "use": "x509-svid",
+                    "x5c": [
+                        "MIIFlTCCA32gAwIBAgIUCD73sHXu5IURolSTiJ127a/xUO4wDQYJKoZIhvcNAQELBQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGzAZBgNVBAMMEmZvby5iYXIuaG9vLmNhLmNvbTAeFw0yNTAzMDcxOTM0NDZaFw0yNjAzMDcxOTM0NDZaMFoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRswGQYDVQQDDBJmb28uYmFyLmhvby5jYS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC19PImArmxbwgqo2QG2si8BU6E69Bvyqxz8Je3swxBIGwZ9uIobMSBLeTCYyXuf+o90Zf0kMwzmrAKeLEeky5W/j07zGXAtgUBPA7L1Uk0TxOdJXBCUvEm5Oc4GxubfO7F+pdJKZ+XkRVqbnStGe5qX6KNs5rcJfFUhewbtM0snGEIf2yhaA0mNNuGtlIm4VB7jmiyHNU5YTowByVCVrV3/t2RI9+T4ya0AlkW93rU0M0qQauJ35LkJIXifbzrnLxmztEyb+mnVUB+GJgz01E4teWo/PJb1aNJ/ojf/UONsQ5IFRdza6RhaQB7C+Dxlnt/SJ3MMaxHgVycYuJeVtJQuncGRSuQ2YrmW9b36HVnxa0xBDeSluUjv48hMRlLNaXaH4yuK6oc8TNJie++/ir6Kb4H+0RjcKMGqxZYfotU1obxa+5N3wzGSjUDUrhofzlfvqbp+NCwdFH+qczM4IZPL8YMMh6goKr9BRN9/xRIieotyH6rfKNcnkUgDp750U0cZ7P2eRUpldyc9hZS5AlF4cKQXgLIrv1LrZHkiIietetInUEBAa/PF2YHRLXUyI1PCSBKBu7wdwAU15J9dVFC9jkmOLYhoRdPfrobpWhs5+FfPJumSoiusdGXd7x4l313xi2V02YXz5mRGbT2lCb6aJPweuziiEBZn+5KV++DkQIDAQABo1MwUTAdBgNVHQ4EFgQUL/YTEXKZRjbOv82CwGMuBeNwErIwHwYDVR0jBBgwFoAUL/YTEXKZRjbOv82CwGMuBeNwErIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAIYtNERVMj4dS/KgoXD3a6mF6KD+IuxDTlVbVTwDJMzdNW5UPB2qCiDKbirK8Eedh90r3qxKzBiQPE5AHDZrbWSBAxDhlpQqCXgx8Z+nCB38K9i8hbKM/ablm9DkeHiHohoP1kJEN2HtPz1K6OabbKbPtVGt8y+QTIlZDodPPzKPmQ6dUTm9tWlU1oIxl4pPVR6WXDr0qAmNRvdW7+8/Ai+gMDi6fKQJCe/r/meVrI9lSrnn8VQyO4xXvyolHEROTUiomlJ1QE9IWAM9LNFOuWFwQjayo2d6O+zHqjGUtbzBHFb8/BLJcQzogWYT0/+rEboJhR+/OGe0ntPPkudmLL0HTx0Q1aajnChMVlaOd2wucePZSa6GqGEVA+lCqwbteqwJCnL7deVHo+UlORFgQwYir2CQbyN9Yd4MtQT7VZuDiVC6tlGbj1ogrpOnW8n49jXrNPWuPz2hcPQb2gzYFGI9WRKIX4SjvvS3QcHUyigQkjAQ12Ldj5CXBYTjSmjcgsa2QfUY5qrXsHLz7e4uXD9XYnB/XEEfxQfQTFEy5CtzTrTxnT+cwRuBooIr6InjqdbdJ+UbkWcIY6w+c7ndFT849pUgtrojbVpm9ZzsmHhAahR/+iHYH+LGeHYIpq71o/YDgM4vV9z3sWic7u1YE9JULBdlaDw8Xik82zjvw4IA="
+                    ],
+                    "n": "tfTyJgK5sW8IKqNkBtrIvAVOhOvQb8qsc_CXt7MMQSBsGfbiKGzEgS3kwmMl7n_qPdGX9JDMM5qwCnixHpMuVv49O8xlwLYFATwOy9VJNE8TnSVwQlLxJuTnOBsbm3zuxfqXSSmfl5EVam50rRnual-ijbOa3CXxVIXsG7TNLJxhCH9soWgNJjTbhrZSJuFQe45oshzVOWE6MAclQla1d_7dkSPfk-MmtAJZFvd61NDNKkGrid-S5CSF4n2865y8Zs7RMm_pp1VAfhiYM9NROLXlqPzyW9WjSf6I3_1DjbEOSBUXc2ukYWkAewvg8ZZ7f0idzDGsR4FcnGLiXlbSULp3BkUrkNmK5lvW9-h1Z8WtMQQ3kpblI7-PITEZSzWl2h-MriuqHPEzSYnvvv4q-im-B_tEY3CjBqsWWH6LVNaG8WvuTd8Mxko1A1K4aH85X76m6fjQsHRR_qnMzOCGTy_GDDIeoKCq_QUTff8USInqLch-q3yjXJ5FIA6e-dFNHGez9nkVKZXcnPYWUuQJReHCkF4CyK79S62R5IiInrXrSJ1BAQGvzxdmB0S11MiNTwkgSgbu8HcAFNeSfXVRQvY5Jji2IaEXT366G6VobOfhXzybpkqIrrHRl3e8eJd9d8YtldNmF8-ZkRm09pQm-miT8Hrs4ohAWZ_uSlfvg5E",
+                    "e": "AQAB"
+                }
+            ]
+        }
+    }
+}
diff --git a/test/core/tsi/test_creds/spiffe_end2end/spiffe-openssl.cnf b/test/core/tsi/test_creds/spiffe_end2end/spiffe-openssl.cnf
new file mode 100644
index 0000000..6bd2128
--- /dev/null
+++ b/test/core/tsi/test_creds/spiffe_end2end/spiffe-openssl.cnf
@@ -0,0 +1,34 @@
+[spiffe_client]
+subjectAltName = @alt_names
+
+[spiffe_client_multi]
+subjectAltName = @alt_names_multi
+
+[spiffe_client_non_utf8]
+subjectAltName = @alt_names_non_utf8
+
+[spiffe_server_e2e]
+subjectAltName = @alt_names_server_e2e
+
+[spiffe_client_e2e]
+subjectAltName = @alt_names_client_e2e
+
+[alt_names]
+URI = spiffe://foo.bar.com/client/workload/1
+
+[alt_names_multi]
+URI.1 = spiffe://foo.bar.com/client/workload/1
+URI.2 = spiffe://foo.bar.com/client/workload/2
+
+[alt_names_non_utf8]
+URI.1 = spiffe://foo.bar.com/client/workload/1\\xc0
+
+[alt_names_server_e2e]
+DNS.1 = *.test.google.fr
+DNS.2 = waterzooi.test.google.be
+DNS.3 = *.test.youtube.com
+IP.1 = "192.168.1.3"
+URI = spiffe://example.com/workload/9eebccd2
+
+[alt_names_client_e2e]
+URI = spiffe://foo.bar.com/9eebccd2-12bf-40a6-b262-65fe0487d453
diff --git a/test/cpp/end2end/BUILD b/test/cpp/end2end/BUILD
index a2e8f14..59e7398 100644
--- a/test/cpp/end2end/BUILD
+++ b/test/cpp/end2end/BUILD
@@ -1240,3 +1240,39 @@
         "//test/cpp/util:test_util",
     ],
 )
+
+grpc_cc_test(
+    name = "spiffe_bundle_map_end2end_test",
+    srcs = ["spiffe_bundle_map_end2end_test.cc"],
+    data = [
+        "//test/core/tsi/test_creds/spiffe_end2end:ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffebundle.json",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate.cnf",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_gen.sh",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_and_intermediate_chain.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffebundle.json",
+    ],
+    external_deps = [
+        "absl/log:check",
+        "gtest",
+    ],
+    tags = ["crl_provider_test"],
+    deps = [
+        ":test_service_impl",
+        "//:gpr",
+        "//:grpc",
+        "//:grpc++",
+        "//src/proto/grpc/testing:echo_cc_grpc",
+        "//src/proto/grpc/testing:echo_messages_cc_proto",
+        "//test/core/test_util:grpc_test_util",
+        "//test/cpp/util:test_util",
+    ],
+)
diff --git a/test/cpp/end2end/spiffe_bundle_map_end2end_test.cc b/test/cpp/end2end/spiffe_bundle_map_end2end_test.cc
new file mode 100644
index 0000000..30e87db
--- /dev/null
+++ b/test/cpp/end2end/spiffe_bundle_map_end2end_test.cc
@@ -0,0 +1,473 @@
+//
+//
+// Copyright 2025 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//
+#include <grpc/grpc_security.h>
+#include <grpcpp/channel.h>
+#include <grpcpp/client_context.h>
+#include <grpcpp/create_channel.h>
+#include <grpcpp/security/credentials.h>
+#include <grpcpp/security/server_credentials.h>
+#include <grpcpp/security/tls_certificate_provider.h>
+#include <grpcpp/security/tls_certificate_verifier.h>
+#include <grpcpp/security/tls_credentials_options.h>
+#include <grpcpp/server.h>
+#include <grpcpp/server_builder.h>
+#include <grpcpp/support/channel_arguments.h>
+#include <grpcpp/support/status.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "absl/log/check.h"
+#include "absl/log/log.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/synchronization/notification.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/cpp/client/secure_credentials.h"
+#include "src/proto/grpc/testing/echo_messages.pb.h"
+#include "test/core/test_util/port.h"
+#include "test/core/test_util/test_config.h"
+#include "test/core/test_util/tls_utils.h"
+#include "test/cpp/end2end/test_service_impl.h"
+
+namespace grpc {
+namespace testing {
+namespace {
+
+constexpr absl::string_view kMessage = "Hello";
+constexpr absl::string_view kCaPemPath =
+    "test/core/tsi/test_creds/spiffe_end2end/ca.pem";
+constexpr absl::string_view kClientKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client.key";
+constexpr absl::string_view kClientCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem";
+constexpr absl::string_view kServerKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server.key";
+constexpr absl::string_view kServerCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem";
+constexpr absl::string_view kServerChainKeyPath =
+    "test/core/tsi/test_creds/spiffe_end2end/leaf_signed_by_intermediate.key";
+constexpr absl::string_view kServerChainCertPath =
+    "test/core/tsi/test_creds/spiffe_end2end/leaf_and_intermediate_chain.pem";
+constexpr absl::string_view kClientSpiffeBundleMapPath =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json";
+constexpr absl::string_view kServerSpiffeBundleMapPath =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json";
+
+std::string MakeConnectionFailureRegex(absl::string_view prefix) {
+  return absl::StrCat(
+      prefix,
+      "(UNKNOWN|UNAVAILABLE): "
+      // IP address
+      "(ipv6:%5B::1%5D|ipv4:127.0.0.1):[0-9]+: "
+      // Prefixes added for context
+      "(Failed to connect to remote host: )?"
+      "(Timeout occurred: )?"
+      // Parenthetical wrappers
+      "( ?\\(*("
+      "Secure read failed|"
+      "Handshake (read|write) failed|"
+      "Delayed close due to in-progress write|"
+      // Syscall
+      "((connect|sendmsg|recvmsg|getsockopt\\(SO\\_ERROR\\)): ?)?"
+      // strerror() output or other message
+      "(Connection refused"
+      "|Connection reset by peer"
+      "|Socket closed"
+      "|Broken pipe"
+      "|FD shutdown"
+      "|Endpoint closing)"
+      // errno value
+      "( \\([0-9]+\\))?"
+      // close paren from wrappers above
+      ")\\)*)+");
+}
+
+std::string MakeTlsHandshakeFailureRegex(absl::string_view prefix) {
+  return absl::StrCat(
+      prefix,
+      "(UNKNOWN|UNAVAILABLE): "
+      // IP address
+      "(ipv6:%5B::1%5D|ipv4:127.0.0.1):[0-9]+: "
+      // Prefixes added for context
+      "(Failed to connect to remote host: )?"
+      // Tls handshake failure
+      "Tls handshake failed \\(TSI_PROTOCOL_FAILURE\\): SSL_ERROR_SSL: "
+      "error:1000007d:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED"
+      // Detailed reason for certificate verify failure
+      "(: .*)?");
+}
+
+class SpiffeBundleMapTest : public ::testing::Test {
+ protected:
+  void RunServer(absl::Notification* notification, absl::string_view key_path,
+                 absl::string_view cert_path, absl::string_view root_path,
+                 absl::string_view spiffe_bundle_map_path) {
+    auto certificate_provider =
+        std::make_shared<experimental::FileWatcherCertificateProvider>(
+            std::string(key_path), std::string(cert_path),
+            std::string(root_path), std::string(spiffe_bundle_map_path), 1);
+    grpc::experimental::TlsServerCredentialsOptions options(
+        certificate_provider);
+    options.watch_root_certs();
+    options.set_root_cert_name("root");
+    options.watch_identity_key_cert_pairs();
+    options.set_identity_cert_name("identity");
+    options.set_cert_request_type(
+        GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY);
+    auto server_credentials = grpc::experimental::TlsServerCredentials(options);
+    CHECK_NE(server_credentials.get(), nullptr);
+
+    grpc::ServerBuilder builder;
+
+    builder.AddListeningPort(server_addr_, server_credentials);
+    builder.RegisterService("foo.test.google.fr", &service_);
+    server_ = builder.BuildAndStart();
+    notification->Notify();
+    server_->Wait();
+  }
+
+  void TearDown() override {
+    if (server_ != nullptr) {
+      server_->Shutdown();
+      server_thread_->join();
+      delete server_thread_;
+    }
+  }
+
+  TestServiceImpl service_;
+  std::unique_ptr<Server> server_ = nullptr;
+  std::thread* server_thread_ = nullptr;
+  std::string server_addr_;
+};
+
+void DoRpc(const std::string& server_addr,
+           const experimental::TlsChannelCredentialsOptions& tls_options,
+           bool expect_success, absl::string_view failure_message_regex = "",
+           StatusCode failure_code = StatusCode::OK) {
+  ChannelArguments channel_args;
+  channel_args.SetSslTargetNameOverride("foo.test.google.fr");
+  std::shared_ptr<Channel> channel = grpc::CreateCustomChannel(
+      server_addr, grpc::experimental::TlsCredentials(tls_options),
+      channel_args);
+
+  auto stub = grpc::testing::EchoTestService::NewStub(channel);
+  grpc::testing::EchoRequest request;
+  grpc::testing::EchoResponse response;
+  request.set_message(kMessage);
+  ClientContext context;
+  context.set_deadline(grpc_timeout_seconds_to_deadline(/*time_s=*/15));
+  grpc::Status result = stub->Echo(&context, request, &response);
+  if (expect_success) {
+    EXPECT_TRUE(result.ok()) << result.error_message().c_str() << ", "
+                             << result.error_details().c_str();
+    EXPECT_EQ(response.message(), kMessage);
+  } else {
+    EXPECT_FALSE(result.ok());
+    EXPECT_EQ(result.error_code(), failure_code);
+#if GTEST_USES_POSIX_RE
+    EXPECT_THAT(result.error_message(),
+                ::testing::MatchesRegex(failure_message_regex));
+#endif
+  }
+}
+
+TEST_F(SpiffeBundleMapTest, ServerSideSpiffeTLS) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerKeyPath, kServerCertPath, "",
+              kServerSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  std::string root_cert =
+      grpc_core::testing::GetFileContents(std::string(kCaPemPath));
+  std::string client_key =
+      grpc_core::testing::GetFileContents(std::string(kClientKeyPath));
+  std::string client_cert =
+      grpc_core::testing::GetFileContents(std::string(kClientCertPath));
+  experimental::IdentityKeyCertPair key_cert_pair;
+  key_cert_pair.private_key = client_key;
+  key_cert_pair.certificate_chain = client_cert;
+  std::vector<experimental::IdentityKeyCertPair> identity_key_cert_pairs;
+  identity_key_cert_pairs.emplace_back(key_cert_pair);
+  auto certificate_provider =
+      std::make_shared<experimental::StaticDataCertificateProvider>(
+          root_cert, identity_key_cert_pairs);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+
+  DoRpc(server_addr_, options, true);
+}
+
+TEST_F(SpiffeBundleMapTest, ClientSideSpiffeTLS) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerKeyPath, kServerCertPath, kCaPemPath, "");
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kClientSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+
+  DoRpc(server_addr_, options, true);
+}
+
+TEST_F(SpiffeBundleMapTest, SpiffeMTLS) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerKeyPath, kServerCertPath, "",
+              kServerSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kClientSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+
+  DoRpc(server_addr_, options, true);
+}
+
+TEST_F(SpiffeBundleMapTest, SpiffeWithCertChain) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerChainKeyPath, kServerChainCertPath, "",
+              kServerSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kClientSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+
+  DoRpc(server_addr_, options, true);
+}
+
+TEST_F(SpiffeBundleMapTest, ServerSpiffeReload) {
+  auto server_bundle_map = grpc_core::testing::GetFileContents(
+      std::string(kServerSpiffeBundleMapPath));
+  auto client_bundle_map = grpc_core::testing::GetFileContents(
+      std::string(kClientSpiffeBundleMapPath));
+  grpc_core::testing::TmpFile tmp_bundle_map(server_bundle_map);
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerChainKeyPath, kServerChainCertPath, "",
+              tmp_bundle_map.name());
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kClientSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+  DoRpc(server_addr_, options, true);
+
+  // Update the spiffe bundle map to something that will fail
+  tmp_bundle_map.RewriteFile(client_bundle_map);
+  // Wait 2 seconds to ensure a refresh happens
+  gpr_sleep_until(gpr_time_add(gpr_now(GPR_CLOCK_MONOTONIC),
+                               gpr_time_from_seconds(2, GPR_TIMESPAN)));
+  constexpr absl::string_view expected_message_start =
+      "failed to connect to all addresses; last error: ";
+  const StatusCode expected_status = StatusCode::UNAVAILABLE;
+  DoRpc(server_addr_, options, false,
+        MakeConnectionFailureRegex(expected_message_start), expected_status);
+}
+
+TEST_F(SpiffeBundleMapTest, ClientSpiffeReload) {
+  auto server_bundle_map = grpc_core::testing::GetFileContents(
+      std::string(kServerSpiffeBundleMapPath));
+  auto client_bundle_map = grpc_core::testing::GetFileContents(
+      std::string(kClientSpiffeBundleMapPath));
+  grpc_core::testing::TmpFile tmp_bundle_map(client_bundle_map);
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerChainKeyPath, kServerChainCertPath, "",
+              kServerSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", tmp_bundle_map.name(), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+  DoRpc(server_addr_, options, true);
+
+  // Update the spiffe bundle map to something that will fail
+  tmp_bundle_map.RewriteFile(server_bundle_map);
+  // Wait 2 seconds to ensure a refresh happens
+  gpr_sleep_until(gpr_time_add(gpr_now(GPR_CLOCK_MONOTONIC),
+                               gpr_time_from_seconds(2, GPR_TIMESPAN)));
+  constexpr absl::string_view expected_message_start =
+      "failed to connect to all addresses; last error: ";
+  const StatusCode expected_status = StatusCode::UNAVAILABLE;
+  DoRpc(server_addr_, options, false,
+        MakeTlsHandshakeFailureRegex(expected_message_start), expected_status);
+}
+
+TEST_F(SpiffeBundleMapTest, ServerSideSpiffeVerificationFailure) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  // Use the client-side spiffe bundle map on the server side to force a failure
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerKeyPath, kServerCertPath, "",
+              kClientSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kClientSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+
+  constexpr absl::string_view expected_message_start =
+      "failed to connect to all addresses; last error: ";
+  const StatusCode expected_status = StatusCode::UNAVAILABLE;
+  DoRpc(server_addr_, options, false,
+        MakeConnectionFailureRegex(expected_message_start), expected_status);
+}
+
+TEST_F(SpiffeBundleMapTest, ClientSideSpiffeVerificationFailure) {
+  server_addr_ = absl::StrCat("localhost:",
+                              std::to_string(grpc_pick_unused_port_or_die()));
+  absl::Notification notification;
+  server_thread_ = new std::thread([&]() {
+    RunServer(&notification, kServerKeyPath, kServerCertPath, "",
+              kServerSpiffeBundleMapPath);
+  });
+  notification.WaitForNotification();
+
+  // Use the server-side spiffe bundle map on the client side to force a failure
+  auto certificate_provider =
+      std::make_shared<experimental::FileWatcherCertificateProvider>(
+          std::string(kClientKeyPath), std::string(kClientCertPath),
+          /*root_cert_path=*/"", std::string(kServerSpiffeBundleMapPath), 1);
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(certificate_provider);
+  options.watch_root_certs();
+  options.set_root_cert_name("root");
+  options.watch_identity_key_cert_pairs();
+  options.set_identity_cert_name("identity");
+
+  options.set_check_call_host(false);
+  auto verifier = std::make_shared<experimental::NoOpCertificateVerifier>();
+  options.set_certificate_verifier(verifier);
+
+  constexpr absl::string_view expected_message_start =
+      "failed to connect to all addresses; last error: ";
+  const StatusCode expected_status = StatusCode::UNAVAILABLE;
+  DoRpc(server_addr_, options, false,
+        MakeTlsHandshakeFailureRegex(expected_message_start), expected_status);
+}
+
+}  // namespace
+}  // namespace testing
+}  // namespace grpc
+
+int main(int argc, char** argv) {
+  grpc::testing::TestEnvironment env(&argc, argv);
+  ::testing::InitGoogleTest(&argc, argv);
+  int ret = RUN_ALL_TESTS();
+  return ret;
+}
diff --git a/test/cpp/end2end/xds/BUILD b/test/cpp/end2end/xds/BUILD
index b09ebe7..2f0ff8b 100644
--- a/test/cpp/end2end/xds/BUILD
+++ b/test/cpp/end2end/xds/BUILD
@@ -154,6 +154,72 @@
 )
 
 grpc_cc_test(
+    name = "xds_security_spiffe_end2end_test",
+    size = "large",
+    srcs = ["xds_security_spiffe_end2end_test.cc"],
+    data = [
+        "//src/core/tsi/test_creds:badclient.key",
+        "//src/core/tsi/test_creds:badclient.pem",
+        "//src/core/tsi/test_creds:ca.pem",
+        "//src/core/tsi/test_creds:client.key",
+        "//src/core/tsi/test_creds:client.pem",
+        "//src/core/tsi/test_creds:server1.key",
+        "//src/core/tsi/test_creds:server1.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:client_spiffebundle.json",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate.cnf",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_ca.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:intermediate_gen.sh",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_and_intermediate_chain.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:leaf_signed_by_intermediate.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server.key",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffe.pem",
+        "//test/core/tsi/test_creds/spiffe_end2end:server_spiffebundle.json",
+    ],
+    external_deps = [
+        "absl/log:check",
+        "gtest",
+    ],
+    # flaky = True,  # TODO(b/144705388)
+    linkstatic = True,  # Fixes dyld error on MacOS
+    # shard_count = 25,
+    tags = [
+        "no_test_ios",
+        "no_windows",
+        "xds_end2end_test",
+    ],  # TODO(jtattermusch): fix test on windows
+    deps = [
+        ":xds_end2end_test_lib",
+        "//:gpr",
+        "//:grpc",
+        "//:grpc++",
+        "//src/core:grpc_audit_logging",
+        "//src/proto/grpc/testing:echo_cc_grpc",
+        "//src/proto/grpc/testing:echo_messages_cc_proto",
+        "//test/core/test_util:audit_logging_utils",
+        "//test/core/test_util:grpc_test_util",
+        "//test/core/test_util:scoped_env_var",
+        "//test/cpp/util:test_config",
+        "//test/cpp/util:test_util",
+        "//test/cpp/util:tls_test_utils",
+        "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto",
+        "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto",
+        "@envoy_api//envoy/config/listener/v3:pkg_cc_proto",
+        "@envoy_api//envoy/config/route/v3:pkg_cc_proto",
+        "@envoy_api//envoy/extensions/clusters/aggregate/v3:pkg_cc_proto",
+        "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg_cc_proto",
+        "@envoy_api//envoy/extensions/filters/http/router/v3:pkg_cc_proto",
+        "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto",
+        "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto",
+        "@envoy_api//envoy/type/v3:pkg_cc_proto",
+    ],
+)
+
+grpc_cc_test(
     name = "xds_cluster_end2end_test",
     size = "large",
     srcs = ["xds_cluster_end2end_test.cc"],
diff --git a/test/cpp/end2end/xds/xds_end2end_test_lib.cc b/test/cpp/end2end/xds/xds_end2end_test_lib.cc
index dcade49..8a01681 100644
--- a/test/cpp/end2end/xds/xds_end2end_test_lib.cc
+++ b/test/cpp/end2end/xds/xds_end2end_test_lib.cc
@@ -377,6 +377,13 @@
 const char XdsEnd2endTest::kServerKeyPath[] =
     "src/core/tsi/test_creds/server1.key";
 
+const char XdsEnd2endTest::kSpiffeCaCertPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/ca.pem";
+const char XdsEnd2endTest::kSpiffeServerCertPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffe.pem";
+const char XdsEnd2endTest::kSpiffeServerKeyPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/server.key";
+
 const char XdsEnd2endTest::kRequestMessage[] = "Live long and prosper.";
 
 XdsEnd2endTest::XdsEnd2endTest(
@@ -918,12 +925,26 @@
   return {{std::move(private_key), std::move(identity_cert)}};
 }
 
+std::vector<experimental::IdentityKeyCertPair>
+XdsEnd2endTest::MakeIdentityKeyCertPairForSpiffeMtlsCreds() {
+  std::string identity_cert =
+      grpc_core::testing::GetFileContents(kSpiffeServerCertPath);
+  std::string private_key =
+      grpc_core::testing::GetFileContents(kSpiffeServerKeyPath);
+  return {{std::move(private_key), std::move(identity_cert)}};
+}
+
 std::shared_ptr<ChannelCredentials>
 XdsEnd2endTest::CreateXdsChannelCredentials() {
   return XdsCredentials(CreateTlsChannelCredentials());
 }
 
 std::shared_ptr<ChannelCredentials>
+XdsEnd2endTest::CreateSpiffeXdsChannelCredentials() {
+  return XdsCredentials(CreateSpiffeTlsChannelCredentials());
+}
+
+std::shared_ptr<ChannelCredentials>
 XdsEnd2endTest::CreateTlsChannelCredentials() {
   auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(
       grpc_core::testing::GetFileContents(kCaCertPath),
@@ -940,6 +961,23 @@
   return grpc::experimental::TlsCredentials(options);
 }
 
+std::shared_ptr<ChannelCredentials>
+XdsEnd2endTest::CreateSpiffeTlsChannelCredentials() {
+  auto certificate_provider = std::make_shared<StaticDataCertificateProvider>(
+      grpc_core::testing::GetFileContents(kSpiffeCaCertPath),
+      MakeIdentityKeyCertPairForSpiffeMtlsCreds());
+  grpc::experimental::TlsChannelCredentialsOptions options;
+  options.set_certificate_provider(std::move(certificate_provider));
+  options.watch_root_certs();
+  options.watch_identity_key_cert_pairs();
+  auto verifier =
+      ExternalCertificateVerifier::Create<SyncCertificateVerifier>(true);
+  options.set_certificate_verifier(std::move(verifier));
+  options.set_verify_server_certs(true);
+  options.set_check_call_host(false);
+  return grpc::experimental::TlsCredentials(options);
+}
+
 std::shared_ptr<ServerCredentials>
 XdsEnd2endTest::CreateFakeServerCredentials() {
   return std::make_shared<SecureServerCredentials>(
@@ -971,5 +1009,21 @@
   return grpc::experimental::TlsServerCredentials(options);
 }
 
+std::shared_ptr<ServerCredentials>
+XdsEnd2endTest::CreateMtlsSpiffeServerCredentials() {
+  std::string root_cert =
+      grpc_core::testing::GetFileContents(kSpiffeCaCertPath);
+  auto certificate_provider =
+      std::make_shared<grpc::experimental::StaticDataCertificateProvider>(
+          std::move(root_cert), MakeIdentityKeyCertPairForSpiffeMtlsCreds());
+  grpc::experimental::TlsServerCredentialsOptions options(
+      std::move(certificate_provider));
+  options.watch_root_certs();
+  options.watch_identity_key_cert_pairs();
+  options.set_cert_request_type(
+      GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY);
+  return grpc::experimental::TlsServerCredentials(options);
+}
+
 }  // namespace testing
 }  // namespace grpc
diff --git a/test/cpp/end2end/xds/xds_end2end_test_lib.h b/test/cpp/end2end/xds/xds_end2end_test_lib.h
index b345178..7763a33 100644
--- a/test/cpp/end2end/xds/xds_end2end_test_lib.h
+++ b/test/cpp/end2end/xds/xds_end2end_test_lib.h
@@ -203,6 +203,9 @@
   static const char kCaCertPath[];
   static const char kServerCertPath[];
   static const char kServerKeyPath[];
+  static const char kSpiffeCaCertPath[];
+  static const char kSpiffeServerCertPath[];
+  static const char kSpiffeServerKeyPath[];
 
   // Message used in EchoRequest to the backend.
   static const char kRequestMessage[];
@@ -1029,14 +1032,24 @@
   static std::vector<experimental::IdentityKeyCertPair>
   MakeIdentityKeyCertPairForTlsCreds();
 
+  // Internal helper function for creating TLS and mTLS creds.
+  // Not intended to be used by tests.
+  static std::vector<experimental::IdentityKeyCertPair>
+  MakeIdentityKeyCertPairForSpiffeMtlsCreds();
+
   // Returns XdsCredentials with mTLS fallback creds.
   static std::shared_ptr<ChannelCredentials> CreateXdsChannelCredentials();
   static std::shared_ptr<ChannelCredentials> CreateTlsChannelCredentials();
+  static std::shared_ptr<ChannelCredentials>
+  CreateSpiffeXdsChannelCredentials();
+  static std::shared_ptr<ChannelCredentials>
+  CreateSpiffeTlsChannelCredentials();
 
   // Creates various types of server credentials.
   static std::shared_ptr<ServerCredentials> CreateFakeServerCredentials();
   static std::shared_ptr<ServerCredentials> CreateMtlsServerCredentials();
   static std::shared_ptr<ServerCredentials> CreateTlsServerCredentials();
+  static std::shared_ptr<ServerCredentials> CreateMtlsSpiffeServerCredentials();
 
   // event_engine_scope_ always has to be at the top of the list to make sure
   // that all other objects are destroyed before this and other event engine
diff --git a/test/cpp/end2end/xds/xds_security_spiffe_end2end_test.cc b/test/cpp/end2end/xds/xds_security_spiffe_end2end_test.cc
new file mode 100644
index 0000000..039ccf1
--- /dev/null
+++ b/test/cpp/end2end/xds/xds_security_spiffe_end2end_test.cc
@@ -0,0 +1,477 @@
+//
+// Copyright 2025 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <grpc/grpc.h>
+#include <grpc/grpc_security.h>
+#include <grpc/support/alloc.h>
+#include <grpc/support/time.h>
+#include <grpcpp/channel.h>
+#include <grpcpp/client_context.h>
+#include <grpcpp/create_channel.h>
+#include <grpcpp/security/audit_logging.h>
+#include <grpcpp/security/tls_certificate_provider.h>
+#include <grpcpp/server.h>
+#include <grpcpp/server_builder.h>
+#include <grpcpp/xds_server_builder.h>
+
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/functional/function_ref.h"
+#include "absl/log/check.h"
+#include "absl/log/log.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "absl/time/time.h"
+#include "envoy/config/cluster/v3/cluster.pb.h"
+#include "envoy/config/endpoint/v3/endpoint.pb.h"
+#include "envoy/config/listener/v3/listener.pb.h"
+#include "envoy/config/route/v3/route.pb.h"
+#include "envoy/extensions/clusters/aggregate/v3/cluster.pb.h"
+#include "envoy/extensions/filters/http/rbac/v3/rbac.pb.h"
+#include "envoy/extensions/filters/http/router/v3/router.pb.h"
+#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"
+#include "envoy/extensions/transport_sockets/tls/v3/tls.pb.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/core/config/config_vars.h"
+#include "src/core/config/core_configuration.h"
+#include "src/core/credentials/transport/fake/fake_credentials.h"
+#include "src/core/credentials/transport/tls/certificate_provider_registry.h"
+#include "src/core/credentials/transport/tls/grpc_tls_certificate_provider.h"
+#include "src/core/ext/filters/http/client/http_client_filter.h"
+#include "src/core/lib/security/authorization/audit_logging.h"
+#include "src/core/util/env.h"
+#include "src/core/util/ref_counted_ptr.h"
+#include "src/core/util/string.h"
+#include "src/core/util/sync.h"
+#include "src/cpp/client/secure_credentials.h"
+#include "src/cpp/server/secure_server_credentials.h"
+#include "src/proto/grpc/testing/echo.grpc.pb.h"
+#include "src/proto/grpc/testing/echo_messages.pb.h"
+#include "test/core/test_util/audit_logging_utils.h"
+#include "test/core/test_util/port.h"
+#include "test/core/test_util/resolve_localhost_ip46.h"
+#include "test/core/test_util/scoped_env_var.h"
+#include "test/core/test_util/test_config.h"
+#include "test/core/test_util/tls_utils.h"
+#include "test/cpp/end2end/xds/xds_end2end_test_lib.h"
+#include "test/cpp/util/test_config.h"
+#include "test/cpp/util/tls_test_utils.h"
+#include "xds/type/v3/typed_struct.pb.h"
+
+namespace grpc {
+namespace testing {
+namespace {
+
+using ::envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext;
+using ::envoy::type::matcher::v3::StringMatcher;
+
+constexpr char kClientKeyPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/client.key";
+constexpr char kClientCertPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem";
+constexpr char kClientSpiffeBundleMapPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json";
+constexpr char kServerSpiffeBundleMapPath[] =
+    "test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json";
+constexpr char kBadClientCertPath[] = "src/core/tsi/test_creds/badclient.pem";
+constexpr char kBadClientKeyPath[] = "src/core/tsi/test_creds/badclient.key";
+
+// Based on StaticDataCertificateProvider, but provides alternate certificates
+// if the certificate name is not empty.
+class FakeCertificateProvider final : public grpc_tls_certificate_provider {
+ public:
+  struct CertData {
+    std::string root_certificate;
+    grpc_core::PemKeyCertPairList identity_key_cert_pairs;
+    grpc_core::SpiffeBundleMap spiffe_bundle_map;
+  };
+
+  using CertDataMap = std::map<std::string /*cert_name */, CertData>;
+  class CertDataMapWrapper {
+   public:
+    CertDataMap Get() {
+      grpc_core::MutexLock lock(&mu_);
+      return cert_data_map_;
+    }
+
+    void Set(CertDataMap data) {
+      grpc_core::MutexLock lock(&mu_);
+      cert_data_map_ = std::move(data);
+    }
+
+   private:
+    grpc_core::Mutex mu_;
+    CertDataMap cert_data_map_ ABSL_GUARDED_BY(mu_);
+  };
+
+  explicit FakeCertificateProvider(CertDataMap cert_data_map)
+      : distributor_(
+            grpc_core::MakeRefCounted<grpc_tls_certificate_distributor>()),
+        cert_data_map_(std::move(cert_data_map)) {
+    distributor_->SetWatchStatusCallback([this](std::string cert_name,
+                                                bool root_being_watched,
+                                                bool identity_being_watched) {
+      if (!root_being_watched && !identity_being_watched) return;
+      auto it = cert_data_map_.find(cert_name);
+      if (it == cert_data_map_.end()) {
+        grpc_error_handle error = GRPC_ERROR_CREATE(absl::StrCat(
+            "No certificates available for cert_name \"", cert_name, "\""));
+        distributor_->SetErrorForCert(cert_name, error, error);
+      } else {
+        std::shared_ptr<RootCertInfo> root_cert_info;
+        std::optional<grpc_core::PemKeyCertPairList> pem_key_cert_pairs;
+        if (root_being_watched) {
+          if (it->second.spiffe_bundle_map.size() != 0) {
+            root_cert_info =
+                std::make_shared<RootCertInfo>(it->second.spiffe_bundle_map);
+          } else {
+            root_cert_info =
+                std::make_shared<RootCertInfo>(it->second.root_certificate);
+          }
+        }
+        if (identity_being_watched) {
+          pem_key_cert_pairs = it->second.identity_key_cert_pairs;
+        }
+        distributor_->SetKeyMaterials(cert_name, std::move(root_cert_info),
+                                      std::move(pem_key_cert_pairs));
+      }
+    });
+  }
+
+  ~FakeCertificateProvider() override {
+    distributor_->SetWatchStatusCallback(nullptr);
+  }
+
+  grpc_core::RefCountedPtr<grpc_tls_certificate_distributor> distributor()
+      const override {
+    return distributor_;
+  }
+
+  grpc_core::UniqueTypeName type() const override {
+    static grpc_core::UniqueTypeName::Factory kFactory("fake");
+    return kFactory.Create();
+  }
+
+ private:
+  int CompareImpl(const grpc_tls_certificate_provider* other) const override {
+    // TODO(yashykt): Maybe do something better here.
+    return grpc_core::QsortCompare(
+        static_cast<const grpc_tls_certificate_provider*>(this), other);
+  }
+
+  grpc_core::RefCountedPtr<grpc_tls_certificate_distributor> distributor_;
+  CertDataMap cert_data_map_;
+};
+
+class FakeCertificateProviderFactory
+    : public grpc_core::CertificateProviderFactory {
+ public:
+  class Config : public grpc_core::CertificateProviderFactory::Config {
+   public:
+    explicit Config(absl::string_view name) : name_(name) {}
+
+    absl::string_view name() const override { return name_; }
+
+    std::string ToString() const override { return "{}"; }
+
+   private:
+    absl::string_view name_;
+  };
+
+  FakeCertificateProviderFactory(
+      absl::string_view name,
+      FakeCertificateProvider::CertDataMapWrapper* cert_data_map)
+      : name_(name), cert_data_map_(cert_data_map) {
+    CHECK_NE(cert_data_map, nullptr);
+  }
+
+  absl::string_view name() const override { return name_; }
+
+  grpc_core::RefCountedPtr<grpc_core::CertificateProviderFactory::Config>
+  CreateCertificateProviderConfig(
+      const grpc_core::Json& /*config_json*/,
+      const grpc_core::JsonArgs& /*args*/,
+      grpc_core::ValidationErrors* /*errors*/) override {
+    return grpc_core::MakeRefCounted<Config>(name_);
+  }
+
+  grpc_core::RefCountedPtr<grpc_tls_certificate_provider>
+  CreateCertificateProvider(
+      grpc_core::RefCountedPtr<grpc_core::CertificateProviderFactory::Config>
+      /*config*/) override {
+    CHECK_NE(cert_data_map_, nullptr);
+    return grpc_core::MakeRefCounted<FakeCertificateProvider>(
+        cert_data_map_->Get());
+  }
+
+ private:
+  absl::string_view name_;
+  FakeCertificateProvider::CertDataMapWrapper* cert_data_map_;
+};
+
+// Global variables for each provider.
+FakeCertificateProvider::CertDataMapWrapper* g_fake1_cert_data_map = nullptr;
+FakeCertificateProvider::CertDataMapWrapper* g_fake2_cert_data_map = nullptr;
+
+//
+// Client-side mTLS tests
+//
+
+class XdsSecurityTest : public XdsEnd2endTest {
+ protected:
+  void SetUp() override {
+    XdsBootstrapBuilder builder = MakeBootstrapBuilder();
+    builder.AddCertificateProviderPlugin("fake_plugin1", "fake1");
+    builder.AddCertificateProviderPlugin("fake_plugin2", "fake2");
+    std::vector<std::string> fields;
+    fields.push_back(absl::StrFormat("        \"certificate_file\": \"%s\"",
+                                     kClientCertPath));
+    fields.push_back(absl::StrFormat("        \"private_key_file\": \"%s\"",
+                                     kClientKeyPath));
+    fields.push_back(
+        absl::StrFormat("        \"spiffe_bundle_map_file\": \"%s\"",
+                        kClientSpiffeBundleMapPath));
+    builder.AddCertificateProviderPlugin("file_plugin", "file_watcher",
+                                         absl::StrJoin(fields, ",\n"));
+
+    InitClient(builder, /*lb_expected_authority=*/"",
+               /*xds_resource_does_not_exist_timeout_ms=*/0,
+               /*balancer_authority_override=*/"", /*args=*/nullptr,
+               CreateSpiffeXdsChannelCredentials());
+    CreateAndStartBackends(2, /*xds_enabled=*/false,
+                           CreateMtlsSpiffeServerCredentials());
+    root_cert_ = grpc_core::testing::GetFileContents(kSpiffeCaCertPath);
+    bad_root_cert_ = grpc_core::testing::GetFileContents(kBadClientCertPath);
+    identity_pair_ = ReadTlsIdentityPair(kClientKeyPath, kClientCertPath);
+
+    // TODO(yashykt): Use different client certs here instead of reusing
+    // server certs after https://github.com/grpc/grpc/pull/24876 is merged
+    fallback_identity_pair_ =
+        ReadTlsIdentityPair(kServerKeyPath, kServerCertPath);
+    bad_identity_pair_ =
+        ReadTlsIdentityPair(kBadClientKeyPath, kBadClientCertPath);
+    server_san_exact_.set_exact("*.test.google.fr");
+    server_san_prefix_.set_prefix("waterzooi.test.google");
+    server_san_suffix_.set_suffix("google.fr");
+    server_san_contains_.set_contains("google");
+    server_san_regex_.mutable_safe_regex()->mutable_google_re2();
+    server_san_regex_.mutable_safe_regex()->set_regex(
+        "(foo|waterzooi).test.google.(fr|be)");
+    bad_san_1_.set_exact("192.168.1.4");
+    bad_san_2_.set_exact("foo.test.google.in");
+    authenticated_identity_ = {
+        "spiffe://foo.bar.com/9eebccd2-12bf-40a6-b262-65fe0487d453"};
+    fallback_authenticated_identity_ = {"*.test.google.fr",
+                                        "waterzooi.test.google.be",
+                                        "*.test.youtube.com", "192.168.1.3"};
+    EdsResourceArgs args({
+        {"locality0", CreateEndpointsForBackends(0, 1)},
+    });
+    balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
+  }
+
+  void MaybeSetUpstreamTlsContextOnCluster(
+      absl::string_view root_instance_name,
+      absl::string_view root_certificate_name,
+      absl::string_view identity_instance_name,
+      absl::string_view identity_certificate_name,
+      const std::vector<StringMatcher>& san_matchers, Cluster* cluster) {
+    if (!identity_instance_name.empty() || !root_instance_name.empty()) {
+      auto* transport_socket = cluster->mutable_transport_socket();
+      transport_socket->set_name("envoy.transport_sockets.tls");
+      UpstreamTlsContext upstream_tls_context;
+      if (!identity_instance_name.empty()) {
+        upstream_tls_context.mutable_common_tls_context()
+            ->mutable_tls_certificate_provider_instance()
+            ->set_instance_name(std::string(identity_instance_name));
+        upstream_tls_context.mutable_common_tls_context()
+            ->mutable_tls_certificate_provider_instance()
+            ->set_certificate_name(std::string(identity_certificate_name));
+      }
+      if (!root_instance_name.empty()) {
+        upstream_tls_context.mutable_common_tls_context()
+            ->mutable_validation_context()
+            ->mutable_ca_certificate_provider_instance()
+            ->set_instance_name(std::string(root_instance_name));
+        upstream_tls_context.mutable_common_tls_context()
+            ->mutable_validation_context()
+            ->mutable_ca_certificate_provider_instance()
+            ->set_certificate_name(std::string(root_certificate_name));
+      }
+      if (!san_matchers.empty()) {
+        auto* validation_context =
+            upstream_tls_context.mutable_common_tls_context()
+                ->mutable_validation_context();
+        for (const auto& san_matcher : san_matchers) {
+          *validation_context->add_match_subject_alt_names() = san_matcher;
+        }
+      }
+      transport_socket->mutable_typed_config()->PackFrom(upstream_tls_context);
+    }
+  }
+
+  // Sends CDS updates with the new security configuration and verifies that
+  // after propagation, this new configuration is used for connections. If \a
+  // identity_instance_name and \a root_instance_name are both empty,
+  // connections are expected to use fallback credentials.
+  // TODO(yashykt): The core of this logic should be inlined into the
+  // individual tests instead of being in this helper function.
+  void UpdateAndVerifyXdsSecurityConfiguration(
+      absl::string_view root_instance_name,
+      absl::string_view root_certificate_name,
+      absl::string_view identity_instance_name,
+      absl::string_view identity_certificate_name,
+      const std::vector<StringMatcher>& san_matchers,
+      const std::vector<std::string>& expected_authenticated_identity,
+      bool test_expects_failure = false) {
+    // Change the backend and use a unique service name to use so that we know
+    // that the CDS update was applied.
+    std::string service_name = absl::StrCat(
+        "eds_service_name",
+        absl::FormatTime("%H%M%E3S", absl::Now(), absl::LocalTimeZone()));
+    backend_index_ = (backend_index_ + 1) % 2;
+    EdsResourceArgs args({
+        {"locality0",
+         CreateEndpointsForBackends(backend_index_, backend_index_ + 1)},
+    });
+    balancer_->ads_service()->SetEdsResource(
+        BuildEdsResource(args, service_name.c_str()));
+    auto cluster = default_cluster_;
+    cluster.mutable_eds_cluster_config()->set_service_name(service_name);
+    MaybeSetUpstreamTlsContextOnCluster(
+        root_instance_name, root_certificate_name, identity_instance_name,
+        identity_certificate_name, san_matchers, &cluster);
+    balancer_->ads_service()->SetCdsResource(cluster);
+    // The updates might take time to have an effect, so use a retry loop.
+    if (test_expects_failure) {
+      SendRpcsUntilFailure(DEBUG_LOCATION, StatusCode::UNAVAILABLE,
+                           // TODO(yashkt): Change individual test cases to
+                           // expect the exact error message here.
+                           ".*", /*timeout_ms=*/20 * 1000,
+                           RpcOptions().set_timeout_ms(5000));
+    } else {
+      backends_[backend_index_]->backend_service()->ResetCounters();
+      SendRpcsUntil(
+          DEBUG_LOCATION,
+          [&](const RpcResult& result) {
+            // Make sure that we are hitting the correct backend.
+            // TODO(yashykt): Even if we haven't moved to the correct backend
+            // and are still using the previous update, we should still check
+            // for the status and make sure that it fits our expectations.
+            if (backends_[backend_index_]->backend_service()->request_count() ==
+                0) {
+              return true;
+            }
+            EXPECT_TRUE(result.status.ok())
+                << "code=" << result.status.error_code()
+                << " message=" << result.status.error_message();
+            // Check that the identity is as expected.
+            EXPECT_EQ(backends_[backend_index_]
+                          ->backend_service()
+                          ->last_peer_identity(),
+                      expected_authenticated_identity);
+            return false;
+          },
+          /* timeout_ms= */ 20 * 1000, RpcOptions().set_timeout_ms(5000));
+    }
+  }
+
+  std::string root_cert_;
+  std::string bad_root_cert_;
+  grpc_core::PemKeyCertPairList identity_pair_;
+  grpc_core::PemKeyCertPairList fallback_identity_pair_;
+  grpc_core::PemKeyCertPairList bad_identity_pair_;
+  grpc_core::SpiffeBundleMap spiffe_bundle_map_;
+  StringMatcher server_san_exact_;
+  StringMatcher server_san_prefix_;
+  StringMatcher server_san_suffix_;
+  StringMatcher server_san_contains_;
+  StringMatcher server_san_regex_;
+  StringMatcher bad_san_1_;
+  StringMatcher bad_san_2_;
+  std::vector<std::string> authenticated_identity_;
+  std::vector<std::string> fallback_authenticated_identity_;
+  int backend_index_ = 0;
+};
+
+INSTANTIATE_TEST_SUITE_P(XdsTest, XdsSecurityTest,
+                         ::testing::Values(XdsTestType()), &XdsTestType::Name);
+
+TEST_P(XdsSecurityTest, TestMtlsConfigurationWithRootPluginUpdateSpiffe) {
+  auto map = grpc_core::SpiffeBundleMap::FromFile(kClientSpiffeBundleMapPath);
+  ASSERT_TRUE(map.ok());
+  auto bad_map =
+      grpc_core::SpiffeBundleMap::FromFile(kServerSpiffeBundleMapPath);
+  ASSERT_TRUE(bad_map.ok());
+  g_fake1_cert_data_map->Set({{"", {"", identity_pair_, *map}}});
+  g_fake2_cert_data_map->Set({{"", {"", bad_identity_pair_, *bad_map}}});
+  UpdateAndVerifyXdsSecurityConfiguration("fake_plugin1", "", "fake_plugin1",
+                                          "", {server_san_exact_},
+                                          authenticated_identity_);
+  UpdateAndVerifyXdsSecurityConfiguration("fake_plugin2" /* bad root */, "",
+                                          "fake_plugin1", "", {}, {},
+                                          true /* failure */);
+  UpdateAndVerifyXdsSecurityConfiguration("fake_plugin1", "", "fake_plugin1",
+                                          "", {server_san_exact_},
+                                          authenticated_identity_);
+}
+
+}  // namespace
+}  // namespace testing
+}  // namespace grpc
+
+int main(int argc, char** argv) {
+  grpc::testing::TestEnvironment env(&argc, argv);
+  ::testing::InitGoogleTest(&argc, argv);
+  // Make the backup poller poll very frequently in order to pick up
+  // updates from all the subchannels's FDs.
+  grpc_core::ConfigVars::Overrides overrides;
+  overrides.client_channel_backup_poll_interval_ms = 1;
+  overrides.trace =
+      "call,channel,client_channel,client_channel_call,client_channel_lb_call,"
+      "handshaker";
+  grpc_core::ConfigVars::SetOverrides(overrides);
+#if TARGET_OS_IPHONE
+  // Workaround Apple CFStream bug
+  grpc_core::SetEnv("grpc_cfstream", "0");
+#endif
+  grpc::testing::FakeCertificateProvider::CertDataMapWrapper cert_data_map_1;
+  grpc::testing::g_fake1_cert_data_map = &cert_data_map_1;
+  grpc::testing::FakeCertificateProvider::CertDataMapWrapper cert_data_map_2;
+  grpc::testing::g_fake2_cert_data_map = &cert_data_map_2;
+  grpc_core::CoreConfiguration::RegisterEphemeralBuilder(
+      [](grpc_core::CoreConfiguration::Builder* builder) {
+        builder->certificate_provider_registry()
+            ->RegisterCertificateProviderFactory(
+                std::make_unique<grpc::testing::FakeCertificateProviderFactory>(
+                    "fake1", grpc::testing::g_fake1_cert_data_map));
+        builder->certificate_provider_registry()
+            ->RegisterCertificateProviderFactory(
+                std::make_unique<grpc::testing::FakeCertificateProviderFactory>(
+                    "fake2", grpc::testing::g_fake2_cert_data_map));
+      });
+  grpc_init();
+  const auto result = RUN_ALL_TESTS();
+  grpc_shutdown();
+  return result;
+}
diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json
index 2fff97c..25537c9 100644
--- a/tools/run_tests/generated/tests.json
+++ b/tools/run_tests/generated/tests.json
@@ -8354,6 +8354,52 @@
     "flaky": false,
     "gtest": true,
     "language": "c++",
+    "name": "spiffe_bundle_map_end2end_test",
+    "platforms": [
+      "linux",
+      "mac",
+      "posix",
+      "windows"
+    ],
+    "uses_polling": true
+  },
+  {
+    "args": [],
+    "benchmark": false,
+    "ci_platforms": [
+      "linux",
+      "mac",
+      "posix"
+    ],
+    "cpu_cost": 1.0,
+    "exclude_configs": [],
+    "exclude_iomgrs": [],
+    "flaky": false,
+    "gtest": true,
+    "language": "c++",
+    "name": "spiffe_ssl_transport_security_test",
+    "platforms": [
+      "linux",
+      "mac",
+      "posix"
+    ],
+    "uses_polling": true
+  },
+  {
+    "args": [],
+    "benchmark": false,
+    "ci_platforms": [
+      "linux",
+      "mac",
+      "posix",
+      "windows"
+    ],
+    "cpu_cost": 1.0,
+    "exclude_configs": [],
+    "exclude_iomgrs": [],
+    "flaky": false,
+    "gtest": true,
+    "language": "c++",
     "name": "spiffe_utils_test",
     "platforms": [
       "linux",
@@ -10773,6 +10819,28 @@
     "ci_platforms": [
       "linux",
       "mac",
+      "posix"
+    ],
+    "cpu_cost": 1.0,
+    "exclude_configs": [],
+    "exclude_iomgrs": [],
+    "flaky": false,
+    "gtest": true,
+    "language": "c++",
+    "name": "xds_security_spiffe_end2end_test",
+    "platforms": [
+      "linux",
+      "mac",
+      "posix"
+    ],
+    "uses_polling": true
+  },
+  {
+    "args": [],
+    "benchmark": false,
+    "ci_platforms": [
+      "linux",
+      "mac",
       "posix",
       "windows"
     ],