1package com.peachpasswords.passkeys23import org.junit.Assert.assertEquals4import org.junit.Assert.assertFalse5import org.junit.Assert.assertTrue6import org.junit.Test789101112class PasskeyCallerOriginTest {1314 @Test fun apk_key_hash_origin_matches_webauthn_l3_format() {15 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) 21 }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 27 assertFalse(PasskeyCallerOrigin.rpIdMatchesOrigin("login.example.com", "https://example.com"))28 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 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 46 47 assertTrue(PasskeyCallerOrigin.canServeCredential(null, "example.com", "https://example.com"))48 assertTrue(PasskeyCallerOrigin.canServeCredential("https://example.com", "example.com", "https://login.example.com"))49 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 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}