Skip to content

Commit ce3a80c

Browse files
Merge pull request #122 from alexanderjordanbaker/VerifiedChainCaching
Add verified chain caching to improve performance
2 parents c59ea47 + db1a1c3 commit ce3a80c

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

appstoreserverlibrary/signed_data_verifier.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
22

3-
from typing import List, Optional
3+
from typing import List, Optional, Dict
44
from base64 import b64decode
55
from enum import IntEnum
66
import time
@@ -155,11 +155,25 @@ def _decode_signed_object(self, signed_obj: str) -> dict:
155155
raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e
156156

157157
class _ChainVerifier:
158+
MAXIMUM_CACHE_SIZE = 32 # There are unlikely to be more than a couple keys at once
159+
CACHE_TIME_LIMIT = 15 * 60 # 15 minutes
160+
158161
def __init__(self, root_certificates: List[bytes], enable_strict_checks=True):
159162
self.enable_strict_checks = enable_strict_checks
160163
self.root_certificates = root_certificates
164+
self.verified_certificates_cache: Dict[tuple[str, ...], (str, int)] = {}
161165

162166
def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str:
167+
if perform_online_checks and len(certificates) > 0:
168+
cached_public_key = self.get_cached_public_key(certificates)
169+
if cached_public_key is not None:
170+
return cached_public_key
171+
verified_public_key = self._verify_chain_without_caching(certificates=certificates, perform_online_checks=perform_online_checks, effective_date=effective_date)
172+
if perform_online_checks:
173+
self.put_verified_public_key(certificates, verified_public_key)
174+
return verified_public_key
175+
176+
def _verify_chain_without_caching(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str:
163177
if len(self.root_certificates) == 0:
164178
raise VerificationException(VerificationStatus.INVALID_CERTIFICATE)
165179
if len(certificates) != 3:
@@ -295,7 +309,22 @@ def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto
295309
return
296310

297311
raise VerificationException(VerificationStatus.VERIFICATION_FAILURE)
312+
313+
def get_cached_public_key(self, certificates: List[str]) -> Optional[str]:
314+
verified_public_key = self.verified_certificates_cache.get(tuple(certificates))
315+
if verified_public_key is None:
316+
return None
317+
if verified_public_key[1] <= time.time():
318+
return None
319+
return verified_public_key[0]
298320

321+
def put_verified_public_key(self, certificates: List[str], verified_public_key: str):
322+
cache_expiration = time.time() + _ChainVerifier.CACHE_TIME_LIMIT
323+
self.verified_certificates_cache[tuple(certificates)] = (verified_public_key, cache_expiration)
324+
if len(self.verified_certificates_cache) > _ChainVerifier.MAXIMUM_CACHE_SIZE:
325+
for k, v in list(self.verified_certificates_cache.items()):
326+
if v[1] <= time.time():
327+
del self.verified_certificates_cache[k]
299328

300329
class VerificationStatus(IntEnum):
301330
OK = 0

tests/test_x509_verifiction.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
22

33
import unittest
4+
from unittest import mock
5+
from unittest.mock import MagicMock, patch
46

57
from appstoreserverlibrary.signed_data_verifier import _ChainVerifier, VerificationException, VerificationStatus
68
from base64 import b64decode, b64encode
@@ -20,6 +22,7 @@
2022
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED = "MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTIxMDgyNTAyNTAzNFoXDTIzMDkyNDAyNTAzM1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOoTcaPcpeipNL9eQ06tCu7pUcwdCXdN8vGqaUjd58Z8tLxiUC0dBeA+euMYggh1/5iAk+FMxUFmA2a1r4aCZ8SjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig97bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFCOCmMBq//1L5imvVmqX1oCYeqrMMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNoADBlAjEAl4JB9GJHixP2nuibyU1k3wri5psGIxPME05sFKq7hQuzvbeyBu82FozzxmbzpogoAjBLSFl0dZWIYl2ejPV+Di5fBnKPu8mymBQtoE/H2bES0qAs8bNueU3CBjjh1lwnDsI="
2123

2224
EFFECTIVE_DATE = 1681312846
25+
CLOCK_DATE = 41231
2326

2427
class X509Verification(unittest.TestCase):
2528
def test_valid_chain_without_ocsp(self):
@@ -115,6 +118,82 @@ def test_apple_chain_is_valid_with_ocsp_and_strict(self):
115118
REAL_APPLE_ROOT_BASE64_ENCODED
116119
], True, EFFECTIVE_DATE)
117120

121+
def test_ocsp_response_caching(self):
122+
verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])
123+
magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)
124+
verifier._verify_chain_without_caching = magic_mock
125+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):
126+
verifier.verify_chain([
127+
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,
128+
REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,
129+
REAL_APPLE_ROOT_BASE64_ENCODED
130+
], True, EFFECTIVE_DATE)
131+
self.assertEqual(1, magic_mock.call_count)
132+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 1)): # 1 second
133+
verifier.verify_chain([
134+
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,
135+
REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,
136+
REAL_APPLE_ROOT_BASE64_ENCODED
137+
], True, EFFECTIVE_DATE)
138+
self.assertEqual(1, magic_mock.call_count)
139+
140+
def test_ocsp_response_caching_has_expiration(self):
141+
verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])
142+
magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)
143+
verifier._verify_chain_without_caching = magic_mock
144+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):
145+
verifier.verify_chain([
146+
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,
147+
REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,
148+
REAL_APPLE_ROOT_BASE64_ENCODED
149+
], True, EFFECTIVE_DATE)
150+
self.assertEqual(1, magic_mock.call_count)
151+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 900)): # 15 minutes
152+
verifier.verify_chain([
153+
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,
154+
REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,
155+
REAL_APPLE_ROOT_BASE64_ENCODED
156+
], True, EFFECTIVE_DATE)
157+
self.assertEqual(2, magic_mock.call_count)
158+
159+
def test_ocsp_response_caching_with_different_chain(self):
160+
verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])
161+
magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)
162+
verifier._verify_chain_without_caching = magic_mock
163+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):
164+
verifier.verify_chain([
165+
LEAF_CERT_BASE64_ENCODED,
166+
INTERMEDIATE_CA_BASE64_ENCODED,
167+
ROOT_CA_BASE64_ENCODED
168+
], True, EFFECTIVE_DATE)
169+
self.assertEqual(1, magic_mock.call_count)
170+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same
171+
verifier.verify_chain([
172+
REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,
173+
REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,
174+
REAL_APPLE_ROOT_BASE64_ENCODED
175+
], True, EFFECTIVE_DATE)
176+
self.assertEqual(2, magic_mock.call_count)
177+
178+
def test_ocsp_response_caching_with_slightly_different_chain(self):
179+
verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])
180+
magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)
181+
verifier._verify_chain_without_caching = magic_mock
182+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):
183+
verifier.verify_chain([
184+
LEAF_CERT_BASE64_ENCODED,
185+
INTERMEDIATE_CA_BASE64_ENCODED,
186+
ROOT_CA_BASE64_ENCODED
187+
], True, EFFECTIVE_DATE)
188+
self.assertEqual(1, magic_mock.call_count)
189+
with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same
190+
verifier.verify_chain([
191+
LEAF_CERT_BASE64_ENCODED,
192+
INTERMEDIATE_CA_BASE64_ENCODED,
193+
REAL_APPLE_ROOT_BASE64_ENCODED
194+
], True, EFFECTIVE_DATE)
195+
self.assertEqual(2, magic_mock.call_count)
196+
118197

119198
if __name__ == '__main__':
120199
unittest.main()

0 commit comments

Comments
 (0)