From 304c3522e992a3db641411fd21df4088db579dad Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 17 Sep 2022 10:59:06 +0200 Subject: [PATCH] Fido: Add support for proper attestation --- play-services-fido-core/build.gradle | 2 + .../microg/gms/fido/core/RequestHandling.kt | 3 + .../core/privileged/Fido2PrivilegedService.kt | 11 ++- .../protocol/AndroidKeyAttestationObject.kt | 30 ++++++ .../core/protocol/AttestedCredentialData.kt | 3 - .../fido/core/protocol/AuthenticatorData.kt | 1 - .../fido/core/transport/TransportHandler.kt | 5 +- .../screenlock/ScreenLockCredentialStore.kt | 20 ++-- .../screenlock/ScreenLockTransportHandler.kt | 95 ++++++++++++++----- .../gms/fido/core/ui/AuthenticatorActivity.kt | 2 +- .../gms/fido/fido2/Fido2ApiClient.java | 1 - .../fido/fido2/Fido2PrivilegedGmsClient.java | 2 +- 12 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AndroidKeyAttestationObject.kt diff --git a/play-services-fido-core/build.gradle b/play-services-fido-core/build.gradle index 0b3a223799..25bd158d00 100644 --- a/play-services-fido-core/build.gradle +++ b/play-services-fido-core/build.gradle @@ -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" diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt index 77a0ac8e6e..4288adc76e 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -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) { diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt index eeb3e049d8..41dab87760 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt @@ -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 @@ -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) + ) + } ); } } diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AndroidKeyAttestationObject.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AndroidKeyAttestationObject.kt new file mode 100644 index 0000000000..d3bd76a251 --- /dev/null +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AndroidKeyAttestationObject.kt @@ -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 +) : + 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()) + } + }) + } +} diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AttestedCredentialData.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AttestedCredentialData.kt index e000abec50..89890da68a 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AttestedCredentialData.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AttestedCredentialData.kt @@ -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 diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AuthenticatorData.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AuthenticatorData.kt index 4fce64d1ab..b4c94e9d26 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AuthenticatorData.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/protocol/AuthenticatorData.kt @@ -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 diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt index 6fea7f3f51..f6cf52f3be 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt @@ -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, diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt index 81f69b95e6..384fafcc16 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt @@ -8,6 +8,7 @@ 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 @@ -15,6 +16,7 @@ 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 @@ -27,18 +29,17 @@ 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 } @@ -46,6 +47,9 @@ class ScreenLockCredentialStore(val context: Context) { fun getPublicKey(rpId: String, keyId: ByteArray): PublicKey? = keyStore.getCertificate(getAlias(rpId, keyId))?.publicKey + fun getCertificateChain(rpId: String, keyId: ByteArray): Array = + keyStore.getCertificateChain(getAlias(rpId, keyId)) + fun getSignature(rpId: String, keyId: ByteArray): Signature? { val privateKey = getPrivateKey(rpId, keyId) ?: return null val signature = Signature.getInstance("SHA256withECDSA") diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index 01c92ef05f..28ed8cdd41 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -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 @@ -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()?.isDeviceSecure == true + get() = activity.getSystemService()?.isDeviceSecure == true suspend fun showBiometricPrompt(applicationName: String, signature: Signature?) { suspendCancellableCoroutine { continuation -> @@ -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 @@ -96,7 +102,6 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac attestedCredentialData = credentialData ) - @RequiresApi(23) suspend fun register( options: RequestOptions, callerPackage: String @@ -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() ) } @@ -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) @@ -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) @@ -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 + ) + } } diff --git a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index 7e30b20aed..3dabf9afae 100644 --- a/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido-core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -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 ) } diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/Fido2ApiClient.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/Fido2ApiClient.java index 1be460796f..978da0391e 100644 --- a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/Fido2ApiClient.java +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/Fido2ApiClient.java @@ -8,7 +8,6 @@ package com.google.android.gms.fido.fido2; -import android.app.Activity; import android.app.PendingIntent; import android.content.Context; diff --git a/play-services-fido/src/main/java/org/microg/gms/fido/fido2/Fido2PrivilegedGmsClient.java b/play-services-fido/src/main/java/org/microg/gms/fido/fido2/Fido2PrivilegedGmsClient.java index dba8e9fd1c..1044f94ed7 100644 --- a/play-services-fido/src/main/java/org/microg/gms/fido/fido2/Fido2PrivilegedGmsClient.java +++ b/play-services-fido/src/main/java/org/microg/gms/fido/fido2/Fido2PrivilegedGmsClient.java @@ -22,7 +22,7 @@ public class Fido2PrivilegedGmsClient extends GmsClient { 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; }