LicenseCodecTest.kt

4.9 KB · raw

1package com.peachpasswords.license23import java.math.BigInteger4import java.security.SecureRandom5import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator6import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters7import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters8import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters9import org.bouncycastle.crypto.signers.Ed25519Signer10import org.junit.Assert.assertEquals11import org.junit.Assert.assertFalse12import org.junit.Assert.assertNotNull13import org.junit.Assert.assertTrue14import org.junit.Test1516/**17 * PB-26-147: nothing covered the licence validation boundary — the one write18 * path whose previous defect was a missing validation call.19 *20 * The verifier's public key is a parameter (defaulting to the compiled-in key),21 * so these tests mint blobs with their own keypair. That is what makes the22 * rejections meaningful: a verifier that rejected everything would pass a23 * negative-only suite.24 */25class LicenseCodecTest {2627    private val alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"2829    private class KeyPairFixture {30        val privateKey: Ed25519PrivateKeyParameters31        val publicKeyHex: String32        constructor() {33            val generator = Ed25519KeyPairGenerator()34            generator.init(Ed25519KeyGenerationParameters(SecureRandom()))35            val pair = generator.generateKeyPair()36            privateKey = pair.private as Ed25519PrivateKeyParameters37            publicKeyHex = (pair.public as Ed25519PublicKeyParameters).encoded.joinToString("") { "%02x".format(it) }38        }3940        fun sign(payload: ByteArray): ByteArray {41            val signer = Ed25519Signer()42            signer.init(true, privateKey)43            signer.update(payload, 0, payload.size)44            return signer.generateSignature()45        }46    }4748    private fun tagPayload(flipProduct: Boolean = false, flipPlan: Boolean = false, flipVersion: Boolean = false) =49        ByteArray(LicenseCodec.PAYLOAD_SIZE).also {50            it[0] = if (flipProduct) 0x02 else LicenseCodec.PRODUCT_TAG51            it[1] = if (flipPlan) 0x02 else LicenseCodec.PLAN_TAG52            it[2] = if (flipVersion) 0x02 else LicenseCodec.VERSION_TAG53            it[7] = 20 // issue date, days54        }5556    private fun key(payload: ByteArray, signature: ByteArray): String =57        "PPK-" + base58Encode(payload + signature)5859    private fun base58Encode(bytes: ByteArray): String {60        var n = BigInteger(1, bytes)61        val out = StringBuilder()62        while (n > BigInteger.ZERO) {63            val (quotient, remainder) = n.divideAndRemainder(BigInteger.valueOf(58))64            out.append(alphabet[remainder.toInt()])65            n = quotient66        }67        return out.reverse().toString()68    }6970    @Test fun a_correctly_signed_blob_is_accepted() {71        val fixture = KeyPairFixture()72        val payload = tagPayload()73        val license = key(payload, fixture.sign(payload))7475        assertTrue("fixture must be a valid licence shape", license.length - 4 <= 104)76        assertTrue(LicenseCodec.validate(license, fixture.publicKeyHex))77        assertNotNull(LicenseCodec.issuedAtMillis(license))78    }7980    @Test fun a_flipped_signature_byte_is_rejected() {81        val fixture = KeyPairFixture()82        val payload = tagPayload()83        val signature = fixture.sign(payload)84        signature[10] = (signature[10].toInt() xor 0x01).toByte()8586        assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex))87    }8889    @Test fun a_mutated_payload_is_rejected() {90        val fixture = KeyPairFixture()91        val payload = tagPayload()92        val signature = fixture.sign(payload)93        payload[7] = (payload[7] + 1).toByte() // issue date moved after signing9495        assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex))96    }9798    @Test fun a_wrong_product_plan_or_version_tag_is_rejected_before_signature_work() {99        val fixture = KeyPairFixture()100        listOf(101            tagPayload(flipProduct = true),102            tagPayload(flipPlan = true),103            tagPayload(flipVersion = true),104        ).forEach { payload ->105            // Correctly signed, so only the tag check can reject it.106            assertFalse(LicenseCodec.validate(key(payload, fixture.sign(payload)), fixture.publicKeyHex))107        }108    }109110    @Test fun over_length_and_illegal_character_input_is_rejected() {111        val fixture = KeyPairFixture()112        val payload = tagPayload()113        val valid = key(payload, fixture.sign(payload))114115        assertFalse("no PPK- prefix", LicenseCodec.validate(valid.removePrefix("PPK-"), fixture.publicKeyHex))116        assertFalse("over-length body", LicenseCodec.validate("PPK-" + "1".repeat(400), fixture.publicKeyHex))117        assertFalse("illegal base58 character", LicenseCodec.validate("PPK-0OIl" + valid, fixture.publicKeyHex))118        assertEquals(null, LicenseCodec.issuedAtMillis("PPK-0OIl"))119    }120}