diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index cf60a602b4..e1fcb1e93c 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -226,6 +226,7 @@ 4F6BEE3B2A27B45300CD9322 /* StoreKitTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571E7AD3279F2D0C003B3606 /* StoreKitTestHelpers.swift */; }; 4F6BEE3C2A27B45900CD9322 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE61A83264190830021CEA0 /* Constants.swift */; }; 4F6BEE882A27E16B00CD9322 /* TestLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57057FF728B0048900995F21 /* TestLogHandler.swift */; }; + 4F6EEBD92A38ED76007FD783 /* FakeSigning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6EEBD82A38ED76007FD783 /* FakeSigning.swift */; }; 4F7C37B22A27E2E8001E17D3 /* AsyncTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A8EE02922C56300936709 /* AsyncTestHelpers.swift */; }; 4F7C37E42A27EFE1001E17D3 /* BaseBackendIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579234E127F777EE00B39C68 /* BaseBackendIntegrationTests.swift */; }; 4F7C37E52A27EFF7001E17D3 /* BaseStoreKitIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5753EE0F294B93CC00CBAB54 /* BaseStoreKitIntegrationTests.swift */; }; @@ -926,6 +927,7 @@ 4F6BEDE12A26B69500CD9322 /* DebugContentViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugContentViews.swift; sourceTree = ""; }; 4F6BEE022A27ADF900CD9322 /* CustomEntitlementsComputationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEntitlementsComputationIntegrationTests.swift; sourceTree = ""; }; 4F6BEE312A27B02400CD9322 /* BackendCustomEntitlementsIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BackendCustomEntitlementsIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F6EEBD82A38ED76007FD783 /* FakeSigning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeSigning.swift; sourceTree = ""; }; 4F7DBFBC2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcher.swift; sourceTree = ""; }; 4F8038322A1EA7C300D21039 /* TransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPoster.swift; sourceTree = ""; }; 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfflineCustomerInfoCreator.swift; sourceTree = ""; }; @@ -2332,6 +2334,7 @@ 57E6C27B29723A94001AFE98 /* Signing.swift */, 5791FE492994453500F1FEDA /* Signing+ResponseVerification.swift */, 5740FCD22996CE5E00E049F9 /* VerificationResult.swift */, + 4F6EEBD82A38ED76007FD783 /* FakeSigning.swift */, ); path = Security; sourceTree = ""; @@ -3160,6 +3163,7 @@ 35D832F4262E606500E60AC5 /* HTTPResponse.swift in Sources */, 352B7D7927BD919B002A47DD /* DangerousSettings.swift in Sources */, A56F9AB126990E9200AFC48F /* CustomerInfo.swift in Sources */, + 4F6EEBD92A38ED76007FD783 /* FakeSigning.swift in Sources */, 2DDF41AE24F6F37C005BC22D /* InAppPurchase.swift in Sources */, B32B750126868C1D005647BF /* EntitlementInfo.swift in Sources */, 57ABA76D28F08DDA003D9181 /* Either.swift in Sources */, diff --git a/Sources/Logging/Strings/NetworkStrings.swift b/Sources/Logging/Strings/NetworkStrings.swift index a2706d7332..5d8fe18b4b 100644 --- a/Sources/Logging/Strings/NetworkStrings.swift +++ b/Sources/Logging/Strings/NetworkStrings.swift @@ -38,6 +38,7 @@ enum NetworkStrings { #if DEBUG case api_request_forcing_server_error(HTTPRequest) + case api_request_forcing_signature_failure(HTTPRequest) #endif } @@ -110,6 +111,9 @@ extension NetworkStrings: CustomStringConvertible { #if DEBUG case let .api_request_forcing_server_error(request): return "Returning fake HTTP 500 error for '\(request.description)'" + + case let .api_request_forcing_signature_failure(request): + return "Returning fake signature verification failure for '\(request.description)'" #endif } } diff --git a/Sources/Misc/DangerousSettings.swift b/Sources/Misc/DangerousSettings.swift index 6dae486f1c..05ea28075e 100644 --- a/Sources/Misc/DangerousSettings.swift +++ b/Sources/Misc/DangerousSettings.swift @@ -19,10 +19,16 @@ import Foundation #if DEBUG let forceServerErrors: Bool + let forceSignatureFailures: Bool - init(enableReceiptFetchRetry: Bool = false, forceServerErrors: Bool = false) { + init( + enableReceiptFetchRetry: Bool = false, + forceServerErrors: Bool = false, + forceSignatureFailures: Bool = false + ) { self.enableReceiptFetchRetry = enableReceiptFetchRetry self.forceServerErrors = forceServerErrors + self.forceSignatureFailures = forceSignatureFailures } #else init(enableReceiptFetchRetry: Bool = false) { @@ -107,6 +113,9 @@ internal protocol InternalDangerousSettingsType: Sendable { #if DEBUG /// Whether `HTTPClient` will fake server errors var forceServerErrors: Bool { get } + + /// Whether `HTTPClient` will fake invalid signatures. + var forceSignatureFailures: Bool { get } #endif } diff --git a/Sources/Networking/HTTPClient/HTTPClient.swift b/Sources/Networking/HTTPClient/HTTPClient.swift index 1735af23fb..22bf7247bc 100644 --- a/Sources/Networking/HTTPClient/HTTPClient.swift +++ b/Sources/Networking/HTTPClient/HTTPClient.swift @@ -276,7 +276,7 @@ private extension HTTPClient { .success(dataIfAvailable(statusCode)) .mapToResponse(response: httpURLResponse, request: request.httpRequest, - signing: self.signing, + signing: self.signing(for: request.httpRequest), verificationMode: request.verificationMode) .map { (response) -> HTTPResponse? in guard let cachedResponse = self.eTagManager.httpResultFromCacheOrBackend( @@ -408,6 +408,17 @@ private extension HTTPClient { } } + private func signing(for request: HTTPRequest) -> SigningType.Type { + #if DEBUG + if self.systemInfo.dangerousSettings.internalSettings.forceSignatureFailures { + Logger.warn(Strings.network.api_request_forcing_signature_failure(request)) + return FakeSigning.self + } + #endif + + return self.signing + } + } // MARK: - Extensions diff --git a/Sources/Security/FakeSigning.swift b/Sources/Security/FakeSigning.swift new file mode 100644 index 0000000000..27a041c4d0 --- /dev/null +++ b/Sources/Security/FakeSigning.swift @@ -0,0 +1,32 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// FakeSigning.swift +// +// Created by Nacho Soto on 6/13/23. + +import Foundation + +#if DEBUG + +/// A `SigningType` implementation that always fails, used for testing. +/// - Seealso: `InternalDangerousSettingsType.forceSignatureFailures` +final class FakeSigning: SigningType { + + static func verify( + signature: String, + with parameters: Signing.SignatureParameters, + publicKey: Signing.PublicKey + ) -> Bool { + return false + } + +} + +#endif diff --git a/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift b/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift index 1251f8ed15..df1680f7b6 100644 --- a/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift @@ -177,5 +177,6 @@ extension BaseBackendIntegrationTests: InternalDangerousSettingsType { var enableReceiptFetchRetry: Bool { return true } var forceServerErrors: Bool { return false } + var forceSignatureFailures: Bool { return false } } diff --git a/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift b/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift index 26ad8d5be0..053b31e700 100644 --- a/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift +++ b/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift @@ -565,25 +565,81 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat expect(response).to(beSuccess()) } -} + func testFakeSignatureFailuresInEnforcedMode() throws { + self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1) + MockSigning.stubbedVerificationResult = true -// MARK: - Private + try self.changeClientToEnforced(forceSignatureFailures: true) -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) -private extension BaseSignatureVerificationHTTPClientTests { + let response: HTTPResponse.Result? = waitUntilValue { completion in + self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) + } - final func changeClient(_ verificationMode: Configuration.EntitlementVerificationMode) throws { - try self.createClient(Signing.verificationMode(with: verificationMode)) + expect(response).to(beFailure()) + expect(response?.error) == NetworkError.signatureVerificationFailed(path: Self.path) } - final func changeClientToEnforced() throws { - try self.createClient(Signing.enforcedVerificationMode()) + func testFakeSignatureFailuresInInformationalMode() throws { + self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1) + MockSigning.stubbedVerificationResult = true + + try self.changeClient(.informational, forceSignatureFailures: true) + + let response: HTTPResponse.Result? = waitUntilValue { completion in + self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) + } + + expect(response).to(beSuccess()) + expect(response?.value?.verificationResult) == .failed } - private final func createClient(_ mode: Signing.ResponseVerificationMode) throws { - self.systemInfo = try MockSystemInfo(platformInfo: nil, - finishTransactions: false, - responseVerificationMode: mode) + func testFakeSignatureFailuresWithDisabledVerification() throws { + self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1) + MockSigning.stubbedVerificationResult = true + + try self.changeClient(.disabled, forceSignatureFailures: true) + + let response: HTTPResponse.Result? = waitUntilValue { completion in + self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) + } + + expect(response).to(beSuccess()) + expect(response?.value?.verificationResult) == .notRequested + } + +} + +// MARK: - Private + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) +private extension BaseSignatureVerificationHTTPClientTests { + + final func changeClient( + _ verificationMode: Configuration.EntitlementVerificationMode, + forceSignatureFailures: Bool = false + ) throws { + try self.createClient(Signing.verificationMode(with: verificationMode), + forceSignatureFailures: forceSignatureFailures) + } + + final func changeClientToEnforced(forceSignatureFailures: Bool = false) throws { + try self.createClient(Signing.enforcedVerificationMode(), + forceSignatureFailures: forceSignatureFailures) + } + + private final func createClient( + _ mode: Signing.ResponseVerificationMode, + forceSignatureFailures: Bool = false + ) throws { + self.systemInfo = try MockSystemInfo( + platformInfo: nil, + finishTransactions: false, + responseVerificationMode: mode, + dangerousSettings: .init( + autoSyncPurchases: true, + internalSettings: DangerousSettings.Internal(forceSignatureFailures: forceSignatureFailures) + ) + ) self.client = self.createClient() }