Skip to content

Commit

Permalink
Trusted entitlements: Use new signature verification format (#1111)
Browse files Browse the repository at this point in the history
### Description
Third PR for SDK-3200

- Adds support to the new signature format (salt + nonce + TS + etag +
content)
- Adds support for intermediate signatures verification
- Makes nonce optional in preparation of static endpoint signing.

Based on #1109 and #1110.
  • Loading branch information
tonidero committed Jul 6, 2023
1 parent ef34062 commit 04fb5f8
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 {
Expand All @@ -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?,
Expand All @@ -43,39 +41,56 @@ 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
}
if (requestTime == null) {
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
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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())
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -192,7 +192,6 @@ internal class HTTPClientVerificationTest: BaseHTTPClientTest() {
verify(exactly = 1) {
mockSigningManager.verifyResponse(
endpoint.getPath(),
responseCode,
"test-signature",
"test-nonce",
"{\"test-key\":\"test-value\"}",
Expand Down Expand Up @@ -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())
}
}

Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading

0 comments on commit 04fb5f8

Please sign in to comment.