Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DCMAW-10311 generate key pair #87

Merged
merged 11 commits into from
Oct 31, 2024
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ dependencies {
kotlin("test-junit5"),
libs.bundles.test,
platform(libs.junit.bom),
libs.mockito.core,
libs.mockito.kotlin
).forEach(::testImplementation)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package uk.gov.android.authentication.integrity

import kotlinx.coroutines.runBlocking
import kotlin.test.BeforeTest
import kotlin.test.Test
import org.mockito.kotlin.mock
import uk.gov.android.authentication.integrity.appcheck.AppChecker
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 kotlin.test.assertEquals

class FirebaseAppIntegrityCheckerTest {
private lateinit var clientAttestationManager: ClientAttestationManager

private val caller: AttestationCaller = mock()
private val mockAppChecker: AppChecker = mock()

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

clientAttestationManager = FirebaseClientAttestationManager(config)
}

@Test
fun check_failure_response_from_get_attestation() = runBlocking {
val result = clientAttestationManager.getAttestation()

assertEquals("Not yet implemented", (result as AttestationResponse.Failure).reason)
}

@Test
fun check_failure_response_from_sign_attestation() = runBlocking {
val result = clientAttestationManager.signAttestation("attestation")

assertEquals("Not yet implemented", (result as SignedResponse.Failure).reason)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package uk.gov.android.authentication.integrity

import android.security.keystore.KeyProperties
import kotlin.test.BeforeTest
import java.security.KeyStore
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class KeystoreManagerTest {
private lateinit var keyStore: KeyStore
private lateinit var keystoreManager: KeystoreManager

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

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

@Test
fun hasAppCheckKeys() {
val actual = keystoreManager.hasAppCheckKeys
assertTrue(actual)
}

@Test
fun appCheckPrivateKey() {
val privateKey = keystoreManager.appCheckPrivateKey
assertEquals(KeyProperties.KEY_ALGORITHM_EC, privateKey.algorithm)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.app.Activity
import android.app.Instrumentation
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal
import androidx.test.espresso.intent.matcher.UriMatchers
import androidx.test.platform.app.InstrumentationRegistry
import net.openid.appauth.AuthorizationManagementActivity
import org.hamcrest.CoreMatchers.not
import uk.gov.android.authentication.TestActivity
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -38,6 +44,10 @@ class AppAuthPresentTest {
loginSession.present(launcher, loginSessionConfig)
}

intending(not(isInternal())).respondWith(
Instrumentation.ActivityResult(Activity.RESULT_OK, null)
)

// Then launch an AuthorizationManagementActivity intent
Intents.intended(IntentMatchers.hasComponent(AuthorizationManagementActivity::class.java.name))
// Aimed at the host, path, and scheme set by the LoginSessionConfiguration.authorizeEndpoint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
Expand Down Expand Up @@ -63,6 +63,6 @@ class AppAuthSessionTest {
}
// Then throw an AuthenticationError
assertEquals(AuthenticationError.ErrorType.OAUTH, error.type)
assertEquals(AuthenticationError.NULL_AUTH_MESSAGE, error.message)
assertEquals(AuthenticationError.Companion.NULL_AUTH_MESSAGE, error.message)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.net.Uri
import kotlin.test.assertEquals
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.net.Uri
import net.openid.appauth.AuthorizationRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import net.openid.appauth.TokenRequest
import net.openid.appauth.TokenResponse
import uk.gov.android.authentication.login.openid.TestValues
import kotlin.test.BeforeTest
import kotlin.test.Test
import uk.gov.android.authentication.openid.TestValues
import kotlin.test.assertEquals

class TokenResponseUtilTest {
Expand All @@ -12,15 +13,15 @@ class TokenResponseUtilTest {
private val accessTokenExpiryTime = 1000L
private val idToken = "idToken"
private val refreshToken = "idToken"
private lateinit var responseBuilder: net.openid.appauth.TokenResponse.Builder
private lateinit var responseBuilder: TokenResponse.Builder

@BeforeTest
fun setUp() {
val request = TokenRequest.Builder(TestValues.testServiceConfig, TestValues.TEST_CLIENT_ID)
.setAuthorizationCode(TestValues.TEST_AUTH_CODE)
.setRedirectUri(TestValues.TEST_APP_REDIRECT_URI)
.build()
responseBuilder = net.openid.appauth.TokenResponse.Builder(request)
responseBuilder = TokenResponse.Builder(request)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* Note this file is used for internal testing purposes only and is not for distribution.
*/
package uk.gov.android.authentication.openid
package uk.gov.android.authentication.login.openid

import android.net.Uri
import net.openid.appauth.AuthorizationServiceConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package uk.gov.android.authentication.integrity

import uk.gov.android.authentication.integrity.model.AttestationResponse
import uk.gov.android.authentication.integrity.model.SignedResponse

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

import uk.gov.android.authentication.integrity.appcheck.AppChecker
import uk.gov.android.authentication.integrity.model.AppIntegrityConfiguration
import uk.gov.android.authentication.integrity.model.AttestationResponse
import uk.gov.android.authentication.integrity.model.SignedResponse

@Suppress("UnusedPrivateProperty")
class FirebaseClientAttestationManager(
config: AppIntegrityConfiguration
) : ClientAttestationManager {
private val appChecker: AppChecker = config.appChecker
private val keyManager = KeystoreManager()

override suspend fun getAttestation(): AttestationResponse {
// Not yet implemented
return AttestationResponse.Failure("Not yet implemented")
}

override suspend fun signAttestation(attestation: String): SignedResponse {
// Not yet implemented
return SignedResponse.Failure("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package uk.gov.android.authentication.integrity

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey

@Suppress("MemberVisibilityCanBePrivate", "unused")
internal class KeystoreManager {
private val ks: KeyStore = KeyStore.getInstance(KEYSTORE).apply {
load(null)
}

private val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).run {
setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
build()
}

val hasAppCheckKeys: Boolean
get() = ks.containsAlias(ALIAS)

val appCheckPrivateKey: PrivateKey
get() = ks.getKey(ALIAS, null) as PrivateKey

init {
if (!hasAppCheckKeys) {
Log.d(this::class.simpleName, "Generating key pair")
createNewKeys()
}
}

private fun createNewKeys() {
KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
KEYSTORE
).apply {
initialize(parameterSpec)
genKeyPair()
}
}

companion object {
private const val ALIAS = "app_check_keys"
private const val KEYSTORE = "AndroidKeyStore"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package uk.gov.android.authentication.integrity.appcheck

import android.content.Context

interface AppChecker {
fun init(context: Context)

fun getAppCheckToken(
onSuccess: (String) -> Unit,
onFailure: (Exception) -> Unit
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package uk.gov.android.authentication.integrity.model

import uk.gov.android.authentication.integrity.appcheck.AppChecker
import uk.gov.android.authentication.integrity.usecase.AttestationCaller

data class AppIntegrityConfiguration(
val attestationCaller: AttestationCaller,
val appChecker: AppChecker
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package uk.gov.android.authentication.integrity.model

sealed class AttestationResponse {
data class Success(val attestationJwt: String) : AttestationResponse()
data class Failure(val reason: String, val error: Throwable? = null) : AttestationResponse()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package uk.gov.android.authentication.integrity.model

sealed class SignedResponse {
data class Success(val signedAttestationJwt: String) : SignedResponse()
data class Failure(val reason: String, val error: Throwable? = null) : SignedResponse()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package uk.gov.android.authentication.integrity.usecase

@Suppress("unused")
fun interface AttestationCaller {
suspend fun call(
signedProofOfPossession: String,
jwkX: String,
jwkY: String
): Result<Response>

data class Response(val jwt: String, val expiresIn: Long)

companion object {
protected const val FIREBASE_HEADER = "X-Firebase-AppCheck"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package uk.gov.android.authentication.integrity.usecase

import org.jose4j.jwk.JsonWebKey

@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
)
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.content.Context
import android.content.Intent
Expand All @@ -25,6 +25,7 @@ class AppAuthSession(
) {
val authResponse = AuthorizationResponse.fromIntent(intent)
val request = authResponse?.createTokenExchangeRequest() ?: throw AuthenticationError.from(intent)
request.additionalParameters[""] = ""
authService.performTokenRequest(
request
) { response, exception ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.content.Intent
import net.openid.appauth.AuthorizationException
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import android.net.Uri

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.android.authentication
package uk.gov.android.authentication.login

import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationServiceConfiguration
Expand Down
Loading