From c09b2079d9426da8ce2b5f8515865b94df5fcbf7 Mon Sep 17 00:00:00 2001 From: Vishesh Upadhyay Date: Sun, 25 Oct 2026 23:11:03 +0530 Subject: [PATCH] fix: bound verified certificate cache size --- .../storekit/verification/ChainVerifier.java | 23 ++++++++++++++----- .../verification/ChainVerifierTest.java | 17 ++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/apple/itunes/storekit/verification/ChainVerifier.java b/src/main/java/com/apple/itunes/storekit/verification/ChainVerifier.java index 0b989725..cbed0a6e 100644 --- a/src/main/java/com/apple/itunes/storekit/verification/ChainVerifier.java +++ b/src/main/java/com/apple/itunes/storekit/verification/ChainVerifier.java @@ -20,6 +20,7 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Base64; +import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.LinkedList; @@ -130,13 +131,23 @@ private PublicKey getCachedPublicKey(List certificateChain) { return null; } - private void putVerifiedPublicKey(List certificateChain, PublicKey publicKey) { - Instant cacheExpiration = clock.instant().plus(CACHE_TIME_LIMIT, ChronoUnit.MINUTES); - verifiedPublicKeyCache.put(certificateChain, new CachedEntry(cacheExpiration, publicKey)); - if (verifiedPublicKeyCache.size() > MAXIMUM_CACHE_SIZE) { - // In the very unlikely event that the map has become too large, clear out old entries - verifiedPublicKeyCache.entrySet().removeIf(e -> e.getValue().cachedExpirationDate.isBefore(clock.instant())); + private synchronized void putVerifiedPublicKey(List certificateChain, PublicKey publicKey) { + Instant now = clock.instant(); + verifiedPublicKeyCache.entrySet().removeIf(e -> e.getValue().cachedExpirationDate.isBefore(now)); + + while (verifiedPublicKeyCache.size() >= MAXIMUM_CACHE_SIZE) { + List certificateChainToEvict = verifiedPublicKeyCache.entrySet().stream() + .min(Comparator.comparing(e -> e.getValue().cachedExpirationDate)) + .map(e -> e.getKey()) + .orElse(null); + if (certificateChainToEvict == null) { + break; + } + verifiedPublicKeyCache.remove(certificateChainToEvict); } + + Instant cacheExpiration = now.plus(CACHE_TIME_LIMIT, ChronoUnit.MINUTES); + verifiedPublicKeyCache.put(certificateChain, new CachedEntry(cacheExpiration, publicKey)); } private static class CachedEntry { diff --git a/src/test/java/com/apple/itunes/storekit/verification/ChainVerifierTest.java b/src/test/java/com/apple/itunes/storekit/verification/ChainVerifierTest.java index 34fbea44..a7e0cae4 100644 --- a/src/test/java/com/apple/itunes/storekit/verification/ChainVerifierTest.java +++ b/src/test/java/com/apple/itunes/storekit/verification/ChainVerifierTest.java @@ -229,6 +229,23 @@ public void testOcspResponseCachingWithSlightlyDifferentChains() throws Verifica Mockito.verify(mockedChainVerifier, Mockito.times(2)).verifyChainWithoutCaching(Mockito.any(), Mockito.anyBoolean(), Mockito.any()); } + @Test + public void testOcspResponseCacheDoesNotExceedMaximumSizeWhenEntriesAreFresh() throws Exception { + for (int index = 0; index <= 32; index++) { + mockedChainVerifier.verifyChain(new String[] { + LEAF_CERT_BASE64_ENCODED, + INTERMEDIATE_CA_BASE64_ENCODED, + ROOT_CA_BASE64_ENCODED + index + }, true, EFFECTIVE_DATE); + } + + Field cacheField = ChainVerifier.class.getDeclaredField("verifiedPublicKeyCache"); + cacheField.setAccessible(true); + Map cache = (Map) cacheField.get(mockedChainVerifier); + + Assertions.assertEquals(32, cache.size()); + } + @Test public void testOcspResponseCachingWhenEntryIsRemovedDuringLookup() throws Exception { String[] chain = new String[] {