PasskeyCallerOriginTest.kt

4.1 KB · raw

1package com.peachpasswords.passkeys23import org.junit.Assert.assertEquals4import org.junit.Assert.assertFalse5import org.junit.Assert.assertTrue6import org.junit.Test78/**9 * F1/F2: caller-origin resolution policy — which callers may register for an10 * rpId and which may obtain a stored credential.11 */12class PasskeyCallerOriginTest {1314    @Test fun apk_key_hash_origin_matches_webauthn_l3_format() {15        // sha256("cert") = known digest; asserts prefix + b64url-no-padding.16        val origin = PasskeyCallerOrigin.apkKeyHashOrigin("cert".toByteArray())17        assertTrue(origin.startsWith("android:apk-key-hash:"))18        val digest = origin.removePrefix("android:apk-key-hash:")19        assertFalse(digest.contains('='))20        assertEquals(43, digest.length) // 32 bytes -> 43 b64url chars unpadded21    }2223    @Test fun rp_id_matches_origin_subdomain_semantics() {24        assertTrue(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "https://example.com"))25        assertTrue(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "https://login.example.com"))26        // rpId may not claim a sibling subdomain's host.27        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("login.example.com", "https://example.com"))28        // Suffix matching is dot-anchored: "ample.com" is not a parent of example.com.29        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("ample.com", "https://example.com"))30        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "https://evilexample.com"))31        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "https://evil-example.com"))32    }3334    @Test fun rp_id_public_suffixes_are_rejected() {35        // PSL guard: an rpId that is itself a public suffix must not match.36        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("co.uk", "https://bank.co.uk"))37    }3839    @Test fun non_web_origins_never_match_rp_ids() {40        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "android:apk-key-hash:AAAA"))41        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "not-an-origin"))42    }4344    @Test fun web_callers_pass_rp_id_domain_gate_regardless_of_binding() {45        // Legacy (null) and web-bound credentials both serve a verified web46        // caller inside the rpId domain.47        assertTrue(PasskeyCallerOrigin.canServeCredential(null, "example.com", "https://example.com"))48        assertTrue(PasskeyCallerOrigin.canServeCredential("https://example.com", "example.com", "https://login.example.com"))49        // A web caller from another domain gets nothing.50        assertFalse(PasskeyCallerOrigin.canServeCredential(null, "example.com", "https://evil.com"))51    }5253    @Test fun native_callers_only_get_their_own_bound_credentials() {54        val appA = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"55        val appB = "android:apk-key-hash:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"56        assertTrue(PasskeyCallerOrigin.canServeCredential(appA, "example.com", appA))57        assertFalse(PasskeyCallerOrigin.canServeCredential(appA, "example.com", appB))58        // Legacy credentials are never released to native-app callers.59        assertFalse(PasskeyCallerOrigin.canServeCredential(null, "example.com", appA))60    }6162    @Test fun browser_allowlist_requires_both_package_and_signing_certificate() {63        val fingerprint = PasskeyCallerOrigin.certificateFingerprint("browser-cert".toByteArray())64        val allowlist = """65            {"apps":[{"info":{"package_name":"com.example.browser","signatures":[{"cert_fingerprint_sha256":"$fingerprint"}]}}]}66        """.trimIndent()6768        assertTrue(69            PasskeyCallerOrigin.isAllowlistedBrowser(70                "com.example.browser", setOf(fingerprint.lowercase()), allowlist,71            ),72        )73        assertFalse(74            PasskeyCallerOrigin.isAllowlistedBrowser(75                "com.example.malicious", setOf(fingerprint), allowlist,76            ),77        )78        assertFalse(79            PasskeyCallerOrigin.isAllowlistedBrowser(80                "com.example.browser", setOf("00:11:22"), allowlist,81            ),82        )83    }84}