Skip to content

Commit

Permalink
Fido: Allow for PIN-less authentication when no uv is required
Browse files Browse the repository at this point in the history
  • Loading branch information
mar-v-in committed Oct 5, 2022
1 parent 0ade509 commit 4cd7c92
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package org.microg.gms.fido.core.protocol.msgs
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.AsInt32Sequence
import org.microg.gms.fido.core.protocol.AsStringSequence
import org.microg.gms.utils.ToStringHelper

class AuthenticatorGetInfoCommand : Ctap2Command<AuthenticatorGetInfoRequest, AuthenticatorGetInfoResponse>(AuthenticatorGetInfoRequest()) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorGetInfoResponse.decodeFromCbor(obj)
Expand All @@ -31,19 +32,67 @@ class AuthenticatorGetInfoResponse(
val clientPin: Boolean?,
val userPresence: Boolean,
val userVerification: Boolean?,
val pinUvAuthToken: Boolean?,
val noMcGaPermissionsWithClientPin: Boolean,
val largeBlobs: Boolean?,
val enterpriseAttestation: Boolean?,
val bioEnroll: Boolean?,
val userVerificationMgmtPreview: Boolean?,
val uvBioEnroll: Boolean?,
val authenticatorConfigSupported: Boolean?,
val uvAcfg: Boolean?,
val credentialManagementSupported: Boolean?,
val credentialMgmtPreview: Boolean?,
val setMinPINLengthSupported: Boolean?,
val makeCredUvNotRqd: Boolean,
val alwaysUv: Boolean?,
) {
companion object {
fun decodeFromCbor(map: CBORObject?) = Options(
platformDevice = map?.get("plat")?.AsBoolean() == true,
residentKey = map?.get("rk")?.AsBoolean() == true,
clientPin = map?.get("clientPin")?.AsBoolean(),
userPresence = map?.get("up")?.AsBoolean() != false,
userVerification = map?.get("uv")?.AsBoolean()
userVerification = map?.get("uv")?.AsBoolean(),
pinUvAuthToken = map?.get("pinUvAuthToken")?.AsBoolean(),
noMcGaPermissionsWithClientPin = map?.get("noMcGaPermissionsWithClientPin")?.AsBoolean() == true,
largeBlobs = map?.get("largeBlobs")?.AsBoolean(),
enterpriseAttestation = map?.get("ep")?.AsBoolean(),
bioEnroll = map?.get("bioEnroll")?.AsBoolean(),
userVerificationMgmtPreview = map?.get("userVerificationMgmtPreview")?.AsBoolean(),
uvBioEnroll = map?.get("uvBioEnroll")?.AsBoolean(),
authenticatorConfigSupported = map?.get("authnrCfg")?.AsBoolean(),
uvAcfg = map?.get("uvAcfg")?.AsBoolean(),
credentialManagementSupported = map?.get("credMgmt")?.AsBoolean(),
credentialMgmtPreview = map?.get("credentialMgmtPreview")?.AsBoolean(),
setMinPINLengthSupported = map?.get("setMinPINLength")?.AsBoolean(),
makeCredUvNotRqd = map?.get("makeCredUvNotRqd")?.AsBoolean() == true,
alwaysUv = map?.get("alwaysUv")?.AsBoolean(),
)
}

override fun toString(): String {
return "Options(platformDevice=$platformDevice, residentKey=$residentKey, clientPin=$clientPin, userPresence=$userPresence, userVerification=$userVerification)"
return ToStringHelper.name("Options")
.field("platformDevice", platformDevice)
.field("residentKey", residentKey)
.field("clientPin", clientPin)
.field("userPresence", userPresence)
.field("userVerification", userVerification)
.field("pinUvAuthToken", pinUvAuthToken)
.field("noMcGaPermissionsWithClientPin", noMcGaPermissionsWithClientPin)
.field("largeBlobs", largeBlobs)
.field("enterpriseAttestation", enterpriseAttestation)
.field("bioEnroll", bioEnroll)
.field("userVerificationMgmtPreview", userVerificationMgmtPreview)
.field("uvBioEnroll", uvBioEnroll)
.field("authenticatorConfigSupported", authenticatorConfigSupported)
.field("uvAcfg", uvAcfg)
.field("credentialManagementSupported", credentialManagementSupported)
.field("credentialMgmtPreview", credentialMgmtPreview)
.field("setMinPINLengthSupported", setMinPINLengthSupported)
.field("makeCredUvNotRqd", makeCredUvNotRqd)
.field("alwaysUv", alwaysUv)
.end()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,31 @@ import com.google.android.gms.fido.fido2.api.common.ErrorCode
import org.microg.gms.fido.core.RequestHandlingException
import org.microg.gms.fido.core.protocol.msgs.*

const val CAPABILITY_CTAP_1 = 1 shl 0
const val CAPABILITY_CTAP_2 = 1 shl 1
const val CAPABILITY_CTAP_2_1 = 1 shl 2
const val CAPABILITY_CLIENT_PIN = 1 shl 3
const val CAPABILITY_WINK = 1 shl 4
const val CAPABILITY_MAKE_CRED_WITHOUT_UV = 1 shl 5

interface CtapConnection {
val capabilities: Int

val hasCtap1Support: Boolean
get() = capabilities and CAPABILITY_CTAP_1 > 0
val hasCtap2Support: Boolean
get() = capabilities and CAPABILITY_CTAP_2 > 0
val hasCtap21Support: Boolean
get() = capabilities and CAPABILITY_CTAP_2_1 > 0
val hasClientPin: Boolean
get() = capabilities and CAPABILITY_CLIENT_PIN > 0
val hasWinkSupport: Boolean
get() = capabilities and CAPABILITY_WINK > 0
val canMakeCredentialWithoutUserVerification: Boolean
get() = capabilities and CAPABILITY_MAKE_CRED_WITHOUT_UV > 0

suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S
suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S
}

class Ctap2StatusException(val status: Byte) : Exception("Received status ${(status.toInt() and 0xff).toString(16)}")
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package org.microg.gms.fido.core.transport
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement.REQUIRED
import com.upokecenter.cbor.CBORObject
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.*
Expand Down Expand Up @@ -48,7 +50,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
): Pair<AuthenticatorMakeCredentialResponse, ByteArray?> {
val reqOptions = AuthenticatorMakeCredentialRequest.Companion.Options(
options.registerOptions.authenticatorSelection?.requireResidentKey == true,
options.registerOptions.authenticatorSelection?.requireUserVerification == UserVerificationRequirement.REQUIRED
options.registerOptions.authenticatorSelection?.requireUserVerification == REQUIRED
)
val extensions = mutableMapOf<String, CBORObject>()
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
Expand Down Expand Up @@ -144,7 +146,18 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
): AuthenticatorAttestationResponse {
val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
val (response, keyHandle) = when {
connection.hasCtap2Support -> ctap2register(connection, options, clientDataHash)
connection.hasCtap2Support -> {
if (connection.hasCtap1Support &&
!connection.canMakeCredentialWithoutUserVerification && connection.hasClientPin &&
options.registerOptions.authenticatorSelection.requireUserVerification != REQUIRED &&
!options.registerOptions.authenticatorSelection.requireResidentKey
) {
Log.d(TAG, "Using CTAP1/U2F for PIN-less registration")
ctap1register(connection, options, clientDataHash)
} else {
ctap2register(connection, options, clientDataHash)
}
}
connection.hasCtap1Support -> ctap1register(connection, options, clientDataHash)
else -> throw IllegalStateException()
}
Expand All @@ -162,7 +175,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
clientDataHash: ByteArray
): Pair<AuthenticatorGetAssertionResponse, ByteArray?> {
val reqOptions = AuthenticatorGetAssertionRequest.Companion.Options(
userVerification = options.signOptions.requireUserVerification == UserVerificationRequirement.REQUIRED
userVerification = options.signOptions.requireUserVerification == REQUIRED
)
val extensions = mutableMapOf<String, CBORObject>()
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
Expand Down Expand Up @@ -244,7 +257,27 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
): AuthenticatorAssertionResponse {
val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
val (response, credentialId) = when {
connection.hasCtap2Support -> ctap2sign(connection, options, clientDataHash)
connection.hasCtap2Support -> {
try {
ctap2sign(connection, options, clientDataHash)
} catch (e: Ctap2StatusException) {
if (e.status == 0x2e.toByte() &&
connection.hasCtap1Support && connection.hasClientPin &&
options.signOptions.allowList.isNotEmpty() &&
options.signOptions.requireUserVerification != REQUIRED
) {
Log.d(TAG, "Falling back to CTAP1/U2F")
try {
ctap1sign(connection, options, clientDataHash)
} catch (e2: Exception) {
// Throw original exception
throw e
}
} else {
throw e
}
}
}
connection.hasCtap1Support -> ctap1sign(connection, options, clientDataHash)
else -> throw IllegalStateException()
}
Expand All @@ -256,6 +289,10 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
null
)
}

