From 212dfa6046d6342c41cc1fdc4e202ba229c0c34d Mon Sep 17 00:00:00 2001 From: mimi89999 Date: Thu, 18 Dec 2025 16:56:10 +0100 Subject: [PATCH] Add WebAuthn Level 2 response fields --- app/src/main/java/pl/lebihan/authnkey/CTAP.kt | 15 ++++ .../authnkey/CredentialProviderActivity.kt | 82 ++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/lebihan/authnkey/CTAP.kt b/app/src/main/java/pl/lebihan/authnkey/CTAP.kt index cd597b2..a0d4215 100644 --- a/app/src/main/java/pl/lebihan/authnkey/CTAP.kt +++ b/app/src/main/java/pl/lebihan/authnkey/CTAP.kt @@ -30,6 +30,7 @@ data class DeviceInfo( } object CTAP { + // Commands const val CMD_MAKE_CREDENTIAL = 0x01 const val CMD_GET_ASSERTION = 0x02 const val CMD_GET_INFO = 0x04 @@ -48,6 +49,20 @@ object CTAP { const val PIN_CMD_CHANGE_PIN = 0x04 const val PIN_CMD_GET_PIN_TOKEN = 0x05 + // AuthData flags + const val AUTH_DATA_FLAG_UP = 0x01 // User present + const val AUTH_DATA_FLAG_UV = 0x04 // User verified + const val AUTH_DATA_FLAG_AT = 0x40 // Attested credential data present + const val AUTH_DATA_FLAG_ED = 0x80 // Extension data present + + // COSE key types + const val COSE_KTY_OKP = 1 + const val COSE_KTY_EC2 = 2 + + // COSE curves + const val COSE_CRV_P256 = 1 + const val COSE_CRV_ED25519 = 6 + private const val STATUS_SUCCESS: Byte = 0x00 enum class Error(val code: Int) { diff --git a/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt b/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt index 38022ee..47600c3 100644 --- a/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt +++ b/app/src/main/java/pl/lebihan/authnkey/CredentialProviderActivity.kt @@ -672,6 +672,19 @@ class CredentialProviderActivity : AppCompatActivity() { put("transports", org.json.JSONArray().apply { put(transport.transportType.webauthnName) }) + put("authenticatorData", Base64.encodeToString( + makeCredResult.authData, + Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + )) + extractPublicKeyAlgorithm(makeCredResult.authData)?.let { alg -> + put("publicKeyAlgorithm", alg) + } + extractPublicKeySpki(makeCredResult.authData)?.let { spki -> + put("publicKey", Base64.encodeToString( + spki, + Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + )) + } }) put("clientExtensionResults", JSONObject().apply { if (credPropsRequested) { @@ -977,7 +990,7 @@ class CredentialProviderActivity : AppCompatActivity() { } val flags = authData[32].toInt() and 0xFF - val hasAttestedCredData = (flags and 0x40) != 0 + val hasAttestedCredData = (flags and CTAP.AUTH_DATA_FLAG_AT) != 0 if (!hasAttestedCredData) { return ByteArray(0) @@ -992,6 +1005,60 @@ class CredentialProviderActivity : AppCompatActivity() { return authData.sliceArray(credIdOffset until credIdOffset + credIdLength) } + private fun extractPublicKeyAlgorithm(authData: ByteArray): Int? { + val coseKey = extractCoseKeyFromAuthData(authData) ?: return null + return (coseKey[3L] as? Number)?.toInt() + } + + private fun extractPublicKeySpki(authData: ByteArray): ByteArray? { + val coseKey = extractCoseKeyFromAuthData(authData) ?: return null + + val kty = (coseKey[1L] as? Number)?.toInt() ?: return null + val crv = (coseKey[-1L] as? Number)?.toInt() ?: return null + + return when (kty) { + CTAP.COSE_KTY_EC2 -> encodeEc2KeyAsSpki(coseKey, crv) + CTAP.COSE_KTY_OKP -> encodeOkpKeyAsSpki(coseKey, crv) + else -> null + } + } + + private fun extractCoseKeyFromAuthData(authData: ByteArray): Map<*, *>? { + if (authData.size < 55) return null + + val flags = authData[32].toInt() and 0xFF + if ((flags and CTAP.AUTH_DATA_FLAG_AT) == 0) return null + + val credIdLenOffset = 32 + 1 + 4 + 16 + val credIdLen = ((authData[credIdLenOffset].toInt() and 0xFF) shl 8) or + (authData[credIdLenOffset + 1].toInt() and 0xFF) + + val coseKeyOffset = credIdLenOffset + 2 + credIdLen + return CborDecoder.decode(authData.sliceArray(coseKeyOffset until authData.size)) as? Map<*, *> + } + + private fun encodeEc2KeyAsSpki(coseKey: Map<*, *>, crv: Int): ByteArray? { + if (crv != CTAP.COSE_CRV_P256) return null + + val x = coseKey[-2L] as? ByteArray ?: return null + val y = coseKey[-3L] as? ByteArray ?: return null + + val point = byteArrayOf(0x04) + x + y + val bitString = byteArrayOf(0x03, (point.size + 1).toByte(), 0x00) + point + val content = EC2_P256_ALGORITHM_ID + bitString + return byteArrayOf(0x30, content.size.toByte()) + content + } + + private fun encodeOkpKeyAsSpki(coseKey: Map<*, *>, crv: Int): ByteArray? { + if (crv != CTAP.COSE_CRV_ED25519) return null + + val x = coseKey[-2L] as? ByteArray ?: return null + + val bitString = byteArrayOf(0x03, (x.size + 1).toByte(), 0x00) + x + val content = OKP_ED25519_ALGORITHM_ID + bitString + return byteArrayOf(0x30, content.size.toByte()) + content + } + private fun returnCreateResult(responseJson: String) { val response = CreatePublicKeyCredentialResponse(responseJson) val resultData = Intent() @@ -1082,5 +1149,18 @@ class CredentialProviderActivity : AppCompatActivity() { companion object { private const val TAG = "CredProviderActivity" private const val ACTION_USB_PERMISSION = "pl.lebihan.authnkey.CRED_USB_PERMISSION" + + // SPKI AlgorithmIdentifier for EC P-256: OID 1.2.840.10045.2.1 + OID 1.2.840.10045.3.1.7 + private val EC2_P256_ALGORITHM_ID = byteArrayOf( + 0x30, 0x13, + 0x06, 0x07, 0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x02, 0x01, + 0x06, 0x08, 0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x03, 0x01, 0x07 + ) + + // SPKI AlgorithmIdentifier for Ed25519: OID 1.3.101.112 + private val OKP_ED25519_ALGORITHM_ID = byteArrayOf( + 0x30, 0x05, + 0x06, 0x03, 0x2B, 0x65, 0x70 + ) } }