package com.peachpasswords.passkeys import kotlinx.serialization.json.Json import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test import java.util.Base64 class PasskeyRegistrationTest { @Test fun builds_es256_registration_response_and_entry() { val result = PasskeyRegistration.build( requestJson = """ {"challenge":"Y2hhbGxlbmdl","rp":{"id":"example.com","name":"Example"},"user":{"id":"dXNlcg","name":"june","displayName":"June"},"pubKeyCredParams":[{"type":"public-key","alg":-7}]} """.trimIndent(), origin = "https://login.example.com", now = 123, ) assertNotNull(result) result!! val passkey = result.entryDraft.passkey!! assertEquals("example.com", passkey.rpId) assertEquals("june", passkey.userName) assertEquals(-7, passkey.algorithm) assertEquals(123, passkey.createdAt) assertTrue(passkey.privateKeyJwk!!.contains("\"d\"")) assertTrue(b64Decode(passkey.credentialId).size == 32) assertTrue(b64Decode(passkey.publicKeyCose).isNotEmpty()) val response = Json.parseToJsonElement(result.responseJson).jsonObject assertEquals(passkey.credentialId, response["id"]?.jsonPrimitive?.contentOrNull) val payload = response["response"]!!.jsonObject assertEquals(-7, payload["publicKeyAlgorithm"]?.jsonPrimitive?.int) assertEquals("internal", payload["transports"]?.jsonArray?.single()?.jsonPrimitive?.contentOrNull) val clientData = Json.parseToJsonElement(String(b64Decode(payload["clientDataJSON"]!!.jsonPrimitive.content), Charsets.UTF_8)).jsonObject assertEquals("webauthn.create", clientData["type"]?.jsonPrimitive?.contentOrNull) assertEquals("https://login.example.com", clientData["origin"]?.jsonPrimitive?.contentOrNull) assertTrue(b64Decode(payload["attestationObject"]!!.jsonPrimitive.content).isNotEmpty()) } @Test fun prefers_eddsa_registration_when_offered() { val result = PasskeyRegistration.build( """{"challenge":"c","rp":{"id":"example.com"},"user":{"id":"u","name":"june"},"pubKeyCredParams":[{"alg":-8},{"alg":-7}]}""", "https://example.com", ) assertNotNull(result) result!! assertEquals(-8, result.entryDraft.passkey?.algorithm) val jwk = Json.parseToJsonElement(result.entryDraft.passkey!!.privateKeyJwk!!).jsonObject assertEquals("OKP", jwk["kty"]?.jsonPrimitive?.contentOrNull) assertEquals("Ed25519", jwk["crv"]?.jsonPrimitive?.contentOrNull) val payload = Json.parseToJsonElement(result.responseJson).jsonObject["response"]!!.jsonObject assertEquals(-8, payload["publicKeyAlgorithm"]?.jsonPrimitive?.int) } @Test fun rejects_requests_without_supported_algorithms() { val result = PasskeyRegistration.build( """{"challenge":"c","rp":{"id":"example.com"},"user":{"id":"u","name":"june"},"pubKeyCredParams":[{"alg":-257}]}""", "https://example.com", ) assertEquals(null, result) } @Test fun registration_writes_backup_eligible_true_and_correct_flag_byte() { val result = PasskeyRegistration.build( requestJson = """ {"challenge":"Y2hhbGxlbmdl","rp":{"id":"example.com","name":"Example"},"user":{"id":"dXNlcg","name":"june","displayName":"June"},"pubKeyCredParams":[{"type":"public-key","alg":-7}]} """.trimIndent(), origin = "https://example.com", ) assertNotNull(result) result!! val passkey = result.entryDraft.passkey!! // Persisted backup state must match the flag byte that's put on the // wire — the audit finding was that these contradicted each other. assertEquals(true, passkey.backupEligible) assertEquals(false, passkey.backupState) val payload = Json.parseToJsonElement(result.responseJson).jsonObject["response"]!!.jsonObject val authData = b64Decode(payload["authenticatorData"]!!.jsonPrimitive.content) // UP(0x01) | UV(0x04) | BE(0x08) | AT(0x40) = 0x4d. BS (0x10) is NOT // set because backupState is false at registration time. assertEquals(0x4d, authData[32].toInt() and 0xff) } private fun b64Decode(value: String): ByteArray = Base64.getUrlDecoder().decode(value.padEnd(((value.length + 3) / 4) * 4, '=')) }