companion object {
const val TAG = "FidoTransportHandler"
}
}

interface TransportHandlerCallback {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,21 @@ import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.CtapConnection
import org.microg.gms.fido.core.transport.*
import org.microg.gms.utils.toBase64

class CtapNfcConnection(
val context: Context,
val tag: Tag
) : CtapConnection {
private val isoDep = IsoDep.get(tag)
private var capabilities: Int = 0

override val hasCtap1Support: Boolean
get() = capabilities and CAPABILITY_CTAP_1 > 0

override val hasCtap2Support: Boolean
get() = capabilities and CAPABILITY_CTAP_2 > 0
override var capabilities: Int = 0

override suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S {
require(hasCtap1Support)
Log.d(TAG, "Send command: ${command.request.apdu.toBase64(Base64.NO_WRAP)}")
Log.d(TAG, "Send CTAP1 command: ${command.request.apdu.toBase64(Base64.NO_WRAP)}")
val (statusCode, payload) = decodeResponseApdu(isoDep.transceive(command.request.apdu))
Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
Log.d(TAG, "Received CTAP1 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
if (statusCode != 0x9000.toShort()) {
throw CtapNfcMessageStatusException(statusCode.toInt() and 0xffff)
}
Expand All @@ -43,13 +37,13 @@ class CtapNfcConnection(
override suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S {
require(hasCtap2Support)
val request = encodeCommandApdu(0x80.toByte(), 0x10, 0x00, 0x00, byteArrayOf(command.request.commandByte) + command.request.payload, extended = true)
Log.d(TAG, "Send command: ${request.toBase64(Base64.NO_WRAP)}")
Log.d(TAG, "Send CTAP2 command: ${request.toBase64(Base64.NO_WRAP)}")
var (statusCode, payload) = decodeResponseApdu(isoDep.transceive(request))
Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
while (statusCode == 0x9100.toShort()) {
Log.d(TAG, "Sending GETRESPONSE")
val res = decodeResponseApdu(isoDep.transceive(encodeCommandApdu(0x00, 0xC0.toByte(), 0x00,0x00)))
Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
statusCode = res.first
payload = res.second
}
Expand All @@ -59,7 +53,7 @@ class CtapNfcConnection(
require(payload.isNotEmpty())
val ctapStatusCode = payload[0]
if (ctapStatusCode != 0x00.toByte()) {
throw CtapNfcMessageStatusException(ctapStatusCode.toInt() and 0xff)
throw Ctap2StatusException(ctapStatusCode)
}
return command.decodeResponse(payload, 1)
}
Expand All @@ -71,6 +65,14 @@ class CtapNfcConnection(

private fun deselect() = isoDep.transceive(encodeCommandApdu(0x80.toByte(), 0x12, 0x01, 0x02))

private suspend fun fetchCapabilities() {
val response = runCommand(AuthenticatorGetInfoCommand())
Log.d(TAG, "Got info: $response")
capabilities = capabilities or CAPABILITY_CTAP_2 or
(if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0) or
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0)
}

suspend fun open(): Boolean = withContext(Dispatchers.IO) {
isoDep.connect()
val (statusCode, version) = select(FIDO2_AID)
Expand All @@ -79,15 +81,19 @@ class CtapNfcConnection(
when (version.decodeToString()) {
"FIDO_2_0" -> {
capabilities = CAPABILITY_CTAP_2
try {
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
}
true
}
"U2F_V2" -> {
capabilities = CAPABILITY_CTAP_1 or CAPABILITY_CTAP_2
try {
val response = runCommand(AuthenticatorGetInfoCommand())
Log.d(TAG, "Got info: $response")
capabilities = capabilities or (if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0)
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
capabilities = CAPABILITY_CTAP_1
}
true
Expand Down Expand Up @@ -131,9 +137,6 @@ class CtapNfcConnection(
companion object {
const val TAG = "FidoCtapNfcConnection"
private val FIDO2_AID = byteArrayOf(0xA0.toByte(), 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01)
private const val CAPABILITY_CTAP_1 = 1
private const val CAPABILITY_CTAP_2 = 2
private const val CAPABILITY_CTAP_2_1 = 4
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,6 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac

val credentialId = candidates.first()
val keyId = credentialId.data

val (x, y) = (credentialId.publicKey as ECPublicKey).w.let { it.affineX to it.affineY }
val authenticatorData = getAuthenticatorData(options.rpId, null)

val signature = getActiveSignature(options, callerPackage, keyId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.CtapConnection
import org.microg.gms.fido.core.transport.*
import org.microg.gms.fido.core.transport.usb.endpoints
import org.microg.gms.fido.core.transport.usb.initialize
import org.microg.gms.fido.core.transport.usb.usbManager
import org.microg.gms.fido.core.transport.usb.use
import org.microg.gms.utils.toBase64
import java.nio.ByteBuffer
import kotlin.experimental.and
Expand All @@ -31,16 +30,7 @@ class CtapHidConnection(
private val inEndpoint = iface.endpoints.first { it.direction == USB_DIR_IN }
private val outEndpoint = iface.endpoints.first { it.direction == USB_DIR_OUT }
private var channelIdentifier = 0xffffffff.toInt()
private var capabilities: Byte = 0

override val hasCtap1Support: Boolean
get() = capabilities and CtapHidInitResponse.CAPABILITY_NMSG == 0.toByte()

override val hasCtap2Support: Boolean
get() = capabilities and CtapHidInitResponse.CAPABILITY_CBOR > 0

val hasWinkSupport: Boolean
get() = capabilities and CtapHidInitResponse.CAPABILITY_WINK > 0
override var capabilities: Int = 0

suspend fun open(): Boolean {
Log.d(TAG, "Opening connection")
Expand All @@ -59,7 +49,18 @@ class CtapHidConnection(
return false
}
channelIdentifier = initResponse.channelId
capabilities = initResponse.capabilities
val caps = initResponse.capabilities
capabilities = 0 or
(if (caps and CtapHidInitResponse.CAPABILITY_NMSG == 0.toByte()) CAPABILITY_CTAP_1 else 0) or
(if (caps and CtapHidInitResponse.CAPABILITY_CBOR > 0) CAPABILITY_CTAP_2 else 0) or
(if (caps and CtapHidInitResponse.CAPABILITY_WINK > 0) CAPABILITY_WINK else 0)
if (hasCtap2Support) {
try {
fetchCapabilities()
} catch (e: Exception) {
Log.w(TAG, e)
}
}
return true
}

Expand All @@ -70,6 +71,14 @@ class CtapHidConnection(
capabilities = 0
}

private suspend fun fetchCapabilities() {
val response = runCommand(AuthenticatorGetInfoCommand())
Log.d(TAG, "Got info: $response")
capabilities = capabilities or CAPABILITY_CTAP_2 or
(if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0) or
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0)
}

suspend fun sendRequest(request: CtapHidRequest) {
val connection = connection ?: throw IllegalStateException("Not opened")
val packets = request.encodePackets(channelIdentifier, outEndpoint.maxPacketSize)
Expand Down Expand Up @@ -155,7 +164,7 @@ class CtapHidConnection(
if (response.statusCode == 0x00.toByte()) {
return command.decodeResponse(response.payload)
}
throw CtapHidMessageStatusException(response.statusCode.toInt() and 0xff)
throw Ctap2StatusException(response.statusCode)
}
throw RuntimeException("Unexpected response: $response")
}
Expand Down

0 comments on commit 4cd7c92

Please sign in to comment.