diff --git a/cloudplatform/connectivity-oauth/pom.xml b/cloudplatform/connectivity-oauth/pom.xml
index 4335d9e8e..93fb985c8 100644
--- a/cloudplatform/connectivity-oauth/pom.xml
+++ b/cloudplatform/connectivity-oauth/pom.xml
@@ -155,6 +155,16 @@
runtime
+
+ org.bouncycastle
+ bcprov-jdk18on
+ test
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ test
+
com.sap.cloud.environment.servicebinding.api
java-access-api
diff --git a/cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/SecurityLibWorkaroundsTest.java b/cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/SecurityLibWorkaroundsTest.java
index 0899cd4c0..180e0a40d 100644
--- a/cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/SecurityLibWorkaroundsTest.java
+++ b/cloudplatform/connectivity-oauth/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/SecurityLibWorkaroundsTest.java
@@ -3,13 +3,30 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.util.Date;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.jupiter.api.Test;
import com.sap.cloud.sdk.cloudplatform.connectivity.SecurityLibWorkarounds.ZtisClientIdentity;
import com.sap.cloud.security.config.CredentialType;
+import lombok.SneakyThrows;
+
class SecurityLibWorkaroundsTest
{
@Test
@@ -23,6 +40,103 @@ void testZtisClientIdentityImplementsEquals()
assertThat(sut).doesNotHaveSameHashCodeAs(new ZtisClientIdentity("id2", mock(KeyStore.class)));
}
+ @SneakyThrows
+ @Test
+ void testZtisClientIdentityEqualityWithRealCertificates()
+ {
+ // Two keystores loaded with different certificates must be considered unequal.
+ // This is what drives cache-key differentiation in OAuth2Service.tokenServiceCache
+ // when the SVID cert rotates.
+ final KeyPair keyPair = generateKeyPair();
+ final Certificate certA = generateCertificate(keyPair, "CN=cert-a");
+ final Certificate certB = generateCertificate(keyPair, "CN=cert-b");
+
+ final KeyStore ksA = buildKeyStore(keyPair, certA);
+ final KeyStore ksB = buildKeyStore(keyPair, certB);
+ final KeyStore ksSameAsA = buildKeyStore(keyPair, certA);
+
+ final ZtisClientIdentity identityA = new ZtisClientIdentity("client", ksA);
+ final ZtisClientIdentity identityB = new ZtisClientIdentity("client", ksB);
+ final ZtisClientIdentity identitySameAsA = new ZtisClientIdentity("client", ksSameAsA);
+
+ // Different cert → unequal: a new tokenServiceCache entry would be created after rotation
+ assertThat(identityA).isNotEqualTo(identityB);
+ assertThat(identityA).doesNotHaveSameHashCodeAs(identityB);
+
+ // Same cert content → equal: tokenServiceCache hit, HttpClient is reused (expected behaviour)
+ assertThat(identityA).isEqualTo(identitySameAsA);
+ assertThat(identityA).hasSameHashCodeAs(identitySameAsA);
+ }
+
+ @SneakyThrows
+ @Test
+ void testOAuth2ServiceTokenCacheKeyChangesWhenCertRotates()
+ {
+ // This test documents the self-healing behaviour of OAuth2Service.tokenServiceCache:
+ // when the SVID cert rotates, getZtisIdentity() produces a new ZtisClientIdentity with a
+ // different certificate, which hashes to a different cache key, causing tokenServiceCache
+ // to create a new OAuth2TokenService (and thus a new HttpClient with a fresh SSLContext).
+ //
+ // PRECONDITION for this to work: tryGetDestination() (and therefore getZtisIdentity())
+ // must be called again after cert rotation. If the HttpDestination is cached at a higher
+ // level and re-used indefinitely, the ZtisClientIdentity inside OAuth2Service is never
+ // refreshed and the cache key never changes.
+ final KeyPair keyPair = generateKeyPair();
+ final Certificate certBeforeRotation = generateCertificate(keyPair, "CN=before-rotation");
+ final Certificate certAfterRotation = generateCertificate(keyPair, "CN=after-rotation");
+
+ final KeyStore ksBefore = buildKeyStore(keyPair, certBeforeRotation);
+ final KeyStore ksAfter = buildKeyStore(keyPair, certAfterRotation);
+
+ final ZtisClientIdentity identityBefore = new ZtisClientIdentity("client", ksBefore);
+ final ZtisClientIdentity identityAfter = new ZtisClientIdentity("client", ksAfter);
+
+ // Simulate OAuth2Service.getTokenService() cache key computation
+ final com.sap.cloud.sdk.cloudplatform.cache.CacheKey keyBefore =
+ com.sap.cloud.sdk.cloudplatform.cache.CacheKey.fromIds(null, null).append(identityBefore);
+ final com.sap.cloud.sdk.cloudplatform.cache.CacheKey keyAfter =
+ com.sap.cloud.sdk.cloudplatform.cache.CacheKey.fromIds(null, null).append(identityAfter);
+
+ // Cache keys must differ → cache miss → new HttpClient built with rotated cert
+ assertThat(keyBefore).isNotEqualTo(keyAfter);
+ }
+
+ @SneakyThrows
+ private static KeyPair generateKeyPair()
+ {
+ final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(2048);
+ return kpg.generateKeyPair();
+ }
+
+ @SneakyThrows
+ private static Certificate generateCertificate( final KeyPair keyPair, final String subject )
+ {
+ final long now = System.currentTimeMillis();
+ final X500Name name = new X500Name(subject);
+ final BigInteger serial = new BigInteger(Long.toString(now));
+ final Date startDate = new Date(now);
+ final Date endDate = new Date(now + 3_600_000L);
+
+ final JcaX509v3CertificateBuilder certBuilder =
+ new JcaX509v3CertificateBuilder(name, serial, startDate, endDate, name, keyPair.getPublic());
+ certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, new BasicConstraints(true));
+
+ final Provider prov = new BouncyCastleProvider();
+ Security.addProvider(prov);
+ final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
+ return new JcaX509CertificateConverter().setProvider(prov).getCertificate(certBuilder.build(contentSigner));
+ }
+
+ @SneakyThrows
+ private static KeyStore buildKeyStore( final KeyPair keyPair, final Certificate cert )
+ {
+ final KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(null);
+ ks.setKeyEntry("spiffe", keyPair.getPrivate(), new char[0], new Certificate[] { cert });
+ return ks;
+ }
+
@Test
void testGetCredentialType()
{
diff --git a/cloudplatform/connectivity-ztis/pom.xml b/cloudplatform/connectivity-ztis/pom.xml
index e807fa422..c5c2570e1 100644
--- a/cloudplatform/connectivity-ztis/pom.xml
+++ b/cloudplatform/connectivity-ztis/pom.xml
@@ -123,6 +123,16 @@
assertj-core
test
+
+ org.bouncycastle
+ bcprov-jdk18on
+ test
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ test
+
org.junit.jupiter
junit-jupiter-api
diff --git a/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java b/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java
index b9433549d..c66b060b3 100644
--- a/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java
+++ b/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java
@@ -49,6 +49,8 @@ public class ZeroTrustIdentityService
private static final String DEFAULT_SOCKET_PATH = "unix:///tmp/spire-agent/public/api.sock";
private static final String SOCKET_ENVIRONMENT_VARIABLE = "SPIFFE_ENDPOINT_SOCKET";
private static final Duration DEFAULT_SOCKET_TIMEOUT = Duration.ofSeconds(10);
+ // Invalidate the cached KeyStore this long before the SVID expires to tolerate slow SPIRE rotation
+ static final Duration SVID_EXPIRY_SAFETY_MARGIN = Duration.ofDays(1);
@Getter
private static final ZeroTrustIdentityService instance = new ZeroTrustIdentityService();
private final Lazy source = Lazy.of(this::initX509Source);
@@ -223,8 +225,12 @@ KeyStore loadKeyStore( @Nonnull final X509Svid svid )
boolean isKeyStoreCached( @Nonnull final X509Svid svid )
{
- // X509Svid does implement equals, so we don't have to manually compare the certificates
- return keyStoreCache != null && svid.equals(keyStoreCache.svid());
+ if( keyStoreCache == null || !svid.getSpiffeId().equals(keyStoreCache.svid().getSpiffeId()) ) {
+ return false;
+ }
+ // Treat as not cached if the SVID is already expired or expires within the safety margin.
+ final Instant expiryWithMargin = svid.getLeaf().getNotAfter().toInstant().minus(SVID_EXPIRY_SAFETY_MARGIN);
+ return Instant.now().isBefore(expiryWithMargin);
}
@RequiredArgsConstructor
diff --git a/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java b/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java
index b9e064853..6c0100c39 100644
--- a/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java
+++ b/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java
@@ -1,22 +1,43 @@
package com.sap.cloud.sdk.cloudplatform.connectivity;
+import static com.sap.cloud.sdk.cloudplatform.connectivity.ZeroTrustIdentityService.SVID_EXPIRY_SAFETY_MARGIN;
import static com.sap.cloud.sdk.cloudplatform.connectivity.ZeroTrustIdentityService.ZTIS_IDENTIFIER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import java.security.KeyStore;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Provider;
+import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
+import javax.annotation.Nonnull;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -27,20 +48,31 @@
import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException;
import io.spiffe.svid.x509svid.X509Svid;
+import lombok.SneakyThrows;
class ZeroTrustIdentityServiceTest
{
- private static final ServiceBinding binding = mockBinding();
+ private static final String SPIFFE_ID = "spiffe://example.org/workload";
+ private static final ServiceBinding BINDING = mockBinding();
+
+ private static KeyPair keyPair;
private ZeroTrustIdentityService sut;
- private X509Svid svidMock;
+
+ @BeforeAll
+ @SneakyThrows
+ static void setUpClass()
+ {
+ Security.addProvider(new BouncyCastleProvider());
+ final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
+ kpg.initialize(256);
+ keyPair = kpg.generateKeyPair();
+ }
@BeforeEach
void setUp()
{
- sut = spy(new ZeroTrustIdentityService(binding));
- mockSvid(Instant.now().plusSeconds(300));
- doReturn(mock(KeyStore.class)).when(sut).loadKeyStore(any());
+ sut = spy(new ZeroTrustIdentityService(BINDING));
}
@Test
@@ -48,7 +80,7 @@ void testLazyInitialization()
{
// it's important here to spy using the class, not on an already existing instance
// otherwise the method reference stored in Lazy.of(this::initX509Source) would not point to the mock
- sut = spy(ZeroTrustIdentityService.class);
+ final ZeroTrustIdentityService sut = spy(ZeroTrustIdentityService.class);
verify(sut, never()).initX509Source();
assertThatThrownBy(sut::getOrCreateKeyStore).isInstanceOf(CloudPlatformException.class);
@@ -61,22 +93,31 @@ void testLazyInitialization()
@Test
void testKeyStoreCache()
{
- assertThat(sut.isKeyStoreCached(svidMock)).isFalse();
+ // Cache is keyed by SpiffeId, not by SVID object reference.
+ // Two SVIDs with the same SpiffeId and a valid expiry share the same cache entry.
+ final X509Svid svid = newSvid(Instant.now().plus(SVID_EXPIRY_SAFETY_MARGIN).plusSeconds(300));
+ doReturn(svid).when(sut).getX509Svid();
+
+ assertThat(sut.isKeyStoreCached(svid)).isFalse();
sut.getOrCreateKeyStore();
- assertThat(sut.isKeyStoreCached(svidMock)).isTrue();
+ assertThat(sut.isKeyStoreCached(svid)).isTrue();
+ // Second call with the same SVID reference → cache hit, loadKeyStore not called again
sut.getOrCreateKeyStore();
- verify(sut, times(1)).loadKeyStore(svidMock);
+ verify(sut, times(1)).loadKeyStore(svid);
- // actually the time doesn't matter here, as the logic depends on the equals comparison of the X509Svid
- // however, equals cannot be mocked, so equals compares the mock objects, which would be different even with the same times
- // we could make this test more realistic by using real certificate + key objects
- mockSvid(Instant.now().plusSeconds(20));
+ // New SVID object with the same SpiffeId (simulates SPIRE delivering a rotated cert with the same identity)
+ // → still a cache HIT as long as the cert is well within the expiry margin.
+ // The expiry margin is the sole invalidation mechanism; SpiffeId equality means "same workload".
+ final X509Svid rotated =
+ newSvid(Instant.now().plus(ZeroTrustIdentityService.SVID_EXPIRY_SAFETY_MARGIN).plusSeconds(300));
+ doReturn(rotated).when(sut).getX509Svid();
- assertThat(sut.isKeyStoreCached(svidMock)).isFalse();
+ assertThat(sut.isKeyStoreCached(rotated)).isTrue();
sut.getOrCreateKeyStore();
- assertThat(sut.isKeyStoreCached(svidMock)).isTrue();
+ // loadKeyStore was not called a second time — cached KeyStore is reused
+ verify(sut, times(1)).loadKeyStore(any());
}
@Test
@@ -90,10 +131,42 @@ void testThrowsWithoutBinding()
@Test
void testCheckForInvalidCertificate()
{
- mockSvid(Instant.now().minusSeconds(20));
+ final X509Svid expired = newSvid(Instant.now().minusSeconds(20));
+ doReturn(expired).when(sut).getX509Svid();
assertThatThrownBy(sut::getOrCreateKeyStore).isInstanceOf(IllegalStateException.class);
}
+ @Test
+ void testCachedKeyStoreIsRejectedWhenCachedSvidHasExpired()
+ {
+ final X509Svid svid = newSvid(Instant.now().plus(SVID_EXPIRY_SAFETY_MARGIN).plusSeconds(300));
+ doReturn(svid).when(sut).getX509Svid();
+
+ sut.getOrCreateKeyStore();
+ assertThat(sut.isKeyStoreCached(svid)).isTrue();
+
+ // Simulate SPIRE being slow: a new SVID arrives with the same SpiffeId but its cert is already past notAfter.
+ final X509Svid expiredSvid = newSvid(Instant.now().plus(SVID_EXPIRY_SAFETY_MARGIN).minusSeconds(1));
+ doReturn(expiredSvid).when(sut).getX509Svid();
+
+ assertThat(sut.isKeyStoreCached(expiredSvid)).isFalse();
+ }
+
+ @Test
+ void testCachedKeyStoreIsRejectedWhenSvidIsAboutToExpire()
+ {
+ // SVID expiring within the safety margin must not be considered cached,
+ // even when it is the same object reference
+ final X509Svid nearExpiry = newSvid(Instant.now().plus(SVID_EXPIRY_SAFETY_MARGIN).minus(1, ChronoUnit.HOURS));
+ doReturn(nearExpiry).when(sut).getX509Svid();
+
+ // getOrCreateKeyStore() fills the cache (assertSvidNotExpired does not fire — cert is still valid)
+ sut.getOrCreateKeyStore();
+
+ // isKeyStoreCached must return false: expiry margin kicks in
+ assertThat(sut.isKeyStoreCached(nearExpiry)).isFalse();
+ }
+
@Test
void testSpiffeId()
{
@@ -108,7 +181,7 @@ void testAppIdentifier()
final DefaultServiceBinding emptyBinding =
new DefaultServiceBindingBuilder().withServiceIdentifier(ZTIS_IDENTIFIER).build();
- sut = new ZeroTrustIdentityService(emptyBinding);
+ sut = spy(new ZeroTrustIdentityService(emptyBinding));
assertThat(sut.getAppIdentifier()).isEmpty();
final DefaultServiceBinding emptyValue =
@@ -117,49 +190,65 @@ void testAppIdentifier()
.withCredentials(Map.of("parameters", Map.of("app-identifier", "")))
.build();
- sut = new ZeroTrustIdentityService(emptyValue);
+ sut = spy(new ZeroTrustIdentityService(emptyValue));
assertThat(sut.getAppIdentifier()).isEmpty();
}
- @Test
- void testCertOnFileSystem()
+ // -- helpers --
+
+ /**
+ * Generates a SPIFFE-compliant X.509 certificate with the given validity window and parses it into an
+ * {@link X509Svid}. Uses a shared EC key pair so only the certificate (and its expiry) differs between calls.
+ */
+ @SneakyThrows
+ @Nonnull
+ static X509Svid newSvid( @Nonnull final Instant notAfter )
{
- final ServiceBinding binding =
- new DefaultServiceBindingBuilder()
- .withServiceIdentifier(ZTIS_IDENTIFIER)
- .withCredentials(
- Map
- .of(
- "certPath",
- "src/test/resources/ZeroTrustIdentityServiceTest/cert.pem",
- "keyPath",
- "src/test/resources/ZeroTrustIdentityServiceTest/key.pem"))
- .build();
+ final long now = System.currentTimeMillis();
+ final X500Name subject = new X500Name("CN=test-svid");
+ final JcaX509v3CertificateBuilder certBuilder =
+ new JcaX509v3CertificateBuilder(
+ subject,
+ BigInteger.valueOf(now),
+ new Date(now - 1000),
+ Date.from(notAfter),
+ subject,
+ keyPair.getPublic());
- final ZeroTrustIdentityService sut = new ZeroTrustIdentityService(binding);
- final X509Svid svid = sut.getX509Svid();
+ // SPIFFE URI SAN — required by X509Svid.parse()
+ certBuilder
+ .addExtension(
+ Extension.subjectAlternativeName,
+ false,
+ new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, SPIFFE_ID)));
- assertThat(svid.getLeaf().getSubjectX500Principal().getName()).contains("OU=Zero Trust Identity Service");
- assertThat(svid.getPrivateKey()).isNotNull();
- assertThat(svid.getSpiffeId().getTrustDomain().getName()).isEqualTo("0trust.net.sap");
+ // leaf certificate requirements imposed by X509Svid.parse():
+ // - digitalSignature key usage
+ // - no keyCertSign / cRLSign
+ // - BasicConstraints CA=false
+ certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature));
+ certBuilder
+ .addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
+ certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
- assertThat(svid)
- .describedAs("Our cache relies on the equals implementation of SVIDs")
- .isEqualTo(sut.getX509Svid());
+ final Provider bc = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
+ final ContentSigner signer =
+ new JcaContentSignerBuilder("SHA256withECDSA").setProvider(bc).build(keyPair.getPrivate());
+ final X509Certificate cert =
+ new JcaX509CertificateConverter().setProvider(bc).getCertificate(certBuilder.build(signer));
- assertThatThrownBy(sut::getOrCreateKeyStore)
- .describedAs("KeyStore creation should fail since the cert has expired")
- .isInstanceOf(IllegalStateException.class);
+ // SPIFFE library expects PKCS#8 PEM ("PRIVATE KEY"), not SEC1 ("EC PRIVATE KEY")
+ final byte[] certPem = toPem("CERTIFICATE", cert.getEncoded());
+ final byte[] keyPem = toPem("PRIVATE KEY", keyPair.getPrivate().getEncoded());
+ return X509Svid.parse(certPem, keyPem);
}
- private void mockSvid( Instant notAfter )
+ @Nonnull
+ private static byte[] toPem( @Nonnull final String type, @Nonnull final byte[] der )
{
- final X509Svid svid = mock(X509Svid.class);
- final X509Certificate certificate = mock(X509Certificate.class);
- doReturn(Date.from(notAfter)).when(certificate).getNotAfter();
- doReturn(certificate).when(svid).getLeaf();
- doReturn(svid).when(sut).getX509Svid();
- svidMock = svid;
+ final String base64 = java.util.Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(der);
+ final String pem = "-----BEGIN " + type + "-----\n" + base64 + "\n-----END " + type + "-----\n";
+ return pem.getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
private static ServiceBinding mockBinding()
diff --git a/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/cert.pem b/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/cert.pem
deleted file mode 100644
index b68f4c63f..000000000
--- a/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/cert.pem
+++ /dev/null
@@ -1,84 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDUzCCAvmgAwIBAgIQFrwyv3kg1y6j1gIIHLzY4jAKBggqhkjOPQQDAjCBjzEL
-MAkGA1UEBhMCREUxDzANBgNVBAoTBlNBUCBTRTEYMBYGA1UECxMPU0FQIEJUUCBD
-bGllbnRzMRMwEQYDVQQLEwpESTpHQ1AtRVUxMRcwFQYDVQQLEw4wdHJ1c3QubmV0
-LnNhcDEnMCUGA1UEAxMeWmVybyBUcnVzdCBJZGVudGl0eSBTZXJ2aWNlIENBMB4X
-DTI0MDIxNDE2MTg0NVoXDTI0MDIyMTE2MTg1NVowgdoxCzAJBgNVBAYTAkRFMQ8w
-DQYDVQQKEwZTQVAgU0UxGDAWBgNVBAsTD1NBUCBCVFAgQ2xpZW50czETMBEGA1UE
-CxMKREk6R0NQLUVVMTEXMBUGA1UECxMOMHRydXN0Lm5ldC5zYXAxJzAlBgNVBAsT
-Hlplcm8gVHJ1c3QgSWRlbnRpdHkgU2VydmljZSBDQTFJMEcGA1UEAxNAMzI3ODM1
-MjZlNjdlNzAzMzEyZTljYmU5MDExMDgzMWU3ZjA2ZmRiMmE3NmQxY2M1NmFiMGZl
-NTRhNjNkZmEzNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEGEOwzXluGX1HFW
-pOqIcEmmHQa8Urz9umagIG6BuxfDUx9sHJsxu98At2XLowQIdAX/19T+hdLarEm3
-i20GX7+jgekwgeYwDgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMB
-BggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBT7q/+3U52s9XdASuaN
-xzIAn76o3zAfBgNVHSMEGDAWgBSZLO9xp+WgWo3e6ogW8qqSFo4K7DBnBgNVHREE
-YDBehlxzcGlmZmU6Ly8wdHJ1c3QubmV0LnNhcC92MDEvNGI0MDg1ZmZmNGI2MmJk
-YzBjZGNhNjdlZGRhMGEyODQzMTQyNGMyYjU0YTU2ODkwNzgwNTRlMWFhYjgxZGEz
-NjAKBggqhkjOPQQDAgNIADBFAiBbuf+RTh6VwWWdXUJr3y7ptwyo2wJB8FigXrxh
-wXGMtAIhAOsASAJ22PdR+4DWUE/QD7CwBMo5oMjzGtxSZMwusBPq
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIE2jCCAsKgAwIBAgIRAJeqUMBtvrxU18d/8hW71qEwDQYJKoZIhvcNAQELBQAw
-YzELMAkGA1UEBhMCREUxDTALBgNVBAcMBEVVMTAxDzANBgNVBAoMBlNBUCBTRTEY
-MBYGA1UECwwPU0FQIEJUUCBDbGllbnRzMRowGAYDVQQDDBFTQVAgQlRQIENsaWVu
-dCBDQTAeFw0yNDAyMDcwNzM0NDZaFw0yNDAzMjYwODM0NDZaMIGPMQswCQYDVQQG
-EwJERTEPMA0GA1UEChMGU0FQIFNFMRgwFgYDVQQLEw9TQVAgQlRQIENsaWVudHMx
-EzARBgNVBAsTCkRJOkdDUC1FVTExFzAVBgNVBAsTDjB0cnVzdC5uZXQuc2FwMScw
-JQYDVQQDEx5aZXJvIFRydXN0IElkZW50aXR5IFNlcnZpY2UgQ0EwWTATBgcqhkjO
-PQIBBggqhkjOPQMBBwNCAAR0OzIA/EZ8KzpfZ7BTWkj45Gg9BS8RtpeMU70EZfiK
-q9JZ/ihIvuMGCaioQTMy1xZ1Uf4bz7rbhXYWi6Qb/YTxo4IBJTCCASEwIgYDVR0R
-BBswGYYXc3BpZmZlOi8vMHRydXN0Lm5ldC5zYXAwEgYDVR0TAQH/BAgwBgEB/wIB
-ADAfBgNVHSMEGDAWgBSfOpEReBQ7+OAn9OlW7j/auhQPfTAdBgNVHQ4EFgQUmSzv
-cafloFqN3uqIFvKqkhaOCuwwfgYDVR0fBHcwdTBzoHGgb4ZtaHR0cDovL3NhcC1i
-dHAtY2xpZW50LWNhLWV1MTAtY3Jscy5zMy5ldS1jZW50cmFsLTEuYW1hem9uYXdz
-LmNvbS9jcmwvOWU1NjM4OWYtMDE2ZS00OTYzLWEwYWUtYjllMzg1NzE4ZjkxLmNy
-bDAXBgNVHSAEEDAOMAwGCisGAQQBhTYECgEwDgYDVR0PAQH/BAQDAgEGMA0GCSqG
-SIb3DQEBCwUAA4ICAQBUxchZKgegYJX0WN50wrr2MQcDhqJIyptpxI9fQ+YYO1Bn
-S55wtYyODff9W7V3gHYDSv48wccYs3ApZTExGGQ2amKiHIcKNZHwDT4rLN5kE9rL
-O5CvhvtLA+n88vZNV/LwToFRZN6I9lz3/8MMILfLSGyyQZ5VjGLQx8bycgtd+39J
-QWLhmeJphKrcRjBntU8yCvXDW/Wn1palc3aYrXi32Ig5hcF7WK5lTozTsLwlBNir
-JosvmpbiH6t1qfBNeidWYZyYN30WYGIdnzohOQaGVj/tJnHIOXyLSpMsut6K27TB
-t6G7xJS1ONLFBIwibUV4YCzvON1Vq/BQfsQU0BV2HsUzx+q9FK9Iobbtwab11xKb
-kvj9BfYaHV5hVDNdIzkWenkWcUpDo9zyvCOs98wx95UQxJp41u8HBDLvcCSXxRVe
-0aYIwkRQQRay12RpvcAhtKBX3rRsaaLZEZ1185bbLjEZDyxNu6vxTymIIGQW11se
-guUrnf3ExjIH+4WuXdTFptIf5ubXPlhDzA3OSXjFpUMyP/clpGyD3htALhlsDj4M
-0T5a4Lste7XjFZw9j+4iNZzm8eptFY1A5lMSxZMeReQR3mR7MKjeIMN0M58lmQxZ
-50T1eeUWFQfK7xHuNt/G8szHpr4kjYIgj3b2Mu6DpkX3C2Mu8PbvCoYupf0OIg==
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIGSjCCBDKgAwIBAgITcAAAAApMdyh1162sZgAAAAAACjANBgkqhkiG9w0BAQsF
-ADBNMQswCQYDVQQGEwJERTERMA8GA1UEBwwIV2FsbGRvcmYxDzANBgNVBAoMBlNB
-UCBTRTEaMBgGA1UEAwwRU0FQIENsb3VkIFJvb3QgQ0EwHhcNMjIwNTA5MDgzMzU1
-WhcNMzIwNTA5MDg0MzU1WjBjMQswCQYDVQQGEwJERTENMAsGA1UEBwwERVUxMDEP
-MA0GA1UECgwGU0FQIFNFMRgwFgYDVQQLDA9TQVAgQlRQIENsaWVudHMxGjAYBgNV
-BAMMEVNBUCBCVFAgQ2xpZW50IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
-CgKCAgEAtVs/vbOU9P8soC0Y3GNzbLwbbP4z1MC38xMYZlbDLDapaj9k3LF9LSXu
-kHK+tnOmuKX2aFRouYTusJ/mDL3DsYcgEERPNPDnq1lpp3nLr8aPJLGlj7G8bfGW
-dvTVk0Trt9PpwjsbE2J/R0qs5Y0idlb8DOJ2ilRFOK5zsX3YscRKlHknTwlwrNN4
-WjPeNQLMbDoAI+YTs7luCIHItmpb3/ls+bup8hCg7rYOh4mDv3nuHq2OHmc6+2XG
-2Rm6ngrgZwz8Yr3bk7qaWfu2iVPn9nFJhH0+r/oCXWNiUwL3+uJ+pLJKfzYJ/bwl
-XJblYerj3rgs76zhoXX6RJFQ94LAB4qeARtVde87bpD6xzFqUM1mqEyNmPMOwdRC
-4BtOQuYflabIsW/duISQni1e/K27KM/ry59KybdOJav5y+SXT2ZANa7z4+x2nm2y
-10BOhClZv//m3XpiCmAp62y5qfJSknsXmOHzol0fmI5OUbvuvB9po3G6A6wD/ME6
-IdacsqCG2/u8KaGkMYjeLU/hX6DQ3IcloZlD6wloZYyJmNHlbm/367q0LLoSPsxg
-VU9Y66E/kjDkCg+v4lkuZ6N3LI3BXdOvLmbRAkXM4/0ssBWKmhD4uwEa7V24TIEe
-mlizpFO763SyVxBpKWfLI+hUq1ETUdCC697B8vllB7x0otXT+FECAwEAAaOCAQsw
-ggEHMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFJ86kRF4FDv44Cf06Vbu
-P9q6FA99MB8GA1UdIwQYMBaAFBy8ZisOyo1Ln42TcakPymdGaRMiMEoGA1UdHwRD
-MEEwP6A9oDuGOWh0dHA6Ly9jZHAucGtpLmNvLnNhcC5jb20vY2RwL1NBUCUyMENs
-b3VkJTIwUm9vdCUyMENBLmNybDBVBggrBgEFBQcBAQRJMEcwRQYIKwYBBQUHMAKG
-OWh0dHA6Ly9haWEucGtpLmNvLnNhcC5jb20vYWlhL1NBUCUyMENsb3VkJTIwUm9v
-dCUyMENBLmNydDAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAG+f
-q3auXEbXpb2d2bzsK1Vm+jqJCToHypZH+L/elxr9wC4+RpT9YOdHWBKghO0c1xAK
-L5kfDRn9vtobdh11dfDKdFRmWJU5pwU/Ok+XLE+moCZ2LkJy1uze7eJnpFhMs6/F
-s3AgvAROpz7Mi7ChWiGJfEYJQediOE8cmoTJ1KYOjK+ZM02viBA92ShKNL1xk/LK
-JZgpZdp2iYrPyqJsZzosaPyT4cBSs5+qrynf7/Z3ENoTD2/yUWwxLBgWOtWhoEXR
-D4tlP4ITDo03Vmvp4XZuUSN583npcf5H1afISPrpqamlVHD4T8/NYi6vb0bJW8fD
-333Jpz6LEfJtVrTT8s1Xl8oGKp0wu++1Jvg5ojYw8Kbk+DjS9H4hKIbfKtpK+d8R
-OJuLMwcdiD+gL5tmeuGgleQZwRk96+oYOGGpPhkgxmAKGuoVDbB1yY0Uinm4eBJ3
-2wCBwDBFXbWAMoKWKIvARpIu822q26UuAuov7BBfDnrTXIoMvBwM73pYsaOS4jRG
-hRWyDnBm76CiEkI8av2fJoCZTs8itYebNBbIxwimCpYeU4y8K4z4rwNpCJtNY3YN
-pZ7y7TajEf9jXY/2PCXjSOzr9kUi4pPGfSMKXVzIV16upXp1uF2dyzOYAAHQ/UWO
-ZeYjiLJ5fLnwllwT7UjJqd0DTRoC7hlKyuxv493p
------END CERTIFICATE-----
diff --git a/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/key.pem b/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/key.pem
deleted file mode 100644
index 8cfdc47d4..000000000
--- a/cloudplatform/connectivity-ztis/src/test/resources/ZeroTrustIdentityServiceTest/key.pem
+++ /dev/null
@@ -1,5 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOt7smVohjk5hhbL0
-iXozlFrgtoBXVKtVzadX3X7ohv6hRANCAARCv3HNbB6kWJwEwfHwFEO/7VIE8GBF
-59iI7p7nZWXmugUfa2Lnc32ijODsVbN7i+XkMWB+b7C3yL2LYBXz07ts
------END PRIVATE KEY-----
\ No newline at end of file
diff --git a/release_notes.md b/release_notes.md
index b65d73ffa..1d6584e46 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -22,3 +22,4 @@
### 🐛 Fixed Issues
- Fixed stateful OData request path construction caused by shared `ODataResourcePath` instances being mutated when building count, read-by-key, and function requests.
+- Fixed using expired Zero Trust Identity Service certificates