DeviceManifestSupportTest.kt

5.4 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertNotNull4import org.junit.Assert.assertNull5import org.junit.Test6import org.junit.Assert.assertEquals78/**9 * Bounds tests for [verifyDeviceManifest]. The arithmetic fix lives in the10 * verify path itself; these tests pin the inputs that used to slip past it.11 */12class DeviceManifestSupportTest {13    private val master = ByteArray(32) { it.toByte() }14    private val signing = deriveSyncSigningKeypair(master, "issuer")1516    private fun validEntry(deviceId: String = "peer", pairedAt: Long = 1L) = ManifestDeviceEntry(17        deviceId = deviceId,18        deviceName = "Peer",19        deviceType = "android",20        longTermPublicKeyHex = "aa".repeat(32),21        signingPublicKeyHex = "bb".repeat(32),22        pairedAt = pairedAt,23    )2425    private fun manifestWith(issuedAt: Long, devices: List<ManifestDeviceEntry> = listOf(validEntry())) =26        buildSignedDeviceManifest(27            issuerDeviceId = "issuer",28            devices = devices,29            signingPrivateKey = signing.privateKey,30            nowMs = issuedAt,31        )3233    @Test fun rejects_issued_at_long_min_value() {34        // abs(Long.MIN_VALUE) is still negative, so the legacy abs()-based gate35        // accepted this. The new two-sided bound must reject it.36        val manifest = manifestWith(issuedAt = Long.MIN_VALUE)37        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_700_000_000_000L))38    }3940    @Test fun rejects_issued_at_too_old() {41        val manifest = manifestWith(issuedAt = 1L)42        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_700_000_000_000L))43    }4445    @Test fun rejects_issued_at_too_far_in_future() {46        val now = 1_700_000_000_000L47        // 5 min + 1 ms past the default freshness window.48        val manifest = manifestWith(issuedAt = now + 5 * 60_000L + 1L)49        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = now))50    }5152    @Test fun accepts_issued_at_within_window() {53        val now = 1_700_000_000_000L54        val manifest = manifestWith(issuedAt = now)55        assertNotNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = now))56    }5758    @Test fun rejects_oversized_device_id_string() {59        val manifest = manifestWith(60            issuedAt = 1_000L,61            devices = listOf(validEntry(deviceId = "x".repeat(10_000))),62        )63        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L))64    }6566    @Test fun caps_oversized_device_name_at_verify() {67        // Sign the long name RAW (build-side normalise would cap it) so this68        // pins the verify-side policy: admit and truncate at the shared69        // 64-char bound, matching the browser — not reject.70        val now = 1_700_000_000_000L71        val body = DeviceManifestBody(72            manifestVersion = DEVICE_MANIFEST_VERSION,73            issuerDeviceId = "issuer",74            issuedAt = now,75            devices = listOf(validEntry().copy(deviceName = "N".repeat(200))),76            revoked = emptyList(),77        )78        val signature = signEd25519(79            signing.privateKey,80            canonicalDeviceManifestBody(body).toByteArray(Charsets.UTF_8),81        )82        val verified = verifyDeviceManifest(83            SignedDeviceManifest(body, pubkeyToHex(signature)),84            signing.publicKeyRaw,85            nowMs = now,86        )87        assertNotNull(verified)88        assertEquals("N".repeat(MAX_DEVICE_NAME_CHARS), verified!!.devices[0].deviceName)89    }9091    @Test fun caps_device_name_at_build() {92        val manifest = manifestWith(93            issuedAt = 1_000L,94            devices = listOf(validEntry().copy(deviceName = "B".repeat(200))),95        )96        assertEquals("B".repeat(MAX_DEVICE_NAME_CHARS), manifest.body.devices[0].deviceName)97    }9899    @Test fun rejects_negative_paired_at() {100        val manifest = manifestWith(101            issuedAt = 1_000L,102            devices = listOf(validEntry(pairedAt = -1L)),103        )104        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L))105    }106107    @Test fun rejects_too_many_devices() {108        val manifest = buildSignedDeviceManifest(109            issuerDeviceId = "issuer",110            devices = List(300) { validEntry(deviceId = "peer-$it") },111            signingPrivateKey = signing.privateKey,112            nowMs = 1_000L,113        )114        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L))115    }116117    @Test fun rejects_oversized_issuer_device_id() {118        val manifest = manifestWith(issuedAt = 1_000L)119        val oversized = manifest.copy(body = manifest.body.copy(issuerDeviceId = "y".repeat(2_000)))120        assertNull(verifyDeviceManifest(oversized, signing.publicKeyRaw, nowMs = 1_000L))121    }122123    @Test fun rejects_negative_revoked_at() {124        val manifest = buildSignedDeviceManifest(125            issuerDeviceId = "issuer",126            devices = emptyList(),127            signingPrivateKey = signing.privateKey,128            nowMs = 1_000L,129            revoked = listOf(130                DeviceRevocation(131                    deviceId = "peer",132                    longTermPublicKeyHex = "aa".repeat(32),133                    revokedAt = -1L,134                    revokedByDeviceId = "issuer",135                ),136            ),137        )138        assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, nowMs = 1_000L))139    }140}