From 1d2d0658d620d35a622367ac2913ab815349650d Mon Sep 17 00:00:00 2001 From: JohnLCaron Date: Sat, 13 Apr 2024 15:10:36 -0600 Subject: [PATCH] Cleanup some TODOs in encrypt code. Add more tests for encryption. Redo verifyConfirmationChain() require sn > 0. add ElectionRecord.consumer() --- README.md | 4 +- htmlReport/index.html | 656 ------------------ .../cryptobiotic/eg/cli/RunBatchEncryption.kt | 3 +- .../cryptobiotic/eg/cli/RunEncryptBallot.kt | 33 +- .../eg/election/PlaintextBallot.kt | 1 + .../eg/encrypt/AddEncryptedBallot.kt | 5 +- .../eg/encrypt/EncryptedBallotChain.kt | 13 +- .../org/cryptobiotic/eg/encrypt/Encryptor.kt | 20 +- .../eg/input/RandomBallotProvider.kt | 2 +- .../cryptobiotic/eg/publish/ElectionRecord.kt | 1 + .../eg/publish/ElectionRecordFactory.kt | 4 + .../org/cryptobiotic/eg/verifier/Verifier.kt | 4 +- .../eg/verifier/VerifyEncryptedBallots.kt | 30 +- .../eg/cli/RunEncryptBallotTest.kt | 120 +++- .../eg/encrypt/AddEncryptedBallotTest.kt | 2 +- .../eg/verifier/VerifyEncryptedBallotsTest.kt | 85 +++ 16 files changed, 267 insertions(+), 716 deletions(-) delete mode 100644 htmlReport/index.html create mode 100644 src/test/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallotsTest.kt diff --git a/README.md b/README.md index 3062333..544e231 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [![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.2%25%20LOC%20(6890/7642)-blue)](https://github.com/JohnLCaron/egk-ec/blob/main/htmlReport/index.html) +![Coverage](https://img.shields.io/badge/coverage-90.3%25%20LOC%20(6905/7650)-blue) # ElectionGuard-Kotlin Elliptic Curve -_last update 04/10/2024_ +_last update 04/13/2024_ EGK Elliptic Curve (egk-ec) is an experimental implementation of [ElectionGuard](https://github.com/microsoft/electionguard), [version 2.0](https://github.com/microsoft/electionguard/releases/download/v2.0/EG_Spec_2_0.pdf), diff --git a/htmlReport/index.html b/htmlReport/index.html deleted file mode 100644 index 8b978e9..0000000 --- a/htmlReport/index.html +++ /dev/null @@ -1,656 +0,0 @@ - - - - - - - Coverage Report > Summary - - - - - - -
- - -

Overall Coverage Summary

- - - - - - - - - - - - - - - -
Package - Class, % - - Method, % - - Branch, % - - Line, % -
all classes - - 83.4% - - - (476/571) - - - - 87.1% - - - (1271/1460) - - - - 66.6% - - - (1795/2696) - - - - 90.2% - - - (6892/7642) - -
- -
-

Coverage Breakdown

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-Package - Class, % - - Method, % - - Branch, % - - Line, % -
org.cryptobiotic.eg.cli - - 60.9% - - - (81/133) - - - - 69.5% - - - (141/203) - - - - 69.3% - - - (201/290) - - - - 90% - - - (1246/1385) - -
org.cryptobiotic.eg.core - - 90.4% - - - (47/52) - - - - 90.1% - - - (172/191) - - - - 73% - - - (251/344) - - - - 89.6% - - - (675/753) - -
org.cryptobiotic.eg.core.ecgroup - - 92.9% - - - (13/14) - - - - 94% - - - (109/116) - - - - 73.1% - - - (152/208) - - - - 93.4% - - - (566/606) - -
org.cryptobiotic.eg.core.intgroup - - 94.4% - - - (17/18) - - - - 85% - - - (113/133) - - - - 60.5% - - - (92/152) - - - - 86.8% - - - (316/364) - -
org.cryptobiotic.eg.decrypt - - 76.9% - - - (20/26) - - - - 84.5% - - - (49/58) - - - - 63.9% - - - (92/144) - - - - 88.7% - - - (345/389) - -
org.cryptobiotic.eg.election - - 93.9% - - - (46/49) - - - - 86.3% - - - (82/95) - - - - 57.4% - - - (170/296) - - - - 89.2% - - - (597/669) - -
org.cryptobiotic.eg.encrypt - - 82.4% - - - (14/17) - - - - 87% - - - (40/46) - - - - 75.5% - - - (71/94) - - - - 90% - - - (341/379) - -
org.cryptobiotic.eg.input - - 90.6% - - - (29/32) - - - - 89.6% - - - (43/48) - - - - 93.8% - - - (105/112) - - - - 94.5% - - - (241/255) - -
org.cryptobiotic.eg.keyceremony - - 56.2% - - - (9/16) - - - - 84.1% - - - (37/44) - - - - 66.9% - - - (87/130) - - - - 89.2% - - - (281/315) - -
org.cryptobiotic.eg.preencrypt - - 100% - - - (16/16) - - - - 86.5% - - - (32/37) - - - - 57.1% - - - (64/112) - - - - 89.3% - - - (327/366) - -
org.cryptobiotic.eg.publish - - 66.7% - - - (10/15) - - - - 83.3% - - - (45/54) - - - - 69.4% - - - (50/72) - - - - 83.9% - - - (156/186) - -
org.cryptobiotic.eg.publish.json - - 93.8% - - - (137/146) - - - - 94.4% - - - (286/303) - - - - 59.3% - - - (254/428) - - - - 93.9% - - - (1208/1287) - -
org.cryptobiotic.eg.tally - - 100% - - - (3/3) - - - - 100% - - - (9/9) - - - - 66.7% - - - (12/18) - - - - 82.2% - - - (37/45) - -
org.cryptobiotic.eg.verifier - - 100% - - - (22/22) - - - - 90.3% - - - (56/62) - - - - 59.9% - - - (151/252) - - - - 83% - - - (401/483) - -
org.cryptobiotic.util - - 100% - - - (12/12) - - - - 93.4% - - - (57/61) - - - - 97.7% - - - (43/44) - - - - 96.9% - - - (155/160) - -
-
- - - - - - - diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt index e23bab1..78abff3 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunBatchEncryption.kt @@ -358,8 +358,7 @@ class RunBatchEncryption { } // coroutines allow parallel encryption at the ballot level - // TODO not possible to do ballot chaining, since the order is indeterminate? - // or do we just have to work harder?? + // TODO not possible to do ballot chaining, since the order is indeterminate? or do we just have to work harder? private fun CoroutineScope.launchEncryptor( id: Int, input: ReceiveChannel, diff --git a/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt index 30b9be8..352cb9b 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallot.kt @@ -8,11 +8,9 @@ import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.required import org.cryptobiotic.eg.core.GroupContext +import org.cryptobiotic.eg.core.pathExists import org.cryptobiotic.eg.election.EncryptedBallot import org.cryptobiotic.eg.encrypt.EncryptedBallotChain -import org.cryptobiotic.eg.encrypt.EncryptedBallotChain.Companion.makeCodeBaux -import org.cryptobiotic.eg.encrypt.EncryptedBallotChain.Companion.writeChain -import org.cryptobiotic.eg.encrypt.EncryptedBallotChain.Companion.terminateChain import org.cryptobiotic.eg.encrypt.Encryptor import org.cryptobiotic.eg.encrypt.submit import org.cryptobiotic.eg.input.ManifestInputValidation @@ -26,6 +24,7 @@ import org.cryptobiotic.util.ErrorMessages * Reads a plaintext ballot from disk and writes its encryption to disk. * Note that this does not allow for benolah challenge, ie voter submits a ballot, gets a confirmation code * (with or without ballot chaining), then decide to challenge or cast. + * It does allow ballot chaining; if chaining then close the chain by calling with ballotFilepath="CLOSE". */ class RunEncryptBallot { @@ -116,22 +115,24 @@ class RunEncryptBallot { } val ballot = result.unwrap() + if (!pathExists(encryptBallotDir)) { + logger.error { "output Directory '$encryptBallotDir' must already exist $result" } + return 4 + } + val chaining = electionInit.config.chainConfirmationCodes val configBaux0 = electionInit.config.configBaux0 var currentChain: EncryptedBallotChain? = null - val codeBaux: ByteArray? + val codeBaux = if (!chaining) { - codeBaux = configBaux0 + configBaux0 } else { val consumerChain = makeConsumer(encryptBallotDir, consumerIn.group) - // this will read in an existing chain, and so recover from machine going down. - val pair = makeCodeBaux(consumerChain, device, encryptBallotDir, configBaux0, electionInit.extendedBaseHash ) - codeBaux = pair.first + // this reads in an existing chain, or starts one. + val pair = EncryptedBallotChain.makeCodeBaux(consumerChain, device, encryptBallotDir, configBaux0, electionInit.extendedBaseHash ) currentChain = pair.second - } - if (codeBaux == null) { - return 4 + pair.first } val errs = ErrorMessages("Encrypt ${ballot.ballotId}") @@ -142,7 +143,7 @@ class RunEncryptBallot { ) if (errs.hasErrors()) { logger.error { errs.toString() } - return 5 + return 6 } val eballot = cballot!!.submit(EncryptedBallot.BallotState.CAST) @@ -154,7 +155,7 @@ class RunEncryptBallot { logger.info { "success encrypted ballot written to '$fileout' " } if (chaining) { - writeChain( + EncryptedBallotChain.writeChain( publisher, encryptBallotDir, ballot.ballotId, @@ -164,7 +165,7 @@ class RunEncryptBallot { } } catch (t: Throwable) { logger.error(t) { " error writing encrypted ballot ${t.message}" } - return 6 + return 7 } return 0 } @@ -178,8 +179,8 @@ class RunEncryptBallot { val chainResult = consumer.readEncryptedBallotChain(device, encryptBallotDir) val retval = if (chainResult is Ok) { val chain = chainResult.unwrap() - val publisher = makePublisher(encryptBallotDir) // TODO not placing it into the device directory? - val termval = terminateChain(publisher, device, encryptBallotDir, chain) + val publisher = makePublisher(encryptBallotDir) + val termval = EncryptedBallotChain.terminateChain(publisher, encryptBallotDir, chain) if (termval != 0) { logger.info { "Cant terminateBallotChain retval=$termval" } } diff --git a/src/main/kotlin/org/cryptobiotic/eg/election/PlaintextBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/election/PlaintextBallot.kt index 54f1134..142518c 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/election/PlaintextBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/election/PlaintextBallot.kt @@ -14,6 +14,7 @@ data class PlaintextBallot( ) { init { require(ballotId.isNotEmpty()) + require((sn == null) || (sn > 0)) } constructor(org: PlaintextBallot, errors: String): diff --git a/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt index 6571d2a..391dc69 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallot.kt @@ -25,7 +25,7 @@ private val logger = KotlinLogging.logger("AddEncryptedBallot") * (with or without ballot chaining), then decide to challenge or cast. */ class AddEncryptedBallot( - val manifest: ManifestIF, // should already be validated + val manifest: ManifestIF, // must already be validated val ballotValidator: BallotInputValidation, val chaining: Boolean, val configBaux0: ByteArray, @@ -82,7 +82,7 @@ class AddEncryptedBallot( extendedBaseHash, ) currentChain = pair.second - pair.first!! // cant be null + pair.first // cant be null } val ciphertextBallot = encryptor.encrypt(ballot, bauxj, errs) @@ -182,7 +182,6 @@ class AddEncryptedBallot( if (chaining) { EncryptedBallotChain.terminateChain( publisher, - device, null, currentChain!! ) diff --git a/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt b/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt index 83195b2..fda3108 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/encrypt/EncryptedBallotChain.kt @@ -3,11 +3,9 @@ package org.cryptobiotic.eg.encrypt import com.github.michaelbull.result.Ok import com.github.michaelbull.result.unwrap import io.github.oshai.kotlinlogging.KotlinLogging -import org.cryptobiotic.eg.core.Base64.fromBase64 import org.cryptobiotic.eg.core.UInt256 import org.cryptobiotic.eg.core.hashFunction import org.cryptobiotic.eg.core.plus -import org.cryptobiotic.eg.core.toUInt256 import org.cryptobiotic.eg.election.EncryptedBallot import org.cryptobiotic.eg.publish.Consumer import org.cryptobiotic.eg.publish.Publisher @@ -46,7 +44,7 @@ data class EncryptedBallotChain( ballotChainOverrideDir: String?, configBaux0: ByteArray, extendedBaseHash: UInt256, - ): Pair { + ): Pair { var chain: EncryptedBallotChain? = null // If chainCodes is true, and configBaux0 is empty, then the device name UTF-8 bytes will be used when creating the @@ -70,8 +68,9 @@ data class EncryptedBallotChain( if (showChain) println( " makeCodeBaux ${codeBaux.contentToString()}") - if (chain == null) + if (chain == null) { chain = EncryptedBallotChain(device, baux0, extendedBaseHash, emptyList(), UInt256.ZERO, null) + } return Pair(codeBaux, chain) } @@ -99,7 +98,6 @@ data class EncryptedBallotChain( // append finalConfirmationCode and close the ballotChain fun terminateChain( publisher: Publisher, - device: String, ballotChainOverrideDir: String?, currentChain: EncryptedBallotChain, ): Int { @@ -126,7 +124,6 @@ data class EncryptedBallotChain( fun assembleChain( consumer: Consumer, device: String, - ballotChainOverrideDir: String?, configBaux0: ByteArray, extendedBaseHash: UInt256, errs: ErrorMessages @@ -137,7 +134,7 @@ data class EncryptedBallotChain( val H0 = hashFunction(extendedBaseHash.bytes, 0x24.toByte(), baux0).bytes + baux0 val start = BallotChainEntry("START", 0) - val encryptedBallots = consumer.iterateEncryptedBallots(device) { true } // TODO ballotChainOverrideDir ?? + val encryptedBallots = consumer.iterateEncryptedBallots(device) { true } val bauxMap = mutableMapOf() bauxMap[H0.contentHashCode()] = start encryptedBallots.map { eballot -> @@ -205,7 +202,7 @@ data class EncryptedBallotChain( constructor(ballot: EncryptedBallot) : this(ballot.ballotId, ballot.confirmationCode, ballot.codeBaux) override fun toString(): String { - return "$ballotId" + return ballotId } } } diff --git a/src/main/kotlin/org/cryptobiotic/eg/encrypt/Encryptor.kt b/src/main/kotlin/org/cryptobiotic/eg/encrypt/Encryptor.kt index 1de5bca..db42a74 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/encrypt/Encryptor.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/encrypt/Encryptor.kt @@ -39,8 +39,12 @@ class Encryptor( val encryptedContests = mutableListOf() val manifestContests = manifest.contestsForBallotStyle(this.ballotStyle) - if (manifestContests == null || manifestContests.isEmpty()) { - errs.add("Manifest does not have ballotStyle ${this.ballotStyle} or it has no contests for that ballotStyle") + if (manifestContests == null) { + errs.add("Manifest does not have ballotStyle ${this.ballotStyle}") + return null + } + if (manifestContests.isEmpty()) { + errs.add("Manifest has no contests for ballotStyle ${this.ballotStyle}") return null } for (mcontest in manifestContests) { @@ -58,7 +62,6 @@ class Encryptor( // H(B) = H(HE ; 0x24, χ1 , χ2 , . . . , χmB , Baux ) ; spec 2.0.0 eq 58 val contestHashes = sortedContests.map { it.contestHash } val confirmationCode = hashFunction(extendedBaseHash.bytes, 0x24.toByte(), contestHashes, codeBaux) - val timestamp = timestampOverride ?: (System.currentTimeMillis() / 1000) // secs since epoch val encryptedSn: ElGamalCiphertext? = if (this.sn != null) { val snNonce = hashFunction(extendedBaseHashB, 0x110.toByte(), ballotNonce).toElementModQ(group) @@ -69,7 +72,7 @@ class Encryptor( ballotId, ballotStyle, encryptingDevice, - timestamp, + timestampOverride ?: (System.currentTimeMillis() / 1000), // secs since epoch, codeBaux, confirmationCode, extendedBaseHash, @@ -93,11 +96,11 @@ class Encryptor( ): PendingEncryptedBallot.Contest { val ballotSelections = this.selections.associateBy { it.selectionId } + // count the number of votes val votedFor = mutableListOf() var selectionOvervote = false for (mselection: ManifestIF.Selection in mcontest.selections) { - // Find the ballot selection matching the contest description. - val plaintextSelection = ballotSelections[mselection.selectionId] + val plaintextSelection = ballotSelections[mselection.selectionId] // Find the plaintext selection matching the manifest selectionId. if (plaintextSelection != null && plaintextSelection.vote > 0) { votedFor.add(plaintextSelection.sequenceOrder) if (plaintextSelection.vote > optionLimit) { @@ -106,8 +109,7 @@ class Encryptor( } } - // TODO writeIns is adding an extra selection?? Messes with decryption. ability to turn feature off ?? - // when theres a writein and a vote, the contest is overvoted and all votes are set to 0. + // Compute the contest status val totalVotedFor = votedFor.size + this.writeIns.size val status = if (totalVotedFor == 0) ContestDataStatus.null_vote else if (selectionOvervote || totalVotedFor > contestLimit) ContestDataStatus.over_vote @@ -118,7 +120,7 @@ class Encryptor( for (mselection: ManifestIF.Selection in mcontest.selections) { var plaintextSelection = ballotSelections[mselection.selectionId] - // Set vote to zero if not in manifest or this contest is overvoted + // Set vote to zero if not in manifest or this contest is overvoted. See 3.3.3 "Overvotes". if (plaintextSelection == null || (status == ContestDataStatus.over_vote)) { plaintextSelection = makeZeroSelection(mselection.selectionId, mselection.sequenceOrder) } diff --git a/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt b/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt index ee6a92b..35a0af2 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/input/RandomBallotProvider.kt @@ -51,7 +51,7 @@ class RandomBallotProvider(val manifest: Manifest, val nballots: Int = 11) { for (contestp in manifest.contestsForBallotStyle(useStyle)!!) { contests.add(makeContestFrom(contestp as Manifest.ContestDescription)) } - val sn = Random.nextInt(1000) + val sn = Random.nextInt(1,1000) return PlaintextBallot(ballotId, useStyle, contests, sn.toLong()) } diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecord.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecord.kt index 08423a6..0075152 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecord.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecord.kt @@ -12,6 +12,7 @@ interface ElectionRecord { val group : GroupContext fun stage() : Stage + fun consumer() : Consumer fun topdir() : String fun isJson(): Boolean diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecordFactory.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecordFactory.kt index 8279522..96d1796 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecordFactory.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/ElectionRecordFactory.kt @@ -104,6 +104,10 @@ private class ElectionRecordImpl(val consumer: Consumer, return stage } + override fun consumer(): Consumer { + return consumer + } + override fun topdir(): String { return consumer.topdir() } diff --git a/src/main/kotlin/org/cryptobiotic/eg/verifier/Verifier.kt b/src/main/kotlin/org/cryptobiotic/eg/verifier/Verifier.kt index 897e494..2988eed 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/verifier/Verifier.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/verifier/Verifier.kt @@ -5,7 +5,6 @@ import com.github.michaelbull.result.* import org.cryptobiotic.eg.core.* import org.cryptobiotic.eg.core.intgroup.IntGroupConstants import org.cryptobiotic.eg.core.intgroup.Primes4096 -import org.cryptobiotic.eg.core.intgroup.Primes4096.nbytes import org.cryptobiotic.eg.core.intgroup.ProductionGroupContext import org.cryptobiotic.eg.election.* import org.cryptobiotic.eg.publish.ElectionRecord @@ -72,8 +71,7 @@ class Verifier(val record: ElectionRecord, val nthreads: Int = 11) { val chainOk = if (!config.chainConfirmationCodes) true else { val chainErrs = ErrorMessages("") - val ok = encryptionVerifier.verifyConfirmationChain(record, chainErrs) - // encryptionVerifier.verifyConfirmationChain2(record, chainErrs) + val ok = encryptionVerifier.verifyConfirmationChain(record.consumer(), chainErrs) println(" 7. verifyConfirmationChain $ok") if (!ok) { println(chainErrs) diff --git a/src/main/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallots.kt b/src/main/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallots.kt index cd5d031..7ae2e0d 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallots.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallots.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.yield import org.cryptobiotic.eg.encrypt.EncryptedBallotChain import org.cryptobiotic.eg.encrypt.EncryptedBallotChain.Companion.assembleChain import org.cryptobiotic.eg.publish.Consumer -import org.cryptobiotic.eg.publish.ElectionRecord import org.cryptobiotic.util.Stopwatch private const val debugBallots = false @@ -172,7 +171,7 @@ class VerifyEncryptedBallots( ////////////////////////////////////////////////////////////////////////////// // ballot chaining, section 7 - fun verifyConfirmationChain(consumer: ElectionRecord, errs: ErrorMessages): Boolean { + fun verifyConfirmationChain(consumer: Consumer, errs: ErrorMessages): Boolean { var deviceCount = 0 consumer.encryptingDevices().forEach { device -> // println("verifyConfirmationChain device=$device") @@ -180,9 +179,6 @@ class VerifyEncryptedBallots( if (ballotChainResult is Err) { errs.add(ballotChainResult.toString()) } else { - val ballotChain: EncryptedBallotChain = ballotChainResult.unwrap() - val ballots = consumer.encryptedBallots(device) { true } - // If chainCodes is true, and configBaux0 is empty, then the device name UTF-8 bytes will be used when creating the // confirmation codes during encryption. This allows the configuration file to be used across multiple devices, // and still have the device name as part of the ballot chaining as required in the spec. @@ -196,16 +192,22 @@ class VerifyEncryptedBallots( // Baux,j = H(Bj−1) ∥ Baux,0 . var prevCC = H0 var ballotCount = 0 - ballots.forEach { ballot -> - val expectedBaux = prevCC + baux0 // eq 7.D and 7.E - println("expectedBaux ${expectedBaux.contentToString()}") - - if (!expectedBaux.contentEquals(ballot.codeBaux)) { - errs.add(" 7.E. additional input byte array Baux != H(Bj−1 ) ∥ Baux,0 for ballot=${ballot.ballotId}") + val ballotChain: EncryptedBallotChain = ballotChainResult.unwrap() + ballotChain.ballotIds.forEach { ballotId -> + val eballotResult = consumer.readEncryptedBallot(device, ballotId) + if (eballotResult is Err) { + errs.add("Cant find $ballotId") + } else { + val eballot = eballotResult.unwrap() + val expectedBaux = prevCC + baux0 // eq 7.D and 7.E + if (!expectedBaux.contentEquals(eballot.codeBaux)) { + errs.add(" 7.E. additional input byte array Baux != H(Bj−1 ) ∥ Baux,0 for ballot=${eballot.ballotId}") + } + prevCC = eballot.confirmationCode.bytes } - prevCC = ballot.confirmationCode.bytes ballotCount++ } + // 7.F The final additional input byte array is equal to Baux = H(Bℓ ) ∥ Baux,0 ∥ b(“CLOSE”, 5) and // H(Bℓ ) is the final confirmation code on this device. val bauxFinal = prevCC + baux0 + "CLOSE".encodeToByteArray() @@ -242,7 +244,7 @@ class VerifyEncryptedBallots( fun assembleAndVerifyOneChain(consumer: Consumer, device: String, errs: ErrorMessages) { val ballotChain: EncryptedBallotChain? = - assembleChain(consumer, device, null, config.configBaux0, extendedBaseHash, errs) + assembleChain(consumer, device, config.configBaux0, extendedBaseHash, errs) if (ballotChain == null) { errs.add("Cant assembleChain") return @@ -254,7 +256,7 @@ class VerifyEncryptedBallots( val baux0 = if (config.configBaux0.isEmpty()) device.encodeToByteArray() else config.configBaux0 // 7.D The initial hash code H0 satisfies H0 = H(HE ; 0x24, Baux,0 ) - // "and Baux,0 contains the unique voting device information". TODO ambiguous, change spec wording + // "and Baux,0 contains the unique voting device information". val H0 = hashFunction(extendedBaseHash.bytes, 0x24.toByte(), baux0).bytes // (7.E) For all 1 ≤ j ≤ ℓ, the additional input byte array used to compute Hj = H(Bj) is equal to diff --git a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt index b63ed2f..4f677b9 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/cli/RunEncryptBallotTest.kt @@ -2,6 +2,7 @@ package org.cryptobiotic.eg.cli import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok +import org.cryptobiotic.eg.core.createDirectories import org.cryptobiotic.eg.core.removeAllFiles import org.cryptobiotic.eg.input.RandomBallotProvider import org.cryptobiotic.eg.publish.makeConsumer @@ -17,6 +18,47 @@ import kotlin.test.* class RunEncryptBallotTest { + @Test + fun testRunEncryptBadPlaintextBallot() { + val inputDir = "src/test/data/encrypt/testBallotNoChain" + val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBadPlaintextBallot" + val consumerIn = makeConsumer(inputDir) + + val retval = RunEncryptBallot.encryptBallot( + consumerIn, + "$outputDir/pballot-bad.json", + "$outputDir/encrypted_ballots", + "device42", + ) + assertEquals(3, retval) + } + + @Test + fun testRunEncryptBadOutputDir() { + val inputDir = "src/test/data/encrypt/testBallotChain" + val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBadOutputDir" + val ballotId = "3842034" + + val consumerIn = makeConsumer(inputDir) + val record = readElectionRecord(consumerIn) + val manifest = record.manifest() + + val ballotProvider = RandomBallotProvider(manifest) + val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) + + removeAllFiles(Path.of(outputDir)) + val publisher = makePublisher(outputDir, true) + publisher.writePlaintextBallot(outputDir, listOf(ballot)) + + val retval = RunEncryptBallot.encryptBallot( + consumerIn, + "$outputDir/pballot-$ballotId.json", + "nosuchdir", + "device42", + ) + assertEquals(4, retval) + } + @Test fun testRunEncryptOneBallotNoChaining() { val inputDir = "src/test/data/encrypt/testBallotNoChain" @@ -33,6 +75,7 @@ class RunEncryptBallotTest { removeAllFiles(Path.of(outputDir)) val publisher = makePublisher(outputDir, true) publisher.writePlaintextBallot(outputDir, listOf(ballot)) + createDirectories("$outputDir/encrypted_ballots") RunEncryptBallot.main( arrayOf( @@ -63,6 +106,7 @@ class RunEncryptBallotTest { removeAllFiles(Path.of(outputDir)) val publisher = makePublisher(outputDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) + createDirectories("$outputDir/encrypted_ballots") val ballotProvider = RandomBallotProvider(manifest) repeat(nballots) { @@ -93,7 +137,6 @@ class RunEncryptBallotTest { assertEquals(nballots, count) } - @Test fun testRunEncryptBallotsChaining() { val inputDir = "src/test/data/encrypt/testBallotChain" @@ -107,6 +150,7 @@ class RunEncryptBallotTest { val manifest = record.manifest() val publisher = makePublisher(outputDeviceDir, true) val consumerOut = makeConsumer(outputDir, consumerIn.group) + createDirectories("$outputDeviceDir") val ballotProvider = RandomBallotProvider(manifest) repeat(nballots) { @@ -132,9 +176,74 @@ class RunEncryptBallotTest { assertTrue( result is Ok) } + RunEncryptBallot.main( + arrayOf( + "--inputDir", inputDir, + "--ballotFilepath", "CLOSE", + "--encryptBallotDir", outputDeviceDir, + "-device", device, + ) + ) + val count = verifyOutput(inputDir, outputDir, true) assertEquals(nballots, count) } + + @Test + fun testRunEncryptBallotsChainingNotClosed() { + val inputDir = "src/test/data/encrypt/testBallotChain" + val device = "device42" + val outputDir = "${Testing.testOut}/encrypt/testRunEncryptBallotsChainingNotClosed" + val outputDeviceDir = "$outputDir/encrypted_ballots/$device" + val nballots = 10 + + val consumerIn = makeConsumer(inputDir) + val record = readElectionRecord(consumerIn) + val manifest = record.manifest() + val publisher = makePublisher(outputDeviceDir, true) + val consumerOut = makeConsumer(outputDir, consumerIn.group) + + val ballotProvider = RandomBallotProvider(manifest) + repeat(nballots) { + val ballotId = Random.nextInt().toString() + val ballot = ballotProvider.getFakeBallot(manifest, null, ballotId) + + publisher.writePlaintextBallot(outputDeviceDir, listOf(ballot)) + + val ballotFilename = "$outputDeviceDir/pballot-$ballotId.json" + RunEncryptBallot.main( + arrayOf( + "--inputDir", inputDir, + "--ballotFilepath", ballotFilename, + "--encryptBallotDir", outputDeviceDir, + "-device", device, + ) + ) + + val result = consumerOut.readEncryptedBallot(device, ballotId) + if (result is Err) { + println("Error = $result") + } + assertTrue( result is Ok) + } + + val verifier = VerifyEncryptedBallots( + consumerIn.group, + record.manifest(), + record.jointPublicKey()!!, + record.extendedBaseHash()!!, + record.config(), 1 + ) + + val consumerBallots = makeConsumer(outputDir, consumerIn.group) + val chainErrs = ErrorMessages("verifyConfirmationChain") + verifier.verifyConfirmationChain(consumerBallots, chainErrs) + if (chainErrs.hasErrors()) { + println(chainErrs) + } + assertTrue(chainErrs.hasErrors()) + assertTrue(chainErrs.toString().contains("7.G. The closing hash is not equal to H")) + } } fun verifyOutput(inputDir: String, outputDir: String, chained: Boolean = false): Int { @@ -156,6 +265,15 @@ fun verifyOutput(inputDir: String, outputDir: String, chained: Boolean = false): println(" verifyEncryptedBallots $outputDir: ok= $ok result= $errs") assertFalse(errs.hasErrors()) + if (chained) { + val chainErrs = ErrorMessages("verifyConfirmationChain") + verifier.verifyConfirmationChain(consumerBallots, chainErrs) + if (chainErrs.hasErrors()) { + println(chainErrs) + } + assertFalse(chainErrs.hasErrors()) + } + if (chained) { val chain2Errs = ErrorMessages("assembleAndVerifyChains") verifier.assembleAndVerifyChains(consumerBallots, chain2Errs) diff --git a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt index 7d9f105..d5d6184 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/encrypt/AddEncryptedBallotTest.kt @@ -351,7 +351,7 @@ fun checkOutput(outputDir: String, expectedCount: Int, chained: Boolean) { if (chained) { val chainErrs = ErrorMessages("verifyConfirmationChain") - verifyEncryptions.verifyConfirmationChain(record, chainErrs) + verifyEncryptions.verifyConfirmationChain(consumer, chainErrs) println(chainErrs) assertFalse(chainErrs.hasErrors()) } diff --git a/src/test/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallotsTest.kt b/src/test/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallotsTest.kt new file mode 100644 index 0000000..4051465 --- /dev/null +++ b/src/test/kotlin/org/cryptobiotic/eg/verifier/VerifyEncryptedBallotsTest.kt @@ -0,0 +1,85 @@ +package org.cryptobiotic.eg.verifier + +import org.cryptobiotic.eg.core.UInt256 +import org.cryptobiotic.eg.publish.makeConsumer +import org.cryptobiotic.eg.publish.readElectionRecord +import org.cryptobiotic.util.ErrorMessages +import org.cryptobiotic.util.Stats +import kotlin.test.Test +import kotlin.test.assertTrue + +class VerifyEncryptedBallotsTest { + + @Test + fun testVerifyEncryptedBallotBadElectionId() { + val inputDir = "src/test/data/encrypt/testBallotChain" + val wrongDir = "src/test/data/workflow/allAvailableEc" + + val consumerIn = makeConsumer(inputDir) + val record = readElectionRecord(consumerIn) + + val verifier = VerifyEncryptedBallots( + consumerIn.group, + record.manifest(), + record.jointPublicKey()!!, + record.extendedBaseHash()!!, + record.config(), 1 + ) + + val wrongConsumer = makeConsumer(wrongDir, consumerIn.group) + val errs = ErrorMessages("wrongBallot") + val wrongBallot = wrongConsumer.iterateAllEncryptedBallots { true }.iterator().next() + + verifier.verifyEncryptedBallot(wrongBallot, errs, Stats()) + println(errs) + assertTrue(errs.hasErrors()) + assertTrue(errs.toString().contains("has wrong electionId")) + } + + @Test + fun testVerifyEncryptedBallotBadConfirmationCode() { + val inputDir = "src/test/data/encrypt/testBallotChain" + + val consumerIn = makeConsumer(inputDir) + val record = readElectionRecord(consumerIn) + + val verifier = VerifyEncryptedBallots( + consumerIn.group, + record.manifest(), + record.jointPublicKey()!!, + record.extendedBaseHash()!!, + record.config(), 1 + ) + val eballot = consumerIn.iterateAllEncryptedBallots { true }.iterator().next() + val badBallot = eballot.copy(confirmationCode = UInt256.random()) + val errs = ErrorMessages("wrongCC") + verifier.verifyEncryptedBallot(badBallot, errs, Stats()) + println(errs) + assertTrue(errs.hasErrors()) + assertTrue(errs.toString().contains("7.B. Incorrect ballot confirmation code")) + } + + @Test + fun testBadConfirmationChain() { + val inputDir = "src/test/data/encrypt/testBallotChain" + val wrongDir = "src/test/data/encrypt/testBallotNoChain" + + val consumerIn = makeConsumer(inputDir) + val record = readElectionRecord(consumerIn) + + val verifier = VerifyEncryptedBallots( + consumerIn.group, + record.manifest(), + record.jointPublicKey()!!, + record.extendedBaseHash()!!, + record.config(), 1 + ) + + val wrongConsumer = makeConsumer(wrongDir, consumerIn.group) + val errs = ErrorMessages("testBadConfirmationChain") + verifier.verifyConfirmationChain(wrongConsumer, errs) + println(errs) + assertTrue(errs.hasErrors()) + assertTrue(errs.toString().contains("file does not exist")) + } +} \ No newline at end of file