diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsKeyStoreUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsKeyStoreUtils.java index d8f51c61935f..70dc7c5abee8 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsKeyStoreUtils.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsKeyStoreUtils.java @@ -64,11 +64,13 @@ * *
Everything else matches the upstream method: key store type (JKS), alias, empty password, * accepted key format (PKCS#8 {@code PRIVATE KEY}), which private key is chosen, and error - * messages. Any input that the upstream method accepted is still accepted, even though more of it - * is now used: once a certificate and a private key have been found (the point where the upstream - * method stopped reading), a malformed later PEM section ends the input instead of failing, and an - * extra certificate that cannot be parsed is left out of the chain. Unlike the upstream method, the - * input is decoded as UTF-8 rather than the platform default charset, and it is read to the end. + * messages. Any valid certificate-and-key pair that the upstream method accepted is still accepted, + * even though more of the input is now used: once a certificate and a private key have been found + * (the point where the upstream method stopped reading), a malformed later PEM section ends the + * input instead of failing, and an extra certificate that cannot be parsed is left out of the + * chain. Unlike the upstream method, the input is decoded as UTF-8 rather than the platform default + * charset, it is read to the end, and the private key is verified against the leaf certificate's + * public key. */ @NullMarked final class MtlsKeyStoreUtils { @@ -89,7 +91,8 @@ private MtlsKeyStoreUtils() {} * @throws IllegalArgumentException if the input has no certificate or no private key, or if a PEM * section before both a certificate and a private key have been read is malformed * @throws GeneralSecurityException if the first certificate or the private key cannot be parsed, - * or the key store cannot be created + * if the private key does not match the leaf certificate's public key, or if the key store + * cannot be created * @throws IOException if the input cannot be read */ static KeyStore createMtlsKeyStore(InputStream certAndKey) @@ -143,9 +146,12 @@ static KeyStore createMtlsKeyStore(InputStream certAndKey) LOGGER.log(Level.FINE, "Skipping unparsable extra certificate in mTLS chain", e); } } + X509Certificate leaf = chain.get(0); + String keyAlgorithm = leaf.getPublicKey().getAlgorithm(); PrivateKey key = - KeyFactory.getInstance(chain.get(0).getPublicKey().getAlgorithm()) + KeyFactory.getInstance("ECDSA".equals(keyAlgorithm) ? "EC" : keyAlgorithm) .generatePrivate(new PKCS8EncodedKeySpec(privateKey)); + MtlsUtils.verifyKeyPair(leaf, key); KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(null); diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java index cdda93f49b44..ac9f4dacc856 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java @@ -42,12 +42,21 @@ import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; +import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.security.spec.X509EncodedKeySpec; import java.util.Locale; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -64,6 +73,9 @@ public class MtlsUtils { static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json"; static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; + private static final byte[] VERIFICATION_DATA = + "verification-data".getBytes(StandardCharsets.UTF_8); + /** * Well-known directory where workload SPIFFE credentials and certificate configurations are * delivered at container startup. @@ -151,6 +163,62 @@ static void setTimeServiceForTesting(@Nullable TimeService service) { timeService = service != null ? service : Thread::sleep; } + /** + * Verifies that {@code privateKey} corresponds to the public key in {@code cert} by performing a + * test signature and verification ({@code SHA256withRSA} for RSA keys, {@code SHA256withECDSA} + * for EC/ECDSA keys). + * + *
Certificates with key algorithms other than RSA, EC, or ECDSA return without error so
+ * callers that accept any JCA key algorithm (such as {@link MtlsKeyStoreUtils}) pass through
+ * unchanged.
+ *
+ * @param cert the X.509 certificate whose public key is checked
+ * @param privateKey the private key to verify against {@code cert}
+ * @throws GeneralSecurityException if {@code privateKey} does not match {@code cert}'s public
+ * key, or if signature initialization, signing, or verification fails
+ */
+ @InternalApi
+ public static void verifyKeyPair(X509Certificate cert, PrivateKey privateKey)
+ throws GeneralSecurityException {
+ PublicKey publicKey = cert.getPublicKey();
+ String keyAlgorithm = publicKey.getAlgorithm();
+ String sigAlg;
+ if ("RSA".equals(keyAlgorithm)) {
+ sigAlg = "SHA256withRSA";
+ } else if ("EC".equals(keyAlgorithm) || "ECDSA".equals(keyAlgorithm)) {
+ sigAlg = "SHA256withECDSA";
+ } else {
+ return;
+ }
+
+ if ("ECDSA".equals(keyAlgorithm) && publicKey.getEncoded() != null) {
+ // SunEC rejects keys whose algorithm is "ECDSA" rather than "EC"; normalize the key.
+ publicKey =
+ KeyFactory.getInstance("EC")
+ .generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
+ }
+
+ Signature signer = Signature.getInstance(sigAlg);
+ signer.initSign(privateKey);
+ signer.update(VERIFICATION_DATA);
+ byte[] signature = signer.sign();
+
+ Signature verifier = Signature.getInstance(sigAlg);
+ verifier.initVerify(publicKey);
+ verifier.update(VERIFICATION_DATA);
+
+ boolean matches;
+ try {
+ matches = verifier.verify(signature);
+ } catch (SignatureException e) {
+ throw new GeneralSecurityException("Certificate and private key do not match", e);
+ }
+
+ if (!matches) {
+ throw new GeneralSecurityException("Certificate and private key do not match");
+ }
+ }
+
/**
* Checks whether {@code pathStr} resides within {@code targetWellKnownDir} (or either of the
* default well-known SPIFFE directories {@link #WELL_KNOWN_SPIFFE_DIR} and {@link
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
index fa7deeda21ca..d2af58079201 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
@@ -35,13 +35,17 @@
import com.google.auth.oauth2.PropertyProvider;
import com.google.auth.oauth2.SystemEnvironmentProvider;
import com.google.auth.oauth2.SystemPropertyProvider;
+import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InterruptedIOException;
import java.io.SequenceInputStream;
import java.security.KeyStore;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@@ -52,6 +56,25 @@
@NullMarked
@InternalApi
public class X509Provider implements MtlsProvider {
+ private static final Logger LOGGER = Logger.getLogger(X509Provider.class.getName());
+
+ // Retries for verifying certificate and private key matching during atomic key rotation.
+ static final int CERT_KEY_MATCH_RETRIES = 3;
+ static final long CERT_KEY_MATCH_RETRY_INTERVAL_MS = 100;
+
+ @VisibleForTesting
+ @FunctionalInterface
+ interface TimeService {
+ void sleep(long millis) throws InterruptedException;
+ }
+
+ private static volatile TimeService timeService = Thread::sleep;
+
+ @VisibleForTesting
+ static void setTimeServiceForTesting(@Nullable TimeService service) {
+ timeService = service != null ? service : Thread::sleep;
+ }
+
private final EnvironmentProvider envProvider;
private final PropertyProvider propProvider;
private final @Nullable String certConfigPathOverride;
@@ -193,22 +216,48 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx
throw configUnavailable;
}
- // Read the certificate and private key file paths into streams.
- try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
- InputStream privateKeyStream =
- new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
- SequenceInputStream certAndPrivateKeyStream =
- new SequenceInputStream(certStream, privateKeyStream)) {
+ // Read the certificate and private key file paths into streams, retrying to handle transient
+ // mid-rotation mismatches or file replacements when separate cert_path and key_path files are
+ // updated non-atomically relative to each other.
+ Exception lastException = null;
+ for (int attempt = 0; attempt < CERT_KEY_MATCH_RETRIES; attempt++) {
+ try (InputStream certStream =
+ new FileInputStream(new File(workloadCertConfig.getCertPath()));
+ InputStream privateKeyStream =
+ new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
+ SequenceInputStream certAndPrivateKeyStream =
+ new SequenceInputStream(certStream, privateKeyStream)) {
- // Build a key store using the combined stream.
- return MtlsKeyStoreUtils.createMtlsKeyStore(certAndPrivateKeyStream);
- } catch (CertificateSourceUnavailableException e) {
- // Throw the CertificateSourceUnavailableException without wrapping.
- throw e;
- } catch (Exception e) {
- // Wrap all other exception types to an IOException.
- throw new IOException("X509Provider: Unexpected IOException:", e);
+ // Build a key store using the combined stream.
+ return MtlsKeyStoreUtils.createMtlsKeyStore(certAndPrivateKeyStream);
+ } catch (CertificateSourceUnavailableException e) {
+ // Throw the CertificateSourceUnavailableException without wrapping.
+ throw e;
+ } catch (Exception e) {
+ lastException = e;
+ if (attempt + 1 < CERT_KEY_MATCH_RETRIES) {
+ LOGGER.log(
+ Level.FINE,
+ "X509Provider: Failed to load or verify certificate and private key on attempt "
+ + (attempt + 1)
+ + ", retrying",
+ e);
+ try {
+ timeService.sleep(CERT_KEY_MATCH_RETRY_INTERVAL_MS);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ InterruptedIOException ioException =
+ new InterruptedIOException(
+ "X509Provider: Interrupted while waiting for certificate and private key"
+ + " match.");
+ ioException.initCause(interrupted);
+ throw ioException;
+ }
+ }
+ }
}
+ // Wrap all other exception types to an IOException.
+ throw new IOException("X509Provider: Unexpected IOException:", lastException);
}
/**
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCertificateValidationUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCertificateValidationUtils.java
index e39b9bc36480..473a7f0d0f60 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCertificateValidationUtils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCertificateValidationUtils.java
@@ -30,6 +30,7 @@
*/
package com.google.auth.oauth2;
+import com.google.auth.mtls.MtlsUtils;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -38,14 +39,10 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
-import java.security.spec.X509EncodedKeySpec;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -79,9 +76,6 @@ final class AgentIdentityCertificateValidationUtils {
private static final String SPIFFE_SCHEME_PREFIX = "spiffe://";
- private static final byte[] VERIFICATION_DATA =
- "verification-data".getBytes(StandardCharsets.UTF_8);
-
private AgentIdentityCertificateValidationUtils() {}
/** Thrown for key algorithms that can never be verified, so callers fail without retrying. */
@@ -159,35 +153,16 @@ static PrivateKey readPrivateKey(final String keyPath, final @Nullable String al
*/
static boolean verifyKeyPair(final X509Certificate cert, final PrivateKey privateKey)
throws UnsupportedKeyAlgorithmException {
- PublicKey publicKey = cert.getPublicKey();
- String keyAlgorithm = publicKey.getAlgorithm();
- String sigAlg;
- if ("RSA".equals(keyAlgorithm)) {
- sigAlg = "SHA256withRSA";
- } else if ("EC".equals(keyAlgorithm) || "ECDSA".equals(keyAlgorithm)) {
- sigAlg = "SHA256withECDSA";
- } else {
+ String keyAlgorithm = cert.getPublicKey().getAlgorithm();
+ if (!"RSA".equals(keyAlgorithm)
+ && !"EC".equals(keyAlgorithm)
+ && !"ECDSA".equals(keyAlgorithm)) {
throw new UnsupportedKeyAlgorithmException(keyAlgorithm);
}
try {
- if ("ECDSA".equals(keyAlgorithm) && publicKey.getEncoded() != null) {
- // SunEC rejects keys whose algorithm is "ECDSA" rather than "EC"; normalize the key.
- publicKey =
- KeyFactory.getInstance("EC")
- .generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
- }
-
- Signature signer = Signature.getInstance(sigAlg);
- signer.initSign(privateKey);
- signer.update(VERIFICATION_DATA);
- byte[] signature = signer.sign();
-
- Signature verifier = Signature.getInstance(sigAlg);
- verifier.initVerify(publicKey);
- verifier.update(VERIFICATION_DATA);
-
- return verifier.verify(signature);
+ MtlsUtils.verifyKeyPair(cert, privateKey);
+ return true;
} catch (Exception e) {
LoggingUtils.log(
LOGGER_PROVIDER,
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsKeyStoreUtilsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsKeyStoreUtilsTest.java
index 5f1e8fca0545..11f54ec990a3 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsKeyStoreUtilsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsKeyStoreUtilsTest.java
@@ -34,6 +34,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -44,13 +45,20 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;
+import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
import java.util.Collections;
import org.junit.jupiter.api.Test;
@@ -62,6 +70,7 @@ class MtlsKeyStoreUtilsTest {
static final String CHAIN_KEY_PATH = "testresources/mtls/test_chain_key.pem";
private static final String SINGLE_CERT_PATH = "testresources/mtls/test_cert.pem";
private static final String SINGLE_KEY_PATH = "testresources/mtls/test_key.pem";
+ private static final String DSA_CERT_PATH = "testresources/agent/agent_spiffe_dsa_cert.pem";
@Test
void createMtlsKeyStore_certificatesFirst_keepsFullChainInOrder() throws Exception {
@@ -310,6 +319,117 @@ void createMtlsKeyStore_missingPrivateKey_throws() throws Exception {
assertEquals("private key is missing from certAndKey string", e.getMessage());
}
+ @Test
+ void createMtlsKeyStore_mismatchedRsaKeyPairSameKeySize_throws() throws Exception {
+ // Generate a 2048-bit RSA key whose modulus is strictly smaller than SINGLE_CERT_PATH's
+ // 2048-bit RSA modulus, guaranteeing that Signature.verify() returns false (rather than
+ // throwing SignatureException on s >= n).
+ X509Certificate singleCert =
+ (X509Certificate)
+ CertificateFactory.getInstance("X.509")
+ .generateCertificate(
+ new ByteArrayInputStream(read(SINGLE_CERT_PATH).getBytes(UTF_8)));
+ RSAPublicKey certPublicKey = (RSAPublicKey) singleCert.getPublicKey();
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(certPublicKey.getModulus().bitLength());
+ KeyPair otherRsaKeyPair;
+ do {
+ otherRsaKeyPair = kpg.generateKeyPair();
+ } while (((RSAPublicKey) otherRsaKeyPair.getPublic())
+ .getModulus()
+ .compareTo(certPublicKey.getModulus())
+ >= 0);
+ String otherRsaKeyPem = toPkcs8Pem(otherRsaKeyPair.getPrivate());
+
+ GeneralSecurityException e =
+ assertThrows(
+ GeneralSecurityException.class,
+ () ->
+ MtlsKeyStoreUtils.createMtlsKeyStore(
+ stream(read(SINGLE_CERT_PATH), otherRsaKeyPem)));
+ assertEquals("Certificate and private key do not match", e.getMessage());
+ assertNull(e.getCause());
+ }
+
+ @Test
+ void createMtlsKeyStore_mismatchedRsaKeyPairDifferentKeySize_throws() throws Exception {
+ // Pairing a 1024-bit RSA private key with the 2048-bit RSA SINGLE_CERT_PATH deterministically
+ // causes Signature.verify() to throw SignatureException ("Bad signature length: got 128 but was
+ // expecting 256"), which must be translated into GeneralSecurityException("Certificate and
+ // private key do not match").
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(1024);
+ KeyPair otherRsaKeyPair = kpg.generateKeyPair();
+ String otherRsaKeyPem = toPkcs8Pem(otherRsaKeyPair.getPrivate());
+
+ GeneralSecurityException e =
+ assertThrows(
+ GeneralSecurityException.class,
+ () ->
+ MtlsKeyStoreUtils.createMtlsKeyStore(
+ stream(read(SINGLE_CERT_PATH), otherRsaKeyPem)));
+ assertEquals("Certificate and private key do not match", e.getMessage());
+ assertTrue(e.getCause() instanceof SignatureException);
+ }
+
+ @Test
+ void createMtlsKeyStore_mismatchedEcKeyPair_throws() throws Exception {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
+ kpg.initialize(256);
+ KeyPair otherEcKeyPair = kpg.generateKeyPair();
+ String otherEcKeyPem = toPkcs8Pem(otherEcKeyPair.getPrivate());
+
+ GeneralSecurityException e =
+ assertThrows(
+ GeneralSecurityException.class,
+ () ->
+ MtlsKeyStoreUtils.createMtlsKeyStore(stream(read(CHAIN_CERT_PATH), otherEcKeyPem)));
+ assertEquals("Certificate and private key do not match", e.getMessage());
+ assertNull(e.getCause());
+ }
+
+ @Test
+ void createMtlsKeyStore_mismatchedEcKeyPairDifferentCurve_throws() throws Exception {
+ // Pairing a 384-bit (secp384r1) EC private key with the 256-bit (secp256r1) EC CHAIN_CERT_PATH.
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
+ kpg.initialize(384);
+ KeyPair otherEcKeyPair = kpg.generateKeyPair();
+ String otherEcKeyPem = toPkcs8Pem(otherEcKeyPair.getPrivate());
+
+ GeneralSecurityException e =
+ assertThrows(
+ GeneralSecurityException.class,
+ () ->
+ MtlsKeyStoreUtils.createMtlsKeyStore(stream(read(CHAIN_CERT_PATH), otherEcKeyPem)));
+ assertEquals("Certificate and private key do not match", e.getMessage());
+ }
+
+ @Test
+ void createMtlsKeyStore_nonRsaOrEcKeyPair_passesThroughLikeUpstream() throws Exception {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
+ kpg.initialize(1024);
+ KeyPair dsaKeyPair = kpg.generateKeyPair();
+ String dsaKeyPem = toPkcs8Pem(dsaKeyPair.getPrivate());
+
+ KeyStore keyStore =
+ MtlsKeyStoreUtils.createMtlsKeyStore(stream(read(DSA_CERT_PATH), dsaKeyPem));
+ KeyStore expected = SecurityUtils.createMtlsKeyStore(stream(read(DSA_CERT_PATH), dsaKeyPem));
+
+ String alias = onlyAlias(keyStore);
+ assertArrayEquals(
+ expected.getCertificate(alias).getEncoded(),
+ keyStore.getCertificateChain(alias)[0].getEncoded());
+ assertArrayEquals(
+ expected.getKey(alias, new char[] {}).getEncoded(),
+ keyStore.getKey(alias, new char[] {}).getEncoded());
+ }
+
+ static String toPkcs8Pem(PrivateKey privateKey) {
+ return "-----BEGIN PRIVATE KEY-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes(UTF_8)).encodeToString(privateKey.getEncoded())
+ + "\n-----END PRIVATE KEY-----\n";
+ }
+
static String read(String path) throws IOException {
return new String(Files.readAllBytes(new File(path).toPath()), UTF_8);
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
index 5c6c9e9ff040..7d5518d0ee0b 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
@@ -44,13 +44,19 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -447,6 +453,202 @@ void x509Provider_explicitConfigInWellKnownSpiffeDir_pollsDuringStartupUntilFile
}
}
+ @Test
+ void x509Provider_mismatchedKeyPair_retriesAndSucceedsWhenKeyRotates(@TempDir Path tempDir)
+ throws Exception {
+ Path certFile = tempDir.resolve("cert.pem");
+ Path keyFile = tempDir.resolve("key.pem");
+ Files.copy(new File(TEST_CERT_PATH).toPath(), certFile);
+
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(1024);
+ KeyPair oldRsaKeyPair = kpg.generateKeyPair();
+ Files.write(
+ keyFile, MtlsKeyStoreUtilsTest.toPkcs8Pem(oldRsaKeyPair.getPrivate()).getBytes(UTF_8));
+
+ Path config = writeWorkloadConfig(tempDir, certFile, keyFile);
+ List