package io.flamenet.e2e import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue /** * ⚠️ The seed→public derivation is checked against the **RFC 8032 §7.1** * vectors, not against a round-trip through our own code. A round-trip proves * only that this file agrees with itself, and every wrong clamp, endianness and * sign convention agrees with itself perfectly. These vectors are what say the * address a user gets here is the address they get on iOS and on the web. */ class Ed25519Test { private fun hex(s: String) = ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() } /** seed, expected public key, message, expected signature. */ private val vectors = listOf( Triple( "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "" to "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b", ), Triple( "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72" to "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00", ), Triple( "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", "af82" to "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a", ), ) @Test fun `a seed derives the public key RFC 8032 says it must`() { for ((seed, expected, _) in vectors) { val pair = Ed25519.fromSeed(hex(seed)) assertContentEquals(hex(expected), pair.publicKey, "seed $seed derived the wrong public key") } } @Test fun `signatures match the published vectors byte for byte`() { for ((seed, _, msgSig) in vectors) { val (message, signature) = msgSig val pair = Ed25519.fromSeed(hex(seed)) assertContentEquals(hex(signature), Ed25519.sign(pair, hex(message))) } } @Test fun `the published signatures verify`() { for ((_, publicKey, msgSig) in vectors) { val (message, signature) = msgSig assertTrue(Ed25519.verify(hex(publicKey), hex(signature), hex(message))) } } @Test fun `a signature does not verify under a different key`() { val (_, otherKey, _) = vectors[1] val (_, _, msgSig) = vectors[0] assertFalse(Ed25519.verify(hex(otherKey), hex(msgSig.second), hex(msgSig.first))) } @Test fun `a flipped bit anywhere is refused`() { val (seed, publicKey, _) = vectors[2] val pair = Ed25519.fromSeed(hex(seed)) val message = "prekey bytes".toByteArray() val signature = Ed25519.sign(pair, message) for (i in signature.indices step 7) { val bent = signature.copyOf() bent[i] = (bent[i].toInt() xor 0x01).toByte() assertFalse(Ed25519.verify(hex(publicKey), bent, message), "byte $i went undetected") } val bentMessage = message.copyOf().also { it[0] = (it[0].toInt() xor 1).toByte() } assertFalse(Ed25519.verify(hex(publicKey), signature, bentMessage)) } /** * Garbage from the wire must be a `false`, not an exception. A caller that * has to catch around every verification eventually catches one badly, and * a swallowed throw reads as a pass. */ @Test fun `malformed input verifies as false rather than throwing`() { val (_, publicKey, msgSig) = vectors[0] assertFalse(Ed25519.verify(hex(publicKey), ByteArray(0), ByteArray(0))) assertFalse(Ed25519.verify(hex(publicKey), ByteArray(64), ByteArray(0))) assertFalse(Ed25519.verify(ByteArray(31), hex(msgSig.second), ByteArray(0))) assertFalse(Ed25519.verify(ByteArray(32), hex(msgSig.second), ByteArray(0))) } @Test fun `a generated keypair round-trips through its raw encoding`() { val pair = Ed25519.generate() assertEquals(32, pair.publicKey.size) val message = "round trip".toByteArray() assertTrue(Ed25519.verify(pair.publicKey, Ed25519.sign(pair, message), message)) } }