Skip to content

Commit

Permalink
feat: Generate ProofOfPosession (#101)
Browse files Browse the repository at this point in the history
* feat: Create ProofOfPosession structure and amend KeyStoreManager

- create KeyStreManager interface
- amend KeyStoreManager impl name and availabel functions to allow for
signing and verifying Signature for Jwt

* refactor: Amend naming to be more accurate

* feat: Amend FirebaseClientAttestationManager to enable getting PoP

- amend implementation to create sign PoP and create PoP Jwt

* test: Fix tests

* fix: Change exp time to 3 minutes

* test: Remove unused import and add test

* feat: Expose ECKeyManager

* refactor: Change the integrity package structure

- pkg structure based on functionality

* test: Amend tests

* style: Add Log for the signature verification for QA purpose

* refactor: Change names to be more specific
  • Loading branch information
BiancaMihaila authored Nov 15, 2024
1 parent cc9fce3 commit f8a1b5d
Show file tree
Hide file tree
Showing 21 changed files with 390 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@ import kotlin.test.BeforeTest
import kotlin.test.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import uk.gov.android.authentication.integrity.appcheck.AppChecker
import uk.gov.android.authentication.integrity.model.AppCheckToken
import uk.gov.android.authentication.integrity.appcheck.usecase.AppChecker
import uk.gov.android.authentication.integrity.keymanager.ECKeyManager
import uk.gov.android.authentication.integrity.keymanager.KeyStoreManager
import uk.gov.android.authentication.integrity.appcheck.model.AppCheckToken
import uk.gov.android.authentication.integrity.appcheck.model.AttestationResponse
import uk.gov.android.authentication.integrity.pop.ProofOfPossessionGenerator
import uk.gov.android.authentication.integrity.pop.SignedPoP
import uk.gov.android.authentication.integrity.appcheck.usecase.AttestationCaller
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 java.security.SignatureException
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class FirebaseClientAttestationManagerTest {
private lateinit var clientAttestationManager: ClientAttestationManager

private val caller: AttestationCaller = mock()
private val mockCaller: AttestationCaller = mock()
private val mockAppChecker: AppChecker = mock()
private val mockKeyStoreManager: KeyStoreManager = mock()

@BeforeTest
fun setup() {
val config = AppIntegrityConfiguration(
caller,
mockAppChecker
mockCaller,
mockAppChecker,
mockKeyStoreManager
)

clientAttestationManager = FirebaseClientAttestationManager(config)
Expand All @@ -34,14 +42,18 @@ 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(
whenever(mockCaller.call(any(), any()))
.thenReturn(
AttestationResponse.Success(
"Success",
0
))
whenever(mockKeyStoreManager.getPublicKey())
.thenReturn(Pair("Success", "Success"))
val result = clientAttestationManager.getAttestation()

assertEquals(AttestationResponse.Success("Success", 0),
assertEquals(
AttestationResponse.Success("Success", 0),
result)
}

Expand All @@ -50,6 +62,9 @@ class FirebaseClientAttestationManagerTest {
whenever(mockAppChecker.getAppCheckToken()).thenReturn(
Result.failure(Exception("Error"))
)
whenever(mockKeyStoreManager.getPublicKey())
.thenReturn(Pair("Success", "Success"))

val result = clientAttestationManager.getAttestation()

assertEquals(Exception("Error").toString(),
Expand All @@ -60,18 +75,56 @@ class FirebaseClientAttestationManagerTest {
fun check_failure_response_from_get_attestation() = runBlocking {
whenever(mockAppChecker.getAppCheckToken())
.thenReturn(Result.success(AppCheckToken("Success")))
whenever(caller.call(any(), any()))
whenever(mockCaller.call(any(), any()))
.thenReturn(AttestationResponse.Failure("Error"))
whenever(mockKeyStoreManager.getPublicKey())
.thenReturn(Pair("Success", "Success"))
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")
fun check_success_response_from_generate_PoP() {
val mockSignatureByte = "Success".toByteArray()
val mockSignature = ProofOfPossessionGenerator.getUrlSafeNoPaddingBase64(mockSignatureByte)

whenever(mockKeyStoreManager.sign(any())).thenReturn(mockSignatureByte)

val result = clientAttestationManager.generatePoP(MOCK_VALUE, MOCK_VALUE)

assertTrue(result is SignedPoP.Success)

val splitJwt = result.popJwt.split(".")
assertTrue(splitJwt.size == 3)
assertEquals(mockSignature, splitJwt.last())
}

@OptIn(ExperimentalEncodingApi::class)
@Test(expected = Exception::class)
fun check_failure_response_from_generate_PoP_verify_signature_failure() {
whenever(mockKeyStoreManager.verify(any(), any()))
.thenThrow(ECKeyManager.SigningError.InvalidSignature)
val result = clientAttestationManager.generatePoP("test", "test")

assertTrue(result is SignedPoP.Failure)
assertTrue(result.error!! is ECKeyManager.SigningError)
assertEquals("Signature couldn't be verified.", result.reason)
}

@Test(expected = Exception::class)
fun check_failure_response_from_generate_PoP_signing_failure() {
whenever(mockKeyStoreManager.sign(any()))
.thenThrow(SignatureException())
val result = clientAttestationManager.generatePoP("test", "test")

assertTrue(result is SignedPoP.Failure)
assertTrue(result.error!! is SignatureException)
assertEquals("Signing Error", result.reason)
}

assertEquals("Not yet implemented", (result as SignedResponse.Failure).reason)
companion object {
private const val MOCK_VALUE = "test"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package uk.gov.android.authentication.integrity.keymanager

import org.junit.Assert.assertThrows
import uk.gov.android.authentication.integrity.pop.ProofOfPossessionGenerator
import org.junit.Test as JUnitTest
import java.security.KeyStore
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertTrue

@OptIn(ExperimentalEncodingApi::class)
class ECKeyManagerTest {
private lateinit var keyStore: KeyStore
private lateinit var ecKeyManager: ECKeyManager

@BeforeTest
fun setup() {
keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
ecKeyManager = ECKeyManager()
}

@Test
fun check_keys_created_on_initialization() {
assertTrue(keyStore.containsAlias("app_check_keys"))
}

@Test
fun check_getPublicKey() {
val actual = ecKeyManager.getPublicKey()

assertTrue(checkInputIsBase64(actual.first))
assertTrue(checkInputIsBase64(actual.second))
}

@Test
fun check_sign_success() {
val signature = ecKeyManager.sign("Success".toByteArray())

assertTrue(ecKeyManager.verify("Success".toByteArray(), signature))
}



@JUnitTest
fun check_verify_failure() {
assertThrows(ECKeyManager.SigningError.InvalidSignature::class.java) {
ecKeyManager.verify("Success".toByteArray(), "Success".toByteArray())
}
}

@Suppress("SwallowedException")
private fun checkInputIsBase64(input: String): Boolean {
return try {
ProofOfPossessionGenerator.getUrlSafeNoPaddingBase64(input.toByteArray())
true
} catch (e: IllegalArgumentException) {
false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package uk.gov.android.authentication.integrity

import uk.gov.android.authentication.integrity.model.AttestationResponse
import uk.gov.android.authentication.integrity.model.SignedResponse
import uk.gov.android.authentication.integrity.appcheck.model.AttestationResponse
import uk.gov.android.authentication.integrity.pop.SignedPoP

interface ClientAttestationManager {
suspend fun getAttestation(): AttestationResponse
suspend fun signAttestation(attestation: String): SignedResponse
fun generatePoP(iss: String, aud: String): SignedPoP
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package uk.gov.android.authentication.integrity

import uk.gov.android.authentication.integrity.appcheck.AppChecker
import uk.gov.android.authentication.integrity.model.AppCheckToken
import android.util.Log
import uk.gov.android.authentication.integrity.appcheck.usecase.AppChecker
import uk.gov.android.authentication.integrity.keymanager.ECKeyManager
import uk.gov.android.authentication.integrity.keymanager.KeyStoreManager
import uk.gov.android.authentication.integrity.appcheck.model.AppCheckToken
import uk.gov.android.authentication.integrity.appcheck.model.AttestationResponse
import uk.gov.android.authentication.integrity.pop.ProofOfPossessionGenerator
import uk.gov.android.authentication.integrity.pop.SignedPoP
import uk.gov.android.authentication.integrity.appcheck.usecase.AttestationCaller
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 uk.gov.android.authentication.integrity.appcheck.usecase.JWK
import java.security.SignatureException
import kotlin.io.encoding.ExperimentalEncodingApi

@OptIn(ExperimentalEncodingApi::class)
Expand All @@ -15,15 +20,15 @@ class FirebaseClientAttestationManager(
) : ClientAttestationManager {
private val appChecker: AppChecker = config.appChecker
private val attestationCaller: AttestationCaller = config.attestationCaller
private val keyManager = KeystoreManager()
private val keyStoreManager: KeyStoreManager = config.keyStoreManager

override suspend fun getAttestation(): AttestationResponse {
// Get Firebase token
val token = appChecker.getAppCheckToken().getOrElse { err ->
AttestationResponse.Failure(err.toString())
}
// If successful -> functionality to get signed attestation form Mobile back-end
val pubKeyECCoord = keyManager.getPubKeyBase64ECCoord()
val pubKeyECCoord = keyStoreManager.getPublicKey()
val jwk = JWK.makeJWK(x = pubKeyECCoord.first, y = pubKeyECCoord.second)
return if (token is AppCheckToken) {
attestationCaller.call(
Expand All @@ -36,8 +41,29 @@ class FirebaseClientAttestationManager(
}
}

override suspend fun signAttestation(attestation: String): SignedResponse {
// Not yet implemented
return SignedResponse.Failure("Not yet implemented")
override fun generatePoP(iss: String, aud: String): SignedPoP {
// Create Proof of Possession
val pop = ProofOfPossessionGenerator.createBase64PoP(iss, aud)
// Convert into ByteArray
val popByteArray = pop.toByteArray()
return try {
// Get signature to be appended to PoPJwt
val byteSignature = keyStoreManager.sign(popByteArray)
// Encode signature in Base64 configured with UrlSafe and no padding
val signature = ProofOfPossessionGenerator.getUrlSafeNoPaddingBase64(byteSignature)
// Return the signed PopJwt
val signedPop = "$pop.$signature"
Log.d("SignedPoP", signedPop)
SignedPoP.Success(signedPop)
} catch (e: ECKeyManager.SigningError) {
SignedPoP.Failure(e.message ?: VERIFF_ERROR, e)
} catch (e: SignatureException) {
SignedPoP.Failure(e.message ?: SIGN_ERROR, e)
}
}

companion object {
const val VERIFF_ERROR = "Verification Error"
const val SIGN_ERROR = "Signing Error"
}
}
Loading

0 comments on commit f8a1b5d

Please sign in to comment.