diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt index fc964a8354..9b4be914f9 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt @@ -175,10 +175,9 @@ internal class HTTPClient( } val verificationResult = if (shouldSignResponse && - nonce != null && RCHTTPStatusCodes.isSuccessful(responseCode) ) { - verifyResponse(path, responseCode, connection, payload, nonce) + verifyResponse(path, connection, payload, nonce) } else { VerificationResult.NOT_REQUESTED } @@ -278,15 +277,13 @@ internal class HTTPClient( private fun verifyResponse( urlPath: String, - responseCode: Int, connection: URLConnection, payload: String?, - nonce: String, + nonce: String?, ): VerificationResult { return signingManager.verifyResponse( urlPath = urlPath, - responseCode = responseCode, - signature = connection.getHeaderField(HTTPResult.SIGNATURE_HEADER_NAME), + signatureString = connection.getHeaderField(HTTPResult.SIGNATURE_HEADER_NAME), nonce = nonce, body = payload, requestTime = getRequestTimeHeader(connection), diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SignatureVerificationMode.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SignatureVerificationMode.kt index 1ccc008268..a97aa2dbec 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SignatureVerificationMode.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SignatureVerificationMode.kt @@ -6,21 +6,29 @@ internal sealed class SignatureVerificationMode { companion object { fun fromEntitlementVerificationMode( verificationMode: EntitlementVerificationMode, - signatureVerifier: SignatureVerifier? = null, + rootVerifier: SignatureVerifier? = null, ): SignatureVerificationMode { return when (verificationMode) { EntitlementVerificationMode.DISABLED -> Disabled EntitlementVerificationMode.INFORMATIONAL -> - Informational(signatureVerifier ?: DefaultSignatureVerifier()) + Informational(IntermediateSignatureHelper(rootVerifier ?: DefaultSignatureVerifier())) // Hidden ENFORCED mode during feature beta // EntitlementVerificationMode.ENFORCED -> // Enforced(signatureVerifier ?: DefaultSignatureVerifier()) } } + + private fun createIntermediateSignatureHelper(): IntermediateSignatureHelper { + return IntermediateSignatureHelper(DefaultSignatureVerifier()) + } } object Disabled : SignatureVerificationMode() - data class Informational(val signatureVerifier: SignatureVerifier) : SignatureVerificationMode() - data class Enforced(val signatureVerifier: SignatureVerifier) : SignatureVerificationMode() + data class Informational( + override val intermediateSignatureHelper: IntermediateSignatureHelper = createIntermediateSignatureHelper(), + ) : SignatureVerificationMode() + data class Enforced( + override val intermediateSignatureHelper: IntermediateSignatureHelper = createIntermediateSignatureHelper(), + ) : SignatureVerificationMode() val shouldVerify: Boolean get() = when (this) { @@ -32,10 +40,10 @@ internal sealed class SignatureVerificationMode { true } - val verifier: SignatureVerifier? + open val intermediateSignatureHelper: IntermediateSignatureHelper? get() = when (this) { is Disabled -> null - is Informational -> signatureVerifier - is Enforced -> signatureVerifier + is Informational -> intermediateSignatureHelper + is Enforced -> intermediateSignatureHelper } } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SigningManager.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SigningManager.kt index 74195bbdf4..462629ac9d 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SigningManager.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/verification/SigningManager.kt @@ -5,9 +5,9 @@ import com.revenuecat.purchases.VerificationResult import com.revenuecat.purchases.common.AppConfig import com.revenuecat.purchases.common.errorLog import com.revenuecat.purchases.common.networking.Endpoint -import com.revenuecat.purchases.common.networking.RCHTTPStatusCodes import com.revenuecat.purchases.common.warnLog import com.revenuecat.purchases.strings.NetworkStrings +import com.revenuecat.purchases.utils.Result import java.security.SecureRandom internal class SigningManager( @@ -16,7 +16,6 @@ internal class SigningManager( ) { private companion object { const val NONCE_BYTES_SIZE = 12 - const val SALT_BYTES_SIZE = 16 } fun shouldVerifyEndpoint(endpoint: Endpoint): Boolean { @@ -29,12 +28,11 @@ internal class SigningManager( return String(Base64.encode(bytes, Base64.DEFAULT)) } - @Suppress("LongParameterList", "ReturnCount") + @Suppress("LongParameterList", "ReturnCount", "CyclomaticComplexMethod") fun verifyResponse( urlPath: String, - responseCode: Int, - signature: String?, - nonce: String, + signatureString: String?, + nonce: String?, body: String?, requestTime: String?, eTag: String?, @@ -43,9 +41,10 @@ internal class SigningManager( warnLog("Forcing signing error for request with path: $urlPath") return VerificationResult.FAILED } - val signatureVerifier = signatureVerificationMode.verifier ?: return VerificationResult.NOT_REQUESTED + val intermediateSignatureHelper = signatureVerificationMode.intermediateSignatureHelper + ?: return VerificationResult.NOT_REQUESTED - if (signature == null) { + if (signatureString == null) { errorLog(NetworkStrings.VERIFICATION_MISSING_SIGNATURE.format(urlPath)) return VerificationResult.FAILED } @@ -53,29 +52,45 @@ internal class SigningManager( errorLog(NetworkStrings.VERIFICATION_MISSING_REQUEST_TIME.format(urlPath)) return VerificationResult.FAILED } - - val signatureMessage = getSignatureMessage(responseCode, body, eTag) - if (signatureMessage == null) { + if (body == null && eTag == null) { errorLog(NetworkStrings.VERIFICATION_MISSING_BODY_OR_ETAG.format(urlPath)) return VerificationResult.FAILED } - val decodedNonce = Base64.decode(nonce, Base64.DEFAULT) - val decodedSignature = Base64.decode(signature, Base64.DEFAULT) - val saltBytes = decodedSignature.copyOfRange(0, SALT_BYTES_SIZE) - val signatureToVerify = decodedSignature.copyOfRange(SALT_BYTES_SIZE, decodedSignature.size) - val messageToVerify = saltBytes + decodedNonce + requestTime.toByteArray() + signatureMessage.toByteArray() - val verificationResult = signatureVerifier.verify(signatureToVerify, messageToVerify) - - return if (verificationResult) { - VerificationResult.VERIFIED - } else { - errorLog(NetworkStrings.VERIFICATION_ERROR.format(urlPath)) - VerificationResult.FAILED + val signature: Signature + try { + signature = Signature.fromString(signatureString) + } catch (e: InvalidSignatureSizeException) { + errorLog(NetworkStrings.VERIFICATION_INVALID_SIZE.format(urlPath, e.message)) + return VerificationResult.FAILED } - } - private fun getSignatureMessage(responseCode: Int, body: String?, eTag: String?): String? { - return if (responseCode == RCHTTPStatusCodes.NOT_MODIFIED) eTag else body + when (val result = intermediateSignatureHelper.createIntermediateKeyVerifierIfVerified(signature)) { + is Result.Error -> { + errorLog( + NetworkStrings.VERIFICATION_INTERMEDIATE_KEY_FAILED.format( + urlPath, + result.value.underlyingErrorMessage, + ), + ) + return VerificationResult.FAILED + } + is Result.Success -> { + val intermediateKeyVerifier = result.value + val decodedNonce = nonce?.let { Base64.decode(it, Base64.DEFAULT) } ?: byteArrayOf() + val requestTimeBytes = requestTime.toByteArray() + val eTagBytes = eTag?.toByteArray() ?: byteArrayOf() + val payloadBytes = body?.toByteArray() ?: byteArrayOf() + val messageToVerify = signature.salt + decodedNonce + requestTimeBytes + eTagBytes + payloadBytes + val verificationResult = intermediateKeyVerifier.verify(signature.payload, messageToVerify) + + return if (verificationResult) { + VerificationResult.VERIFIED + } else { + errorLog(NetworkStrings.VERIFICATION_ERROR.format(urlPath)) + VerificationResult.FAILED + } + } + } } } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/strings/NetworkStrings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/strings/NetworkStrings.kt index 2c1e5c1168..58d2c650e6 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/strings/NetworkStrings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/strings/NetworkStrings.kt @@ -11,9 +11,12 @@ internal object NetworkStrings { const val SAME_CALL_ALREADY_IN_PROGRESS = "Same call already in progress, adding to callbacks map with key: %s" const val PROBLEM_CONNECTING = "Unable to start a network connection due to a network configuration issue: %s" const val VERIFICATION_MISSING_SIGNATURE = "Verification: Request to '%s' requires a signature but none provided." + const val VERIFICATION_INTERMEDIATE_KEY_FAILED = "Verification: Request to '%s' provided an intermediate key that" + + " did not verify correctly. Reason %s" const val VERIFICATION_MISSING_REQUEST_TIME = "Verification: Request to '%s' requires a request time" + " but none provided." const val VERIFICATION_MISSING_BODY_OR_ETAG = "Verification: Request to '%s' requires a body or etag" + " but none provided." + const val VERIFICATION_INVALID_SIZE = "Verification: Request to '%s' has signature with wrong size. '%s'" const val VERIFICATION_ERROR = "Verification: Request to '%s' failed verification." } diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientVerificationTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientVerificationTest.kt index daf4be7c32..6dcdfafc52 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientVerificationTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/HTTPClientVerificationTest.kt @@ -41,7 +41,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { ) every { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } returns VerificationResult.VERIFIED client.performRequest( @@ -81,7 +81,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { assertThat(result.verificationResult).isEqualTo(VerificationResult.NOT_REQUESTED) verify(exactly = 0) { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } } @@ -112,7 +112,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { assertThat(result.verificationResult).isEqualTo(VerificationResult.NOT_REQUESTED) verify(exactly = 0) { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } } @@ -143,7 +143,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { assertThat(result.verificationResult).isEqualTo(VerificationResult.NOT_REQUESTED) verify(exactly = 0) { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } } @@ -157,7 +157,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { val responseCode = expectedResult.responseCode every { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } returns VerificationResult.VERIFIED every { @@ -192,7 +192,6 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { verify(exactly = 1) { mockSigningManager.verifyResponse( endpoint.getPath(), - responseCode, "test-signature", "test-nonce", "{\"test-key\":\"test-value\"}", @@ -222,7 +221,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { server.takeRequest() assertThat(result.verificationResult).isEqualTo(VerificationResult.NOT_REQUESTED) verify(exactly = 0) { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } } @@ -236,7 +235,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { ) every { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } returns VerificationResult.FAILED val result = client.performRequest( @@ -261,7 +260,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { ) every { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } returns VerificationResult.FAILED var thrownCorrectException = false @@ -292,7 +291,7 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() { ) every { - mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any(), any()) + mockSigningManager.verifyResponse(any(), any(), any(), any(), any(), any()) } returns VerificationResult.VERIFIED val result = client.performRequest( diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/verification/SignatureVerificationModeTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/verification/SignatureVerificationModeTest.kt index 242eacc42e..c7f2fe6832 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/verification/SignatureVerificationModeTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/verification/SignatureVerificationModeTest.kt @@ -27,9 +27,18 @@ class SignatureVerificationModeTest { @Test fun `shouldVerify has correct values for all the verification modes`() { - val signatureVerifier = DefaultSignatureVerifier() assertThat(SignatureVerificationMode.Disabled.shouldVerify).isFalse - assertThat(SignatureVerificationMode.Informational(signatureVerifier).shouldVerify).isTrue - assertThat(SignatureVerificationMode.Enforced(signatureVerifier).shouldVerify).isTrue + assertThat(SignatureVerificationMode.Informational().shouldVerify).isTrue + assertThat(SignatureVerificationMode.Enforced().shouldVerify).isTrue + } + + @Test + fun `intermediateSignatureHelper has values in enabled verification modes`() { + var verificationMode: SignatureVerificationMode = SignatureVerificationMode.Disabled + assertThat(verificationMode.intermediateSignatureHelper).isNull() + verificationMode = SignatureVerificationMode.Informational() + assertThat(verificationMode.intermediateSignatureHelper).isNotNull + verificationMode = SignatureVerificationMode.Enforced() + assertThat(verificationMode.intermediateSignatureHelper).isNotNull } } diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/verification/SigningManagerTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/verification/SigningManagerTest.kt index f5ca5e7676..35b750c7be 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/verification/SigningManagerTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/verification/SigningManagerTest.kt @@ -2,10 +2,12 @@ package com.revenuecat.purchases.common.verification import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.PurchasesError +import com.revenuecat.purchases.PurchasesErrorCode import com.revenuecat.purchases.VerificationResult import com.revenuecat.purchases.common.AppConfig import com.revenuecat.purchases.common.networking.Endpoint -import com.revenuecat.purchases.common.networking.RCHTTPStatusCodes +import com.revenuecat.purchases.utils.Result import io.mockk.every import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat @@ -17,8 +19,9 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(manifest = Config.NONE) class SigningManagerTest { - private lateinit var verifier: SignatureVerifier + private lateinit var intermediateKeyVerifier: SignatureVerifier private lateinit var appConfig: AppConfig + private lateinit var intermediateSignatureHelper: IntermediateSignatureHelper private lateinit var disabledSigningManager: SigningManager private lateinit var informationalSigningManager: SigningManager @@ -26,14 +29,21 @@ class SigningManagerTest { @Before fun setUp() { - verifier = mockk() appConfig = mockk().apply { every { forceSigningErrors } returns false } + intermediateKeyVerifier = mockk() + intermediateSignatureHelper = mockk().apply { + every { createIntermediateKeyVerifierIfVerified(any()) } returns Result.Success(intermediateKeyVerifier) + } disabledSigningManager = SigningManager(SignatureVerificationMode.Disabled, appConfig) - informationalSigningManager = SigningManager(SignatureVerificationMode.Informational(verifier), appConfig) - enforcedSigningManager = SigningManager(SignatureVerificationMode.Enforced(verifier), appConfig) + informationalSigningManager = SigningManager( + SignatureVerificationMode.Informational(intermediateSignatureHelper), appConfig + ) + enforcedSigningManager = SigningManager( + SignatureVerificationMode.Enforced(intermediateSignatureHelper), appConfig + ) } // region shouldVerifyEndpoint @@ -126,18 +136,26 @@ class SigningManagerTest { fun `verifyResponse returns error if status code not modified and etag is empty`() { val verificationResult = callVerifyResponse( informationalSigningManager, - responseCode = RCHTTPStatusCodes.NOT_MODIFIED, + body = null, eTag = null ) assertThat(verificationResult).isEqualTo(VerificationResult.FAILED) } @Test - fun `verifyResponse returns success if verifier returns success for not modified `() { - every { verifier.verify(any(), any()) } returns true + fun `verifyResponse returns error if failed to verify intermediate key`() { + every { + intermediateSignatureHelper.createIntermediateKeyVerifierIfVerified(any()) + } returns Result.Error(PurchasesError(PurchasesErrorCode.SignatureVerificationError)) + val verificationResult = callVerifyResponse(informationalSigningManager) + assertThat(verificationResult).isEqualTo(VerificationResult.FAILED) + } + + @Test + fun `verifyResponse returns success if intermediate key verifier returns success for not modified `() { + every { intermediateKeyVerifier.verify(any(), any()) } returns true val verificationResult = callVerifyResponse( informationalSigningManager, - responseCode = RCHTTPStatusCodes.NOT_MODIFIED, body = null, eTag = "test-etag" ) @@ -145,34 +163,51 @@ class SigningManagerTest { } @Test - fun `verifyResponse returns success if verifier returns success for given parameters`() { - every { verifier.verify(any(), any()) } returns true + fun `verifyResponse returns success if intermediate key verifier returns success for given parameters`() { + every { intermediateKeyVerifier.verify(any(), any()) } returns true val verificationResult = callVerifyResponse(informationalSigningManager) assertThat(verificationResult).isEqualTo(VerificationResult.VERIFIED) } @Test - fun `verifyResponse returns error if verifier returns success for given parameters`() { - every { verifier.verify(any(), any()) } returns false + fun `verifyResponse returns error if intermediate key verifier returns error for given parameters`() { + every { intermediateKeyVerifier.verify(any(), any()) } returns false val verificationResult = callVerifyResponse(informationalSigningManager) assertThat(verificationResult).isEqualTo(VerificationResult.FAILED) } @Test fun `verifyResponse with real data verifies correctly`() { + val rootVerifier = DefaultSignatureVerifier("yg2wZGAr8Af+Unt9RImQDbL7qA81txk+ga0I+ylmcyo=") val signingManager = SigningManager( - SignatureVerificationMode.Informational(DefaultSignatureVerifier()), - appConfig + SignatureVerificationMode.Informational(IntermediateSignatureHelper(rootVerifier)), + appConfig, ) val verificationResult = callVerifyResponse(signingManager) assertThat(verificationResult).isEqualTo(VerificationResult.VERIFIED) } + @Test + fun `verifyResponse with both payload and etag verifies correctly`() { + val rootVerifier = DefaultSignatureVerifier("yg2wZGAr8Af+Unt9RImQDbL7qA81txk+ga0I+ylmcyo=") + val signingManager = SigningManager( + SignatureVerificationMode.Informational(IntermediateSignatureHelper(rootVerifier)), + appConfig, + ) + val verificationResult = callVerifyResponse( + signingManager, + signature = "xoDYyUeHnIlSIAeOOzmvdNPOlbNSKK+xE0fE/ufS1fsKCgoKkY+e/hYWiSW5cV6pVpp0i3Ag1p/wH4CcPnDSuG4qzPW8l582Q3gE9j5pIG3XrYxpblHCxBnfcBsxNriK0awxAVBTK1hPk60bAeilLRVB2Qwj2phUfQKOqgKxxUh1XHQlJJqUOiAUdv35dB3tmtPWH//MHO/V7mD72P+kwqecZgDxEZxgbe68VczwjIkSPo4F", + eTag = "test-etag", + ) + assertThat(verificationResult).isEqualTo(VerificationResult.VERIFIED) + } + @Suppress("MaxLineLength") @Test fun `verifyResponse with slightly different data does not verify correctly`() { + val rootVerifier = DefaultSignatureVerifier("yg2wZGAr8Af+Unt9RImQDbL7qA81txk+ga0I+ylmcyo=") val signingManager = SigningManager( - SignatureVerificationMode.Informational(DefaultSignatureVerifier()), + SignatureVerificationMode.Informational(IntermediateSignatureHelper(rootVerifier)), appConfig, ) assertThat( @@ -191,7 +226,7 @@ class SigningManagerTest { @Test fun `verifyResponse returns success for enforced mode if verifier returns success for given parameters`() { - every { verifier.verify(any(), any()) } returns true + every { intermediateKeyVerifier.verify(any(), any()) } returns true val verificationResult = callVerifyResponse(enforcedSigningManager) assertThat(verificationResult).isEqualTo(VerificationResult.VERIFIED) } @@ -203,13 +238,17 @@ class SigningManagerTest { private fun callVerifyResponse( signingManager: SigningManager, requestPath: String = "test-url-path", - responseCode: Int = RCHTTPStatusCodes.SUCCESS, - signature: String? = "2bm3QppRywK5ULyCRLS5JJy9sq+84IkMk0Ue4LsywEp87t0tDObpzPlu30l4Desq9X65UFuosqwCLMizruDHbKvPqQLce0hrIuZpgic+cQ8=", + // Generated signature using test keys: + // Test root private key: YMHMQMpepBKamtSzO8KCN2M8Z3AUW5R1JXIFtxUWFUI + // Test root public key: yg2wZGAr8Af+Unt9RImQDbL7qA81txk+ga0I+ylmcyo= + // Test intermediate private key: fPBoIjQ7DecE89ATW6PZsqLVQNyEs5fiX3sUyS3U4YI + // Test intermediate public key: xoDYyUeHnIlSIAeOOzmvdNPOlbNSKK+xE0fE/ufS1fs= + signature: String? = "xoDYyUeHnIlSIAeOOzmvdNPOlbNSKK+xE0fE/ufS1fsKCgoKkY+e/hYWiSW5cV6pVpp0i3Ag1p/wH4CcPnDSuG4qzPW8l582Q3gE9j5pIG3XrYxpblHCxBnfcBsxNriK0awxAd8tsYi90CIUqiJXxN+/9Z4xLNME00pcLbsXFO0GqNVPkYgGgSJWEd/xAIiXQBypugaAb17y4u0xpjcyS5JRFXuLJCD4CGMZtmqWewWuuQgC", nonce: String = "MTIzNDU2Nzg5MGFi", body: String? = "{\"request_date\":\"2023-02-21T18:58:36Z\",\"request_date_ms\":1677005916011,\"subscriber\":{\"entitlements\":{},\"first_seen\":\"2023-02-21T18:58:35Z\",\"last_seen\":\"2023-02-21T18:58:35Z\",\"management_url\":null,\"non_subscriptions\":{},\"original_app_user_id\":\"login\",\"original_application_version\":null,\"original_purchase_date\":null,\"other_purchases\":{},\"subscriptions\":{}}}\n", requestTime: String? = "1677005916012", eTag: String? = null - ) = signingManager.verifyResponse(requestPath, responseCode, signature, nonce, body, requestTime, eTag) + ) = signingManager.verifyResponse(requestPath, signature, nonce, body, requestTime, eTag) // endregion }