PasskeyCallerOriginTest.kt

4.6 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 plaintext_http_origins_never_match_or_serve_credentials() {54        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "http://example.com"))55        assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("example.com", "HTTP://login.example.com"))56        assertFalse(PasskeyCallerOrigin.canServeCredential(null, "example.com", "http://example.com"))57        assertFalse(58            PasskeyCallerOrigin.canServeCredential(59                "https://example.com", "example.com", "http://example.com",60            ),61        )62    }6364    @Test fun native_callers_only_get_their_own_bound_credentials() {65        val appA = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"66        val appB = "android:apk-key-hash:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"67        assertTrue(PasskeyCallerOrigin.canServeCredential(appA, "example.com", appA))68        assertFalse(PasskeyCallerOrigin.canServeCredential(appA, "example.com", appB))69        // Legacy credentials are never released to native-app callers.70        assertFalse(PasskeyCallerOrigin.canServeCredential(null, "example.com", appA))71    }7273    @Test fun browser_allowlist_requires_both_package_and_signing_certificate() {74        val fingerprint = PasskeyCallerOrigin.certificateFingerprint("browser-cert".toByteArray())75        val allowlist = """76            {"apps":[{"info":{"package_name":"com.example.browser","signatures":[{"cert_fingerprint_sha256":"$fingerprint"}]}}]}77        """.trimIndent()7879        assertTrue(80            PasskeyCallerOrigin.isAllowlistedBrowser(81                "com.example.browser", setOf(fingerprint.lowercase()), allowlist,82            ),83        )84        assertFalse(85            PasskeyCallerOrigin.isAllowlistedBrowser(86                "com.example.malicious", setOf(fingerprint), allowlist,87            ),88        )89        assertFalse(90            PasskeyCallerOrigin.isAllowlistedBrowser(91                "com.example.browser", setOf("00:11:22"), allowlist,92            ),93        )94    }95}