Accept bounded StoreKit signing clock skew

Allow legitimate App Store transaction timestamps to differ within the existing verification tolerance while preserving rejection beyond that boundary.
This commit is contained in:
Rocky
2026-08-19 18:26:54 +08:00
parent 75c046d91d
commit 2194e69bb8
2 changed files with 32 additions and 3 deletions
@@ -33,14 +33,16 @@ class StoreKitServiceTest : FunSpec({
accountToken: UUID = userId,
productId: String = product.productId,
revokedAt: Instant? = null,
purchasedAt: Instant = now.minusSeconds(10),
signedAt: Instant = now.minusSeconds(5),
) = VerifiedStoreKitTransaction(
transactionId = transactionId,
originalTransactionId = transactionId,
appAccountToken = accountToken,
productId = productId,
environment = StoreKitEnvironment.SANDBOX,
purchasedAt = now.minusSeconds(10),
signedAt = now.minusSeconds(5),
purchasedAt = purchasedAt,
signedAt = signedAt,
revokedAt = revokedAt,
)
@@ -115,6 +117,33 @@ class StoreKitServiceTest : FunSpec({
store.ledger shouldHaveSize 0
}
test("small App Store signing clock skew is accepted") {
val store = TestBillingStore()
val verified = transaction(
purchasedAt = now.minusSeconds(10),
signedAt = now.minusSeconds(70),
)
val result = service(store, verified).submit(userId, signedTransaction)
result.balanceAfter shouldBeExactly 3_000
store.ledger shouldHaveSize 1
}
test("App Store signing clock skew beyond the tolerance is rejected") {
val store = TestBillingStore()
val verified = transaction(
purchasedAt = now.minusSeconds(10),
signedAt = now.minusSeconds(311),
)
shouldThrow<StoreKitPurchaseConflict> {
service(store, verified).submit(userId, signedTransaction)
}
store.ledger shouldHaveSize 0
}
test("unknown or revoked products never grant credits") {
val unknownStore = TestBillingStore()
shouldThrow<StoreKitPurchaseConflict> {