Skip to content

Commit

Permalink
Fido: Add support for proper attestation
Browse files Browse the repository at this point in the history
  • Loading branch information
mar-v-in committed Sep 17, 2022
1 parent 8eee363 commit 304c352
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 42 deletions.
2 changes: 2 additions & 0 deletions play-services-fido-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies {
api project(':play-services-fido-api')

implementation project(':play-services-base-core')
implementation project(':play-services-safetynet')
implementation project(':play-services-tasks-ktx')

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ val RequestOptions.rpId: String
SIGN -> signOptions.rpId
}

val PublicKeyCredentialCreationOptions.skipAttestation: Boolean
get() = attestationConveyancePreference in setOf(AttestationConveyancePreference.NONE, null)

fun RequestOptions.checkIsValid(context: Context) {
if (type == REGISTER) {
if (registerOptions.authenticatorSelection.requireResidentKey == true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import android.os.Parcel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.Feature
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.internal.ConnectionInfo
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.android.gms.fido.fido2.api.IBooleanCallback
Expand All @@ -43,10 +45,15 @@ const val TAG = "Fido2Privileged"

class Fido2PrivilegedService : BaseService(TAG, FIDO2_PRIVILEGED) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
callback.onPostInitComplete(
callback.onPostInitCompleteWithConnectionInfo(
CommonStatusCodes.SUCCESS,
Fido2PrivilegedServiceImpl(this, lifecycle).asBinder(),
null
ConnectionInfo().apply {
features = arrayOf(
Feature("is_user_verifying_platform_authenticator_available", 1),
Feature("is_user_verifying_platform_authenticator_available_for_credential", 1)
)
}
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.fido.core.protocol

import com.google.android.gms.fido.fido2.api.common.Algorithm
import com.upokecenter.cbor.CBORObject

class AndroidKeyAttestationObject(
authData: AuthenticatorData,
val alg: Algorithm,
val sig: ByteArray,
val x5c: List<ByteArray>
) :
AttestationObject(authData.encode()) {
override val fmt: String
get() = "android-key"
override val attStmt: CBORObject
get() = CBORObject.NewMap().apply {
set("alg", alg.algoValue.encodeAsCbor())
set("sig", sig.encodeAsCbor())
set("x5c", CBORObject.NewArray().apply {
for (certificate in x5c) {
Add(certificate.encodeAsCbor())
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@

package org.microg.gms.fido.core.protocol

import android.util.Base64
import android.util.Log
import com.upokecenter.cbor.CBORObject
import org.microg.gms.utils.toBase64
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

package org.microg.gms.fido.core.protocol

import com.upokecenter.cbor.CBORObject
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.experimental.and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
0,
credentialData
)
val attestationObject =
val attestationObject = if (options.registerOptions.skipAttestation) {
NoneAttestationObject(authData)
} else {
FidoU2fAttestationObject(authData, response.signature, response.attestationCertificate)
}
val ctap2Response = AuthenticatorMakeCredentialResponse(
authData.encode(),
attestationObject.fmt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ package org.microg.gms.fido.core.transport.screenlock
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import org.microg.gms.utils.toBase64
import java.security.*
import java.security.cert.Certificate
import java.security.spec.ECGenParameterSpec
import kotlin.random.Random

Expand All @@ -27,25 +29,27 @@ class ScreenLockCredentialStore(val context: Context) {
private fun getPrivateKey(rpId: String, keyId: ByteArray) = keyStore.getKey(getAlias(rpId, keyId), null) as? PrivateKey

@RequiresApi(23)
fun createKey(rpId: String): ByteArray {
fun createKey(rpId: String, challenge: ByteArray): ByteArray {
val keyId = Random.nextBytes(32)
val identifier = getAlias(rpId, keyId)
Log.d(TAG, "Creating key for $identifier")
val generator = KeyPairGenerator.getInstance("EC", "AndroidKeyStore")
generator.initialize(
KeyGenParameterSpec.Builder(identifier, KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setUserAuthenticationRequired(true)
.build()
)
val builder = KeyGenParameterSpec.Builder(identifier, KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setUserAuthenticationRequired(true)
if (Build.VERSION.SDK_INT >= 24) builder.setAttestationChallenge(challenge)
generator.initialize(builder.build())
generator.generateKeyPair()
return keyId
}

fun getPublicKey(rpId: String, keyId: ByteArray): PublicKey? =
keyStore.getCertificate(getAlias(rpId, keyId))?.publicKey

fun getCertificateChain(rpId: String, keyId: ByteArray): Array<Certificate> =
keyStore.getCertificateChain(getAlias(rpId, keyId))

fun getSignature(rpId: String, keyId: ByteArray): Signature? {
val privateKey = getPrivateKey(rpId, keyId) ?: return null
val signature = Signature.getInstance("SHA256withECDSA")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ package org.microg.gms.fido.core.transport.screenlock

import android.app.KeyguardManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricPrompt
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.AttestationConveyancePreference.NONE
import com.google.android.gms.safetynet.SafetyNet
import com.google.android.gms.tasks.await
import kotlinx.coroutines.suspendCancellableCoroutine
import org.microg.gms.basement.BuildConfig
import org.microg.gms.fido.core.*
import org.microg.gms.fido.core.protocol.*
import org.microg.gms.fido.core.transport.Transport
Expand All @@ -23,12 +28,13 @@ import java.security.interfaces.ECPublicKey
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

@RequiresApi(23)
class ScreenLockTransportHandler(private val activity: FragmentActivity, callback: TransportHandlerCallback? = null) :
TransportHandler(Transport.SCREEN_LOCK, callback) {
private val store by lazy { ScreenLockCredentialStore(activity) }

override val isSupported: Boolean
get() = Build.VERSION.SDK_INT >= 23 && activity.getSystemService<KeyguardManager>()?.isDeviceSecure == true
get() = activity.getSystemService<KeyguardManager>()?.isDeviceSecure == true

suspend fun showBiometricPrompt(applicationName: String, signature: Signature?) {
suspendCancellableCoroutine<BiometricPrompt.AuthenticationResult> { continuation ->
Expand Down Expand Up @@ -76,15 +82,15 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
return signature
}

fun getCredentialData(credentialId: CredentialId, coseKey: CoseKey) = AttestedCredentialData(
ByteArray(16), // 0xb93fd961f2e6462fb12282002247de78 for SafetyNet
fun getCredentialData(aaguid: ByteArray, credentialId: CredentialId, coseKey: CoseKey) = AttestedCredentialData(
aaguid,
credentialId.encode(),
coseKey.encode()
)

fun getAuthenticatorData(
rpId: String,
credentialData: AttestedCredentialData,
credentialData: AttestedCredentialData?,
userPresent: Boolean = true,
userVerified: Boolean = true,
signCount: Int = 0
Expand All @@ -96,7 +102,6 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
attestedCredentialData = credentialData
)

@RequiresApi(23)
suspend fun register(
options: RequestOptions,
callerPackage: String
Expand All @@ -111,34 +116,72 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
}
}
val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage)
if (options.registerOptions.attestationConveyancePreference in setOf(
AttestationConveyancePreference.NONE,
null
)
) {
// No attestation needed
} else {
// TODO: SafetyNet
throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR, "SafetyNet Attestation not yet supported")
}
val keyId = store.createKey(options.rpId)
val aaguid = if (options.registerOptions.skipAttestation) ByteArray(16) else AAGUID
val keyId = store.createKey(options.rpId, clientDataHash)
val publicKey =
store.getPublicKey(options.rpId, keyId) ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)

// We're ignoring the signature object as we don't need it for registration
getActiveSignature(options, callerPackage, keyId)
val signature = getActiveSignature(options, callerPackage, keyId)

val (x, y) = (publicKey as ECPublicKey).w.let { it.affineX to it.affineY }
val coseKey = CoseKey(EC2Algorithm.ES256, x, y, 1, 32)
val credentialId = CredentialId(1, keyId, options.rpId, publicKey)

val credentialData = getCredentialData(credentialId, coseKey)
val credentialData = getCredentialData(aaguid, credentialId, coseKey)
val authenticatorData = getAuthenticatorData(options.rpId, credentialData)

val attestationObject = if (options.registerOptions.skipAttestation) {
NoneAttestationObject(authenticatorData)
} else {
try {
if (Build.VERSION.SDK_INT >= 24) {
createAndroidKeyAttestation(signature, authenticatorData, clientDataHash, options.rpId, keyId)
} else {
createSafetyNetAttestation(authenticatorData, clientDataHash)
}
} catch (e: Exception) {
Log.w("FidoScreenLockTransport", e)
NoneAttestationObject(authenticatorData)
}
}

return AuthenticatorAttestationResponse(
credentialId.encode(),
clientData,
NoneAttestationObject(authenticatorData).encode()
attestationObject.encode()
)
}

@RequiresApi(24)
private fun createAndroidKeyAttestation(
signature: Signature,
authenticatorData: AuthenticatorData,
clientDataHash: ByteArray,
rpId: String,
keyId: ByteArray
): AndroidKeyAttestationObject {
signature.update(authenticatorData.encode() + clientDataHash)
val sig = signature.sign()
return AndroidKeyAttestationObject(
authenticatorData,
EC2Algorithm.ES256,
sig,
store.getCertificateChain(rpId, keyId).map { it.encoded })
}

private suspend fun createSafetyNetAttestation(
authenticatorData: AuthenticatorData,
clientDataHash: ByteArray
): AndroidSafetyNetAttestationObject {
val response = SafetyNet.getClient(activity).attest(
(authenticatorData.encode() + clientDataHash).digest("SHA-256"),
"AIzaSyDqVnJBjE5ymo--oBJt3On7HQx9xNm1RHA"
).await()
return AndroidSafetyNetAttestationObject(
authenticatorData,
BuildConfig.VERSION_CODE.toString(),
response.jwsResult.toByteArray()
)
}

Expand Down Expand Up @@ -173,10 +216,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
val keyId = credentialId.data

val (x, y) = (credentialId.publicKey as ECPublicKey).w.let { it.affineX to it.affineY }
val coseKey = CoseKey(EC2Algorithm.ES256, x, y, 1, 32)

val credentialData = getCredentialData(credentialId, coseKey)
val authenticatorData = getAuthenticatorData(options.rpId, credentialData)
val authenticatorData = getAuthenticatorData(options.rpId, null)

val signature = getActiveSignature(options, callerPackage, keyId)
signature.update(authenticatorData.encode() + clientDataHash)
Expand All @@ -191,7 +231,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
)
}

@RequiresApi(23)
@RequiresApi(24)
override suspend fun start(options: RequestOptions, callerPackage: String): AuthenticatorResponse =
when (options.type) {
RequestOptionsType.REGISTER -> register(options, callerPackage)
Expand All @@ -212,4 +252,11 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
}
return false
}

companion object {
private val AAGUID = byteArrayOf(
0xb9.toByte(), 0x3f, 0xd9.toByte(), 0x61, 0xf2.toByte(), 0xe6.toByte(), 0x46, 0x2f,
0xb1.toByte(), 0x22, 0x82.toByte(), 0x00, 0x22, 0x47, 0xde.toByte(), 0x78
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {
BluetoothTransportHandler(this, this),
NfcTransportHandler(this, this),
if (Build.VERSION.SDK_INT >= 21) UsbTransportHandler(this, this) else null,
ScreenLockTransportHandler(this, this)
if (Build.VERSION.SDK_INT >= 23) ScreenLockTransportHandler(this, this) else null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

package com.google.android.gms.fido.fido2;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

public class Fido2PrivilegedGmsClient extends GmsClient<IFido2PrivilegedService> {
public Fido2PrivilegedGmsClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) {
super(context, callbacks, connectionFailedListener, GmsService.OSS_LICENSES.ACTION);
super(context, callbacks, connectionFailedListener, GmsService.FIDO2_PRIVILEGED.ACTION);
serviceId = GmsService.FIDO2_PRIVILEGED.SERVICE_ID;
}

Expand Down

3 comments on commit 304c352

@CoelacanthusHex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mar-v-in
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CoelacanthusHex is updated now.

@CoelacanthusHex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx

Please sign in to comment.