diff --git a/README.md b/README.md index 544e231..489a47f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![License](https://img.shields.io/github/license/JohnLCaron/egk-ec)](https://github.com/JohnLCaron/egk-ec/blob/main/LICENSE.txt) ![GitHub branch checks state](https://img.shields.io/github/actions/workflow/status/JohnLCaron/egk-ec/unit-tests.yml) -![Coverage](https://img.shields.io/badge/coverage-90.3%25%20LOC%20(6905/7650)-blue) +![Coverage](https://img.shields.io/badge/coverage-90.5%25%20LOC%20(6924/7647)-blue) # ElectionGuard-Kotlin Elliptic Curve diff --git a/build.gradle.kts b/build.gradle.kts index 37268c0..60e0556 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,8 @@ dependencies { implementation(libs.bundles.eglib) implementation(libs.bundles.logging) testImplementation(libs.bundles.egtest) - testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation(libs.kotlin.test) + testImplementation(libs.mockk) } tasks.test { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39cd1b9..45b54a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest-version" } kotest-datatest = { module = "io.kotest:kotest-framework-datatest", version.ref = "kotest-version" } kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest-version" } -mockk = { module = "io.mockk:mockk", version = "1.13.7" } +mockk = { module = "io.mockk:mockk", version = "1.13.10" } [bundles] eglib = ["bull-result", "kotlinx-cli", "kotlinx-coroutines-core", "kotlinx-datetime", "kotlinx-serialization-json", "oshai-logging"] diff --git a/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt b/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt index 91e74fa..0798fba 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt @@ -277,7 +277,6 @@ open class KeyCeremonyTrustee( } // If the MAC verifies, Gā„“ decrypts b(Pi(ā„“), 32) = Ci,ā„“,1 āŠ• k1 . - // TODO prove always 32 bytes return ByteArray(32) { c1[it] xor k1[it] } } diff --git a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt index 4f677b9..fa1b569 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt @@ -150,7 +150,7 @@ class RunEncryptBallotTest { val manifest = record.manifest() val publisher = makePublisher(outputDeviceDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) - createDirectories("$outputDeviceDir") + createDirectories(outputDeviceDir) val ballotProvider = RandomBallotProvider(manifest) repeat(nballots) { diff --git a/src/test/kotlin/org/cryptobiotic/eg/tally/LagrangeCoefficientsTest.kt b/src/test/kotlin/org/cryptobiotic/eg/decrypt/LagrangeCoefficientsTest.kt similarity index 65% rename from src/test/kotlin/org/cryptobiotic/eg/tally/LagrangeCoefficientsTest.kt rename to src/test/kotlin/org/cryptobiotic/eg/decrypt/LagrangeCoefficientsTest.kt index ef39c1b..4db58f4 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/tally/LagrangeCoefficientsTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/decrypt/LagrangeCoefficientsTest.kt @@ -1,13 +1,11 @@ -package org.cryptobiotic.eg.tally +package org.cryptobiotic.eg.decrypt import org.cryptobiotic.eg.core.productionGroup -import org.cryptobiotic.eg.decrypt.computeLagrangeCoefficient import kotlin.test.Test import kotlin.test.assertEquals -private val group = productionGroup() - class LagrangeCoefficientsTest { + private val group = productionGroup() @Test fun testLagrangeCoefficientAreIntegral() { @@ -32,7 +30,7 @@ class LagrangeCoefficientsTest { val coeff: Int = computeLagrangeCoefficientInt(coord, others) val numer: Int = computeLagrangeNumerator(others) val denom: Int = computeLagrangeDenominator(coord, others) - val coeffQ = group.computeLagrangeCoefficient(coord, others.map { it}) + val coeffQ = group.computeLagrangeCoefficient(coord, others.map { it }) println("($coord) $coeff == ${numer} / ${denom} rem ${numer % denom} == $coeffQ") if (exact) { assertEquals(0, numer % denom) @@ -40,31 +38,31 @@ class LagrangeCoefficientsTest { } println() } -} -fun computeLagrangeCoefficientInt(coordinate: Int, others: List): Int { - if (others.isEmpty()) { - return 1 - } - val numerator: Int = others.reduce { a, b -> a * b } + fun computeLagrangeCoefficientInt(coordinate: Int, others: List): Int { + if (others.isEmpty()) { + return 1 + } + val numerator: Int = others.reduce { a, b -> a * b } - val diff: List = others.map { degree: Int -> degree - coordinate } - val denominator = diff.reduce { a, b -> a * b } + val diff: List = others.map { degree: Int -> degree - coordinate } + val denominator = diff.reduce { a, b -> a * b } - return numerator / denominator -} + return numerator / denominator + } -fun computeLagrangeNumerator(others: List): Int { - if (others.isEmpty()) { - return 1 + fun computeLagrangeNumerator(others: List): Int { + if (others.isEmpty()) { + return 1 + } + return others.reduce { a, b -> a * b } } - return others.reduce { a, b -> a * b } -} -fun computeLagrangeDenominator(coordinate: Int, others: List): Int { - if (others.isEmpty()) { - return 1 + fun computeLagrangeDenominator(coordinate: Int, others: List): Int { + if (others.isEmpty()) { + return 1 + } + val diff: List = others.map { degree: Int -> degree - coordinate } + return diff.reduce { a, b -> a * b } } - val diff: List = others.map { degree: Int -> degree - coordinate } - return diff.reduce { a, b -> a * b } } \ No newline at end of file diff --git a/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyMockTest.kt b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyMockTest.kt new file mode 100644 index 0000000..ec8438c --- /dev/null +++ b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyMockTest.kt @@ -0,0 +1,133 @@ +package org.cryptobiotic.eg.keyceremony + +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.unwrap +import io.mockk.every +import io.mockk.spyk +import org.cryptobiotic.eg.core.generateHashedCiphertext +import org.cryptobiotic.eg.core.productionGroup +import kotlin.test.Test + +import kotlin.test.assertTrue + +class KeyCeremonyMockTest { + val group = productionGroup("P-256") + + @Test + fun testKeyCeremonyOk() { + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + val trustees = listOf(trustee1, trustee2, spy3) + + val result = keyCeremonyExchange(trustees) + assertTrue(result is Ok) + } + + @Test + fun testKeyCeremonyMockOk() { + val group = productionGroup() + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + every { spy3.encryptedKeyShareFor(trustee1.id()) } answers { + val result1 = trustee3.encryptedKeyShareFor(trustee1.id()) + assertTrue(result1 is Ok) + val ss21 = result1.unwrap() + Ok(EncryptedKeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), ss21.encryptedCoordinate)) + } + val trustees = listOf(trustee1, trustee2, spy3) + + val result = keyCeremonyExchange(trustees) + assertTrue(result is Ok) + } + + @Test + fun testAllowBadEncryptedShare() { + val group = productionGroup() + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + every { spy3.encryptedKeyShareFor(trustee1.id()) } answers { + trustee3.encryptedKeyShareFor(trustee1.id()) // trustee needs to cache + // bad EncryptedShare + Ok(EncryptedKeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), generateHashedCiphertext(group))) + } + val trustees = listOf(trustee1, trustee2, spy3) + val result = keyCeremonyExchange(trustees, true) + println("result = $result") + assertTrue(result is Ok) + } + + @Test + fun testBadEncryptedShare() { + val group = productionGroup() + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + every { spy3.encryptedKeyShareFor(trustee1.id()) } answers { + trustee3.encryptedKeyShareFor(trustee1.id()) // trustee needs to cache + // bad EncryptedShare + Ok(EncryptedKeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), generateHashedCiphertext(group))) + } + val trustees = listOf(trustee1, trustee2, spy3) + val result = keyCeremonyExchange(trustees, false) + println("result = $result") + assertTrue(result is Err) + assertTrue(result.error.contains("Trustee 'id1' couldnt decrypt EncryptedKeyShare for missingGuardianId 'id3'")) + } + + @Test + fun testBadKeySharesAllowFalse() { + val group = productionGroup() + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + every { spy3.encryptedKeyShareFor(trustee1.id()) } answers { + trustee3.encryptedKeyShareFor(trustee1.id()) // trustee needs to cache + // bad EncryptedShare + Ok(EncryptedKeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), generateHashedCiphertext(group))) + } + every { spy3.keyShareFor(trustee1.id()) } answers { + // bad KeyShare + Ok(KeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), group.TWO_MOD_Q)) + } + val trustees = listOf(trustee1, trustee2, spy3) + val result = keyCeremonyExchange(trustees, false) + println("result = $result") + assertTrue(result is Err) + println(result) + assertTrue(result.error.contains("keyCeremonyExchange not complete")) + } + + @Test + fun testBadKeySharesAllowTrue() { + val group = productionGroup() + val trustee1 = KeyCeremonyTrustee(group, "id1", 1, 3, 3) + val trustee2 = KeyCeremonyTrustee(group, "id2", 3, 3, 3) + val trustee3 = KeyCeremonyTrustee(group, "id3", 2, 3, 3) + val spy3 = spyk(trustee3) + every { spy3.encryptedKeyShareFor(trustee1.id()) } answers { + trustee3.encryptedKeyShareFor(trustee1.id()) // trustee needs to cache + // bad EncryptedShare + Ok(EncryptedKeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), generateHashedCiphertext(group))) + } + every { spy3.keyShareFor(trustee1.id()) } answers { + // bad KeyShare + Ok(KeyShare(spy3.xCoordinate(), spy3.id(), trustee1.id(), group.TWO_MOD_Q)) + } + val trustees = listOf(trustee1, trustee2, spy3) + val result = keyCeremonyExchange(trustees, true) + println("result = $result") + assertTrue(result is Err) + println(result) + assertTrue(result.error.contains("keyCeremonyExchange not complete")) + } + +} diff --git a/src/test/kotlin/org/cryptobiotic/eg/tally/RunTallyAccumulationTest.kt b/src/test/kotlin/org/cryptobiotic/eg/tally/RunTallyAccumulationTest.kt index 8aff635..0453f55 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/tally/RunTallyAccumulationTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/tally/RunTallyAccumulationTest.kt @@ -1,15 +1,26 @@ package org.cryptobiotic.eg.tally +import com.github.michaelbull.result.Err import com.github.michaelbull.result.unwrap import org.cryptobiotic.eg.election.EncryptedTally import org.cryptobiotic.eg.cli.RunAccumulateTally +import org.cryptobiotic.eg.cli.RunTrustedTallyDecryption.Companion.readDecryptingTrustees +import org.cryptobiotic.eg.core.GroupContext +import org.cryptobiotic.eg.core.UInt256 import org.cryptobiotic.eg.core.productionGroup +import org.cryptobiotic.eg.decrypt.DecryptingTrusteeIF +import org.cryptobiotic.eg.decrypt.Guardians +import org.cryptobiotic.eg.decrypt.TallyDecryptor +import org.cryptobiotic.eg.election.DecryptedTallyOrBallot +import org.cryptobiotic.eg.election.ElectionInitialized +import org.cryptobiotic.eg.election.EncryptedBallot.BallotState import org.cryptobiotic.eg.publish.makeConsumer +import org.cryptobiotic.util.ErrorMessages import org.cryptobiotic.util.Testing -import kotlin.test.Test -import kotlin.test.assertNotNull +import kotlin.test.* class RunTallyAccumulationTest { + val group = productionGroup("P-256") @Test fun runTallyAccumulationTestJson() { @@ -25,20 +36,132 @@ class RunTallyAccumulationTest { @Test fun runTallyAccumulationTestJsonNoBallots() { - val group = productionGroup() - val consumerIn = makeConsumer("src/test/data/workflow/someAvailableEc") + val inputDir = "src/test/data/workflow/someAvailableEc" + val trusteeDir = "src/test/data/workflow/someAvailableEc/private_data/trustees" + val consumerIn = makeConsumer(inputDir) val initResult = consumerIn.readElectionInitialized() val electionInit = initResult.unwrap() val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes) val accumulator = AccumulateTally(group, manifest, "name", electionInit.extendedBaseHash, electionInit.jointPublicKey) // nothing accumulated - val tally: EncryptedTally = accumulator.build() - assertNotNull(tally) - /* - tally.contests.forEach { it.selections.forEach { - assertEquals( it.encryptedVote ) // show its an encryption of zero - only by decrypting it - }} - */ + val etally: EncryptedTally = accumulator.build() + assertNotNull(etally) + + val tally = decryptTally(group, etally, electionInit, readDecryptingTrustees(inputDir, trusteeDir)) + tally.contests.forEach { + it.selections.forEach { + assertEquals(0, it.tally) + } + } + } + + @Test + fun testAccumulateTally() { + val inputDir = "src/test/data/workflow/someAvailableEc" + val trusteeDir = "src/test/data/workflow/someAvailableEc/private_data/trustees" + val consumerIn = makeConsumer(inputDir) + val initResult = consumerIn.readElectionInitialized() + val electionInit = initResult.unwrap() + val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes) + + val accum = AccumulateTally(group, manifest, "name", electionInit.extendedBaseHash, electionInit.jointPublicKey) + + val errs = ErrorMessages("addCastBallots") + consumerIn.iterateAllCastBallots().forEach { eballot -> + accum.addCastBallot(eballot, errs) + } + assertFalse(errs.hasErrors()) + + val etally: EncryptedTally = accum.build() + val tally = decryptTally(group, etally, electionInit, readDecryptingTrustees(inputDir, trusteeDir)) + + val decryptResult = consumerIn.readDecryptionResult() + assertFalse(decryptResult is Err) + val actualTally = decryptResult.unwrap().decryptedTally + + assertTrue(compareTallies(tally, actualTally, true)) + } + + @Test + fun testAccumulateTallyErrors() { + val inputDir = "src/test/data/workflow/someAvailableEc" + val consumerIn = makeConsumer(inputDir) + val initResult = consumerIn.readElectionInitialized() + val electionInit = initResult.unwrap() + val manifest = consumerIn.makeManifest(electionInit.config.manifestBytes) + + val accum = AccumulateTally(group, manifest, "name", electionInit.extendedBaseHash, electionInit.jointPublicKey) + + val firstBallot = consumerIn.iterateAllCastBallots().iterator().next() + + // not cast + val uballot = firstBallot.copy( state = BallotState.UNKNOWN) + val uerrs = ErrorMessages("addCastBallots") + accum.addCastBallot(uballot, uerrs) + assertTrue(uerrs.hasErrors()) + assertTrue(uerrs.toString().contains("does not have state CAST")) + + // bad electionId + val idballot = firstBallot.copy( electionId = UInt256.random()) + val iderrs = ErrorMessages("addCastBallots") + accum.addCastBallot(idballot, iderrs) + assertTrue(iderrs.hasErrors()) + assertTrue(iderrs.toString().contains("has wrong electionId")) + + // duplicate + val errs = ErrorMessages("addCastBallots") + accum.addCastBallot(firstBallot, errs) + assertFalse(errs.hasErrors()) + accum.addCastBallot(firstBallot, errs) + assertTrue(errs.hasErrors()) + assertTrue(errs.toString().contains("is duplicate")) + } +} + +fun decryptTally( + group: GroupContext, + encryptedTally: EncryptedTally, + electionInit: ElectionInitialized, + decryptingTrustees: List, +): DecryptedTallyOrBallot { + val guardians = Guardians(group, electionInit.guardians) + val decryptor = TallyDecryptor( + group, + electionInit.extendedBaseHash, + electionInit.jointPublicKey, + guardians, + decryptingTrustees, + ) + + return decryptor.decrypt(encryptedTally, ErrorMessages(""))!! +} + +fun compareTallies( + tally1: DecryptedTallyOrBallot, + tally2: DecryptedTallyOrBallot, + diffOnly: Boolean, +): Boolean { + var same = true + println("Compare ${tally1.id} to ${tally2.id}") + val tally2ContestMap = tally2.contests.associateBy { it.contestId } + tally1.contests.sortedBy { it.contestId }.forEach { contest1 -> + if (!diffOnly) println(" Contest ${contest1.contestId}") + val contest2 = tally2ContestMap[contest1.contestId] ?: throw IllegalStateException("Cant find contest ${contest1.contestId}") + val tally2SelectionMap = contest2.selections.associateBy { it.selectionId } + contest1.selections.sortedBy { it.selectionId }.forEach { selection1 -> + val selection2 = tally2SelectionMap[selection1.selectionId] ?: throw IllegalStateException("Cant find selection ${selection1.selectionId}") + val selSame = selection1.tally == selection2.tally + if (!diffOnly) { + println( + " Selection ${selection1.selectionId}: ${selection1.tally} vs ${selection2.tally}" + + if (selSame) "" else "*********" + ) + } else if (!selSame) { + println(" Selection ${contest1.contestId}/${selection1.selectionId}: ${selection1.tally} != ${selection2.tally}") + same = false + } + } } + return same } \ No newline at end of file