Skip to content

Commit 3e35344

Browse files
Merge pull request #80 from alexanderjordanbaker/VerifiedChainCaching
Add verified chain caching
2 parents 590c6d5 + eb4f9e7 commit 3e35344

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

Sources/AppStoreServerLibrary/ChainVerifier.swift

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@ import Crypto
88
import AsyncHTTPClient
99
import NIOFoundationCompat
1010

11-
struct ChainVerifier {
11+
class ChainVerifier {
1212

1313
private static let EXPECTED_CHAIN_LENGTH = 3
1414
private static let EXPECTED_JWT_SEGMENTS = 3
1515
private static let EXPECTED_ALGORITHM = "ES256"
1616

17+
private static let MAXIMUM_CACHE_SIZE = 32 // There are unlikely to be more than a couple keys at once
18+
private static let CACHE_TIME_LIMIT: Int64 = 15 * 60 // 15 minutes in seconds
19+
1720
private let store: CertificateStore
1821
private let requester: Requester
22+
private var verifiedPublicKeyCache: [CacheKey: CacheValue]
1923

2024
init(rootCertificates: [Data]) throws {
2125
let parsedCertificates = try rootCertificates.map { try Certificate(derEncoded: [UInt8]($0)) }
2226
self.store = CertificateStore(parsedCertificates)
2327
self.requester = Requester()
28+
self.verifiedPublicKeyCache = [:]
2429
}
2530

2631
func verify<T: DecodedSignedData>(signedData: String, type: T.Type, onlineVerification: Bool, environment: AppStoreEnvironment) async -> VerificationResult<T> where T: Decodable {
@@ -62,7 +67,7 @@ struct ChainVerifier {
6267
do {
6368
let leafCertificate = try Certificate(derEncoded: Array(leaf_der_enocded))
6469
let intermediateCertificate = try Certificate(derEncoded: Array(intermeidate_der_encoded))
65-
let validationTime = !onlineVerification && decodedBody.signedDate != nil ? decodedBody.signedDate! : Date()
70+
let validationTime = !onlineVerification && decodedBody.signedDate != nil ? decodedBody.signedDate! : getDate()
6671

6772
let verificationResult = await verifyChain(leaf: leafCertificate, intermediate: intermediateCertificate, online: onlineVerification, validationTime: validationTime)
6873
switch verificationResult {
@@ -90,16 +95,56 @@ struct ChainVerifier {
9095
}
9196

9297
func verifyChain(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
98+
if online {
99+
if let cachedResult = verifiedPublicKeyCache[CacheKey(leaf: leaf, intermediate: intermediate)] {
100+
if cachedResult.expirationTime > getDate() {
101+
return cachedResult.publicKey
102+
}
103+
}
104+
}
105+
let verificationResult = await verifyChainWithoutCaching(leaf: leaf, intermediate: intermediate, online: online, validationTime: validationTime)
106+
107+
if online {
108+
if case let .validCertificate(verifiedChain) = verificationResult {
109+
verifiedPublicKeyCache[CacheKey(leaf: leaf, intermediate: intermediate)] = CacheValue(expirationTime: getDate().addingTimeInterval(TimeInterval(integerLiteral: ChainVerifier.CACHE_TIME_LIMIT)), publicKey: verificationResult)
110+
if verifiedPublicKeyCache.count > ChainVerifier.MAXIMUM_CACHE_SIZE {
111+
for kv in verifiedPublicKeyCache {
112+
if kv.value.expirationTime < getDate() {
113+
verifiedPublicKeyCache.removeValue(forKey: kv.key)
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
return verificationResult
121+
}
122+
123+
func verifyChainWithoutCaching(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
93124
var verifier = Verifier(rootCertificates: self.store) {
94125
RFC5280Policy(validationTime: validationTime)
95126
AppStoreOIDPolicy()
96127
if online {
97-
OCSPVerifierPolicy(failureMode: .hard, requester: requester, validationTime: Date())
128+
OCSPVerifierPolicy(failureMode: .hard, requester: requester, validationTime: getDate())
98129
}
99130
}
100131
let intermediateStore = CertificateStore([intermediate])
101132
return await verifier.validate(leafCertificate: leaf, intermediates: intermediateStore)
102133
}
134+
135+
func getDate() -> Date {
136+
return Date()
137+
}
138+
}
139+
140+
struct CacheKey: Hashable {
141+
let leaf: Certificate
142+
let intermediate: Certificate
143+
}
144+
145+
struct CacheValue {
146+
let expirationTime: Date
147+
let publicKey: X509.VerificationResult
103148
}
104149

105150
struct VaporBody : JWTPayload {

Tests/AppStoreServerLibraryTests/SignedDataVerifierTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ final class SignedDataVerifierTests: XCTestCase {
2222
private var REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED = "MIIEMDCCA7agAwIBAgIQaPoPldvpSoEH0lBrjDPv9jAKBggqhkjOPQQDAzB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTIxMDgyNTAyNTAzNFoXDTIzMDkyNDAyNTAzM1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOoTcaPcpeipNL9eQ06tCu7pUcwdCXdN8vGqaUjd58Z8tLxiUC0dBeA+euMYggh1/5iAk+FMxUFmA2a1r4aCZ8SjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig97bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFCOCmMBq//1L5imvVmqX1oCYeqrMMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNoADBlAjEAl4JB9GJHixP2nuibyU1k3wri5psGIxPME05sFKq7hQuzvbeyBu82FozzxmbzpogoAjBLSFl0dZWIYl2ejPV+Di5fBnKPu8mymBQtoE/H2bES0qAs8bNueU3CBjjh1lwnDsI="
2323

2424
private var EFFECTIVE_DATE: Date = Date(timeIntervalSince1970: TimeInterval(1681312846)); // April 2023
25+
private let CLOCK_DATE: Int64 = 41231
2526

2627
func testValidChainWithoutOCSP() async throws {
2728
let verifier: ChainVerifier = getChainVerifier(base64EncodedRootCertificate: ROOT_CA_BASE64_ENCODED)
@@ -85,6 +86,43 @@ final class SignedDataVerifierTests: XCTestCase {
8586
}
8687
}
8788

89+
func testOcspResponseCaching() async throws {
90+
let verifier: DateOverrideChainVerifier = DateOverrideChainVerifier(expectedCalls: 1, currentDate: CLOCK_DATE, base64EncodedRootCertificate: ROOT_CA_BASE64_ENCODED)!
91+
let leaf = try! Certificate(derEncoded: Array(Data(base64Encoded: LEAF_CERT_BASE64_ENCODED)!))
92+
let intermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: INTERMEDIATE_CA_BASE64_ENCODED)!))
93+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
94+
verifier.setDate(newDate: CLOCK_DATE + 1) // 1 second
95+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
96+
}
97+
98+
func testOcspResponseCachingHasExpiration() async throws {
99+
let verifier: DateOverrideChainVerifier = DateOverrideChainVerifier(expectedCalls: 2, currentDate: CLOCK_DATE, base64EncodedRootCertificate: ROOT_CA_BASE64_ENCODED)!
100+
let leaf = try! Certificate(derEncoded: Array(Data(base64Encoded: LEAF_CERT_BASE64_ENCODED)!))
101+
let intermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: INTERMEDIATE_CA_BASE64_ENCODED)!))
102+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
103+
verifier.setDate(newDate: CLOCK_DATE + 900) // 15 minutes
104+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
105+
}
106+
107+
func testOcspResponseCachingWithDifferentChains() async throws {
108+
let verifier: DateOverrideChainVerifier = DateOverrideChainVerifier(expectedCalls: 2, currentDate: CLOCK_DATE, base64EncodedRootCertificate: ROOT_CA_BASE64_ENCODED)!
109+
let leaf = try! Certificate(derEncoded: Array(Data(base64Encoded: LEAF_CERT_BASE64_ENCODED)!))
110+
let intermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: INTERMEDIATE_CA_BASE64_ENCODED)!))
111+
let altLeaf = try! Certificate(derEncoded: Array(Data(base64Encoded: LEAF_CERT_BASE64_ENCODED)!))
112+
let altIntermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: REAL_APPLE_INTERMEDIATE_BASE64_ENCODED)!))
113+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
114+
let _ = await verifier.verifyChain(leaf: altLeaf, intermediate: altIntermediate, online: true, validationTime: EFFECTIVE_DATE)
115+
}
116+
117+
func testOcspResponseCachingWithSlightlyDifferentChains() async throws {
118+
let verifier: DateOverrideChainVerifier = DateOverrideChainVerifier(expectedCalls: 2, currentDate: CLOCK_DATE, base64EncodedRootCertificate: ROOT_CA_BASE64_ENCODED)!
119+
let leaf = try! Certificate(derEncoded: Array(Data(base64Encoded: LEAF_CERT_BASE64_ENCODED)!))
120+
let intermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: INTERMEDIATE_CA_BASE64_ENCODED)!))
121+
let altIntermediate = try! Certificate(derEncoded: Array(Data(base64Encoded: REAL_APPLE_INTERMEDIATE_BASE64_ENCODED)!))
122+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: intermediate, online: true, validationTime: EFFECTIVE_DATE)
123+
let _ = await verifier.verifyChain(leaf: leaf, intermediate: altIntermediate, online: true, validationTime: EFFECTIVE_DATE)
124+
}
125+
88126
// The following test will communicate with Apple's OCSP servers, disable this test for offline testing
89127
func testAppleChainIsValidWithOCSP() async throws {
90128
let verifier: ChainVerifier = getChainVerifier(base64EncodedRootCertificate: REAL_APPLE_ROOT_BASE64_ENCODED)
@@ -216,4 +254,30 @@ final class SignedDataVerifierTests: XCTestCase {
216254
private func getChainVerifier(base64EncodedRootCertificate: String) -> ChainVerifier {
217255
return try! ChainVerifier(rootCertificates: [Data(base64Encoded: base64EncodedRootCertificate)!])
218256
}
257+
258+
class DateOverrideChainVerifier: ChainVerifier {
259+
var currentDate: Int64
260+
var expectation : XCTestExpectation
261+
262+
init?(expectedCalls: Int, currentDate: Int64, base64EncodedRootCertificate: String) {
263+
self.currentDate = currentDate
264+
self.expectation = XCTestExpectation()
265+
self.expectation.assertForOverFulfill = true
266+
self.expectation.expectedFulfillmentCount = expectedCalls
267+
try? super.init(rootCertificates: [Data(base64Encoded: base64EncodedRootCertificate)!])
268+
}
269+
270+
func setDate(newDate: Int64) {
271+
self.currentDate = newDate
272+
}
273+
274+
override func verifyChainWithoutCaching(leaf: Certificate, intermediate: Certificate, online: Bool, validationTime: Date) async -> X509.VerificationResult {
275+
expectation.fulfill()
276+
return .validCertificate([])
277+
}
278+
279+
override func getDate() -> Date {
280+
return Date(timeIntervalSince1970: TimeInterval(currentDate))
281+
}
282+
}
219283
}

0 commit comments

Comments
 (0)