1package com.peachpasswords.updates23import java.util.Base644import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters5import org.bouncycastle.crypto.signers.Ed25519Signer6import org.junit.Assert.assertEquals7import org.junit.Assert.assertThrows8import org.junit.Test9import com.peachpasswords.sync.verifyEd2551910import com.peachpasswords.sync.hexToBytes1112class UpdateMetadataSignatureTest {13 private val seed = ByteArray(32) { 7 }14 private val privateKey = Ed25519PrivateKeyParameters(seed, 0)15 private val publicKeyHex = privateKey.generatePublicKey().encoded.joinToString("") { "%02x".format(it) }16 private val version = "2.14.97"17 private val platform = ANDROID_PLATFORM_KEY18 private val url = "https://kepr.uk/peach-android/releases/$version/peach-android-$platform"19 private val checksum = "a".repeat(64)2021 private fun signedBody(22 signatureOverride: String? = null,23 urlOverride: String = url,24 signingUrl: String = urlOverride,25 ): String {26 val payload = canonicalReleaseMetadataPayload(version, platform, signingUrl, checksum, "koh-offer")27 val signer = Ed25519Signer().apply {28 init(true, privateKey)29 update(payload, 0, payload.size)30 }31 val signature = signatureOverride ?: Base64.getEncoder().encodeToString(signer.generateSignature())32 return """33 {"version":"$version","platforms":{"$platform":{34 "url":"$urlOverride","blake3":"$checksum",35 "provenance":{"version":"$version","platform":"$platform","checksum":"blake3:$checksum","source":"koh-offer"},36 "signature":"$signature"}}}37 """.trimIndent()38 }3940 @Test41 fun `matching signature is required before manifest is accepted`() {42 val payload = canonicalReleaseMetadataPayload(version, platform, url, checksum, "koh-offer")43 val rawSignature = Ed25519Signer().apply {44 init(true, privateKey)45 update(payload, 0, payload.size)46 }.generateSignature()47 assertEquals(true, verifyEd25519(privateKey.generatePublicKey().encoded, payload, rawSignature))48 assertEquals(64, publicKeyHex.length)49 val encoded = Base64.getEncoder().encodeToString(rawSignature)50 assertEquals(true, verifyEd25519(hexToBytes(publicKeyHex), payload, Base64.getDecoder().decode(encoded)))51 val parsed = parseSignedReleaseManifest(signedBody(signatureOverride = encoded), publicKeyHex = publicKeyHex)52 assertEquals(url, parsed.url)53 assertEquals("koh-offer", parsed.provenanceSource)54 }5556 @Test57 fun `unsigned metadata is rejected`() {58 assertThrows(ManifestFormatException::class.java) {59 parseSignedReleaseManifest(signedBody(signatureOverride = ""), publicKeyHex = publicKeyHex)60 }61 }6263 @Test64 fun `alternate URL under a valid checksum is rejected`() {65 val body = signedBody(urlOverride = "https://evil.invalid/release.apk", signingUrl = url)66 assertThrows(ManifestFormatException::class.java) {67 parseSignedReleaseManifest(body, publicKeyHex = publicKeyHex)68 }69 }7071 @Test72 fun `unknown key is rejected`() {73 assertThrows(ManifestFormatException::class.java) {74 parseSignedReleaseManifest(signedBody(), publicKeyHex = "01".repeat(32))75 }76 }77}