From bcd5c1dc98ea8c89812d392a0c5a18eddc627daf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:23:11 +0000 Subject: [PATCH 1/2] build(deps): Bump androidGradlePlugin from 8.7.1 to 8.7.2 (#94) Bumps `androidGradlePlugin` from 8.7.1 to 8.7.2. Updates `com.android.tools.build:gradle` from 8.7.1 to 8.7.2 Updates `com.android.library` from 8.7.1 to 8.7.2 Updates `com.android.application` from 8.7.1 to 8.7.2 --- updated-dependencies: - dependency-name: com.android.tools.build:gradle dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.android.library dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.android.application dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe06a64..1f8dff6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ core-ktx = "1.13.1" junit = "5.11.3" appcompat = "1.7.0" -androidGradlePlugin = "8.7.1" +androidGradlePlugin = "8.7.2" kotlin = "2.0.21" serialization = "1.7.3" detekt-gradle = "1.23.7" # https://github.com/detekt/detekt/releases/tag/v1.23.6 From 0fb4ce9a77f8e2ae81714de041402a96bd3c18af Mon Sep 17 00:00:00 2001 From: Bianca Mihaila <137913588+BiancaMihaila@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:35:55 +0000 Subject: [PATCH 2/2] feat: Add functionality for mobile-backend/client-attestation call (#98) * feat: Add functionality for mobile-backend/client-attestation call - update AttestationCaller interface to enable use of firebase token and jwk - update jwk to a custom impl (JWK provided by jose4.jwt was not serializable) to enable use in the backend call - update FirebaseClientAttestationManager to add logic for the attestation call once firebase call is successful - update keystore manager to enable getting the public key in the required format * fix: Fix detekt issue in test file * style: Remove unused import * test: Amend tests with the correct impl * refactor: Amend AttestationCaller and AttestationResponse - simplify the result returned from the attestation caller - update attestation response to meet requirements from backend * style: Remove unnecessary lint supression --- .../FirebaseClientAttestationManagerTest.kt | 22 ++++++++++-- .../integrity/KeystoreManagerTest.kt | 29 ++++++++++++++- .../FirebaseClientAttestationManager.kt | 11 ++++-- .../integrity/KeystoreManager.kt | 15 ++++++++ .../integrity/model/AttestationResponse.kt | 13 ++++++- .../integrity/usecase/AttestationCaller.kt | 13 +++---- .../authentication/integrity/usecase/JWK.kt | 35 ++++++++++++------- .../integrity/usecase/JWKTest.kt | 12 +++++-- 8 files changed, 123 insertions(+), 27 deletions(-) diff --git a/app/src/androidTest/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManagerTest.kt b/app/src/androidTest/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManagerTest.kt index bf8dbfb..a6d4771 100644 --- a/app/src/androidTest/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManagerTest.kt +++ b/app/src/androidTest/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManagerTest.kt @@ -1,6 +1,7 @@ package uk.gov.android.authentication.integrity import kotlinx.coroutines.runBlocking +import org.mockito.kotlin.any import kotlin.test.BeforeTest import kotlin.test.Test import org.mockito.kotlin.mock @@ -33,14 +34,19 @@ class FirebaseClientAttestationManagerTest { fun check_success_response_from_get_attestation(): Unit = runBlocking { whenever(mockAppChecker.getAppCheckToken()) .thenReturn(Result.success(AppCheckToken("Success"))) + whenever(caller.call(any(), any())) + .thenReturn(AttestationResponse.Success( + "Success", + "0" + )) val result = clientAttestationManager.getAttestation() - assertEquals(AttestationResponse.Success("Success"), + assertEquals(AttestationResponse.Success("Success", "0"), result) } @Test - fun check_failure_response_from_get_attestation() = runBlocking { + fun check_failure_response_from_get_firebase_token() = runBlocking { whenever(mockAppChecker.getAppCheckToken()).thenReturn( Result.failure(Exception("Error")) ) @@ -50,6 +56,18 @@ class FirebaseClientAttestationManagerTest { (result as AttestationResponse.Failure).reason) } + @Test + fun check_failure_response_from_get_attestation() = runBlocking { + whenever(mockAppChecker.getAppCheckToken()) + .thenReturn(Result.success(AppCheckToken("Success"))) + whenever(caller.call(any(), any())) + .thenReturn(AttestationResponse.Failure("Error")) + val result = clientAttestationManager.getAttestation() + + assertEquals("Error", + (result as AttestationResponse.Failure).reason) + } + @Test fun check_failure_response_from_sign_attestation() = runBlocking { val result = clientAttestationManager.signAttestation("attestation") diff --git a/app/src/androidTest/java/uk/gov/android/authentication/integrity/KeystoreManagerTest.kt b/app/src/androidTest/java/uk/gov/android/authentication/integrity/KeystoreManagerTest.kt index 642506d..8567546 100644 --- a/app/src/androidTest/java/uk/gov/android/authentication/integrity/KeystoreManagerTest.kt +++ b/app/src/androidTest/java/uk/gov/android/authentication/integrity/KeystoreManagerTest.kt @@ -1,12 +1,15 @@ package uk.gov.android.authentication.integrity import android.security.keystore.KeyProperties -import kotlin.test.BeforeTest import java.security.KeyStore +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +@OptIn(ExperimentalEncodingApi::class) class KeystoreManagerTest { private lateinit var keyStore: KeyStore private lateinit var keystoreManager: KeystoreManager @@ -35,4 +38,28 @@ class KeystoreManagerTest { val privateKey = keystoreManager.appCheckPrivateKey assertEquals(KeyProperties.KEY_ALGORITHM_EC, privateKey.algorithm) } + + @Test + fun appCheckPublicKey() { + val publicKey = keystoreManager.appCheckPublicKey + assertEquals(KeyProperties.KEY_ALGORITHM_EC, publicKey.algorithm) + } + + @Test + fun check_getPubKeyBase64ECCoord() { + val actual = keystoreManager.getPubKeyBase64ECCoord() + + assertTrue(checkInputIsBase64(actual.first)) + assertTrue(checkInputIsBase64(actual.second)) + } + + @Suppress("SwallowedException") + private fun checkInputIsBase64(input: String): Boolean { + return try { + Base64.decode(input) + true + } catch (e: IllegalArgumentException) { + false + } + } } diff --git a/app/src/main/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManager.kt b/app/src/main/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManager.kt index 509f505..5e28f79 100644 --- a/app/src/main/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManager.kt +++ b/app/src/main/java/uk/gov/android/authentication/integrity/FirebaseClientAttestationManager.kt @@ -6,8 +6,10 @@ import uk.gov.android.authentication.integrity.model.AppIntegrityConfiguration import uk.gov.android.authentication.integrity.model.AttestationResponse import uk.gov.android.authentication.integrity.model.SignedResponse import uk.gov.android.authentication.integrity.usecase.AttestationCaller +import uk.gov.android.authentication.integrity.usecase.JWK +import kotlin.io.encoding.ExperimentalEncodingApi -@Suppress("UnusedPrivateProperty") +@OptIn(ExperimentalEncodingApi::class) class FirebaseClientAttestationManager( config: AppIntegrityConfiguration ) : ClientAttestationManager { @@ -21,8 +23,13 @@ class FirebaseClientAttestationManager( AttestationResponse.Failure(err.toString()) } // If successful -> functionality to get signed attestation form Mobile back-end + val pubKeyECCoord = keyManager.getPubKeyBase64ECCoord() + val jwk = JWK.makeJWK(x = pubKeyECCoord.first, y = pubKeyECCoord.second) return if (token is AppCheckToken) { - AttestationResponse.Success(token.jwtToken) + attestationCaller.call( + token.jwtToken, + jwk + ) // If unsuccessful -> return the failure } else { token as AttestationResponse.Failure diff --git a/app/src/main/java/uk/gov/android/authentication/integrity/KeystoreManager.kt b/app/src/main/java/uk/gov/android/authentication/integrity/KeystoreManager.kt index e3ba93e..f7e6d2a 100644 --- a/app/src/main/java/uk/gov/android/authentication/integrity/KeystoreManager.kt +++ b/app/src/main/java/uk/gov/android/authentication/integrity/KeystoreManager.kt @@ -6,7 +6,11 @@ import android.util.Log import java.security.KeyPairGenerator import java.security.KeyStore import java.security.PrivateKey +import java.security.interfaces.ECPublicKey +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +@ExperimentalEncodingApi @Suppress("MemberVisibilityCanBePrivate", "unused") internal class KeystoreManager { private val ks: KeyStore = KeyStore.getInstance(KEYSTORE).apply { @@ -27,6 +31,9 @@ internal class KeystoreManager { val appCheckPrivateKey: PrivateKey get() = ks.getKey(ALIAS, null) as PrivateKey + val appCheckPublicKey: ECPublicKey + get() = ks.getCertificate(ALIAS).publicKey as ECPublicKey + init { if (!hasAppCheckKeys) { Log.d(this::class.simpleName, "Generating key pair") @@ -34,6 +41,14 @@ internal class KeystoreManager { } } + fun getPubKeyBase64ECCoord(): Pair { + val xByteArr = appCheckPublicKey.w.affineX.toByteArray() + val yByteArr = appCheckPublicKey.w.affineY.toByteArray() + val x = Base64.encode(xByteArr) + val y = Base64.encode(yByteArr) + return Pair(x, y) + } + private fun createNewKeys() { KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, diff --git a/app/src/main/java/uk/gov/android/authentication/integrity/model/AttestationResponse.kt b/app/src/main/java/uk/gov/android/authentication/integrity/model/AttestationResponse.kt index 8552f31..dc2289a 100644 --- a/app/src/main/java/uk/gov/android/authentication/integrity/model/AttestationResponse.kt +++ b/app/src/main/java/uk/gov/android/authentication/integrity/model/AttestationResponse.kt @@ -1,6 +1,17 @@ package uk.gov.android.authentication.integrity.model +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +@OptIn(ExperimentalSerializationApi::class) sealed class AttestationResponse { - data class Success(val attestationJwt: String) : AttestationResponse() + @Serializable + data class Success( + @JsonNames("client_attestation") + val attestationJwt: String, + @JsonNames("expires_in") + val expiresIn: String + ) : AttestationResponse() data class Failure(val reason: String, val error: Throwable? = null) : AttestationResponse() } diff --git a/app/src/main/java/uk/gov/android/authentication/integrity/usecase/AttestationCaller.kt b/app/src/main/java/uk/gov/android/authentication/integrity/usecase/AttestationCaller.kt index 220f6f1..3bb40d9 100644 --- a/app/src/main/java/uk/gov/android/authentication/integrity/usecase/AttestationCaller.kt +++ b/app/src/main/java/uk/gov/android/authentication/integrity/usecase/AttestationCaller.kt @@ -1,16 +1,17 @@ package uk.gov.android.authentication.integrity.usecase +import uk.gov.android.authentication.integrity.model.AttestationResponse + @Suppress("unused") fun interface AttestationCaller { suspend fun call( - signedProofOfPossession: String, - jwkX: String, - jwkY: String - ): Result - - data class Response(val jwt: String, val expiresIn: Long) + firebaseToken: String, + jwk: JWK.JsonWebKey + ): AttestationResponse companion object { const val FIREBASE_HEADER = "X-Firebase-AppCheck" + const val CONTENT_TYPE = "Content-type" + const val CONTENT_TYPE_VALUE = "application/json" } } diff --git a/app/src/main/java/uk/gov/android/authentication/integrity/usecase/JWK.kt b/app/src/main/java/uk/gov/android/authentication/integrity/usecase/JWK.kt index dd2b366..ea7a0a6 100644 --- a/app/src/main/java/uk/gov/android/authentication/integrity/usecase/JWK.kt +++ b/app/src/main/java/uk/gov/android/authentication/integrity/usecase/JWK.kt @@ -1,25 +1,34 @@ package uk.gov.android.authentication.integrity.usecase -import org.jose4j.jwk.JsonWebKey +import kotlinx.serialization.Serializable @Suppress("MemberVisibilityCanBePrivate") object JWK { - const val keyType = "kty" - const val use = "use" - const val curve = "crv" - const val x = "x" - const val y = "y" private const val keyTypeValue = "EC" private const val useValue = "sig" private const val curveValue = "P-256" - fun makeJWK(x: String, y: String): JsonWebKey = JsonWebKey.Factory.newJwk( - mapOf( - keyType to keyTypeValue, - use to useValue, - curve to curveValue, - JWK.x to x, - JWK.y to y + fun makeJWK(x: String, y: String): JsonWebKey = JsonWebKey( + jwk = JsonWebKeyFormat( + keyTypeValue, + useValue, + curveValue, + x, + y ) ) + + @Serializable + data class JsonWebKey( + val jwk: JsonWebKeyFormat + ) + + @Serializable + data class JsonWebKeyFormat( + val kty: String, + val use: String, + val crv: String, + val x: String, + val y: String + ) } diff --git a/app/src/test/java/uk/gov/android/authentication/integrity/usecase/JWKTest.kt b/app/src/test/java/uk/gov/android/authentication/integrity/usecase/JWKTest.kt index 73f0da4..6a905d2 100644 --- a/app/src/test/java/uk/gov/android/authentication/integrity/usecase/JWKTest.kt +++ b/app/src/test/java/uk/gov/android/authentication/integrity/usecase/JWKTest.kt @@ -7,12 +7,20 @@ class JWKTest { @Test fun `makeJWK sets defaults`() { val actual = JWK.makeJWK(X, Y) - assertEquals(expected = "EC", actual = actual.keyType) - assertEquals(expected = "sig", actual = actual.use) + assertEquals(jwk, actual) } companion object { const val X = "18wHLeIgW9wVN6VD1Txgpqy2LszYkMf6J8njVAibvhM" const val Y = "-V4dS4UaLMgP_4fY4j8ir7cl1TXlFdAgcx55o7TkcSA" + val jwk = JWK.JsonWebKey( + jwk = JWK.JsonWebKeyFormat( + "EC", + "sig", + "P-256", + X, + Y + ) + ) } }