Repository navigation
fix(auth): verify public/private key match with retries in mTLS keystore loading - #14829
macastelaz wants to merge 5 commits into
Conversation
…ore loading Verify that the parsed private key matches the leaf certificate's public key in MtlsKeyStoreUtils.createMtlsKeyStore() via a test signature and verification, and retry up to 3 times (with a 100ms interval) in X509Provider.getKeyStore() when loading separate cert_path and key_path files from disk to handle transient mid-rotation mismatches.
There was a problem hiding this comment.
Code Review
This pull request introduces key pair verification when creating an mTLS KeyStore to ensure the private key matches the leaf certificate's public key. It also adds retry logic in X509Provider to handle transient mismatches during atomic key rotations. The review feedback recommends separating cryptographic setup/execution failures from actual key verification mismatches to prevent misleading error messages, and advises against adding out-of-scope algorithm normalization (such as converting 'ECDSA' to 'EC') to maintain behavioral alignment.
…ate MtlsKeyStoreUtils javadoc
…terministic tests
nbayati
left a comment
There was a problem hiding this comment.
Left a comment on possibly reconciling the cert-key matching logic between this flow and the agentic path that already has it implemented.
Also the PR description mentions a 0-sleep GKE bundle test in X509ProviderTest and a DSA pass-through test in MtlsKeyStoreUtilsTest that don't seem to be in the diff (the GKE bundle one seems worth adding).
| * Verifies that {@code privateKey} corresponds to the public key in {@code cert} by performing a | ||
| * test signature and verification. | ||
| */ | ||
| private static void verifyKeyPair(X509Certificate cert, PrivateKey privateKey) |
There was a problem hiding this comment.
This cert-key check already existed in the Agent Identity flow, and this PR duplicates some of that logic. The two copies have already diverged (for example on "ECDSA" handling and how errors are treated; not sure if that was intentional).
Can we share the core check in a single internal helper (e.g. in MtlsUtils)?
There was a problem hiding this comment.
Good call — extracted the shared check into @internalapi MtlsUtils.verifyKeyPair(X509Certificate, PrivateKey) in 17102f6 (including the "ECDSA" → "EC" KeyFactory normalization for SunEC). Both MtlsKeyStoreUtils.createMtlsKeyStore and AgentIdentityCertificateValidationUtils.verifyKeyPair now delegate to MtlsUtils.verifyKeyPair, while AgentIdentityCertificateValidationUtils retains its strict RSA/EC/ECDSA algorithm allowlist (UnsupportedKeyAlgorithmException) and boolean/logging contract.
| timeService.sleep(CERT_KEY_MATCH_RETRY_INTERVAL_MS); | ||
| } catch (InterruptedException interrupted) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new IOException( |
There was a problem hiding this comment.
nit: The description says an interrupt surfaces as InterruptedIOException, which is what MtlsUtils throws, but this throws a plain IOException. Callers can't tell an interrupt apart from a read failure without digging into the cause.
There was a problem hiding this comment.
Fixed! X509Provider.getKeyStore() now throws InterruptedIOException (with initCause(interrupted)) to match MtlsUtils and the PR description, and X509ProviderTest now asserts InterruptedIOException.class.
…tion, and add missing tests
|
Thanks for the review @nbayati!
|
Summary
Verifies that the parsed
PrivateKeycryptographically matches the leafX509Certificate'sPublicKeyinMtlsKeyStoreUtils.createMtlsKeyStore(), and adds retry handling (3attempts with a100msbackoff, matchingAgentIdentityUtils.loadAndVerifyCredentials()) inX509Provider.getKeyStore()when loading separatecert_pathandkey_pathfiles from disk.Problem
KeyStoreentries:KeyStore.getInstance("JKS").setKeyEntry("alias", key, new char[] {}, chain)does not verify thatkeycorresponds tochain[0].getPublicKey()as long as the key algorithm matches (e.g., both are RSA or both are EC).cert_pathandkey_pathfiles: When an external certificate rotator updatescert_path(cert_A->cert_B) slightly beforekey_path(key_A->key_B), transport channel refresh (CertificateRotationTracker, which trackscert_path's SHA-256 fingerprint) can triggerX509Provider.getKeyStore()during the intermediate(cert_B, key_A)window. Without key-pair verification and retries,getKeyStore()succeeds with a mismatchedKeyStoreand the channel marksFP(cert_B)as refreshed, leaving the mTLS channel with a mismatched certificate and private key until the next rotation.Solution
MtlsKeyStoreUtils.createMtlsKeyStore(): Perform a test signature and verification (SHA256withRSAfor RSA keys,SHA256withECDSAfor EC keys) to ensure the parsed private key matches the leaf certificate's public key, throwingGeneralSecurityException("Certificate and private key do not match")on mismatch.X509Provider.getKeyStore(): Retry up to3times (100msinterval, with injectableTimeServicefor deterministic testing) when reading and verifying separatecert_pathandkey_pathfiles fromWorkloadCertificateConfiguration, handling transient mid-rotation key mismatches or partial file writes while preserving interrupt restoration (InterruptedIOException) onInterruptedException.Testing
MtlsKeyStoreUtilsTestcovering mismatched RSA (same and different key sizes) and EC key pairs, as well as pass-through for non-RSA/EC algorithms (e.g. DSA).X509ProviderTestcovering:key_pathfinishes rotating to matchcert_pathon a subsequent attempt (1sleep of100ms).IOExceptioncaused byGeneralSecurityException("Certificate and private key do not match")after exhausting all 3 attempts (2sleeps of100ms).cert_pathis transiently empty/truncated on the first attempt.InterruptedIOException(0subsequent attempts) if interrupted during retry backoff.0sleeps) for the GKE combined credential bundle (x509.credential-bundle.private-key.pem).--functional-type=agent --identity-type=agent-identity) execution logs (agent-bound-token-java-test-28m2v): https://paste.googleplex.com/6755399436524638