From 62d1d030d3e26d0734031c63ab45eb09f46e9a47 Mon Sep 17 00:00:00 2001 From: Steve Maier <82616727+SteveMaier-IRT@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:11:47 -0400 Subject: [PATCH] - Add checks for the context and manifest matching manifesthash values (#306) - Add C++ and C# unit tests --- .../TestEncrypt.cs | 43 +++++++++++ .../ElectionGuard.Encryption/Encrypt.cs | 5 ++ src/electionguard/encrypt.cpp | 75 ++++++++---------- test/electionguard/test_encrypt.cpp | 77 ++++++++++++++----- 4 files changed, 138 insertions(+), 62 deletions(-) diff --git a/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption.Tests/TestEncrypt.cs b/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption.Tests/TestEncrypt.cs index 0439860f..141e9228 100644 --- a/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption.Tests/TestEncrypt.cs +++ b/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption.Tests/TestEncrypt.cs @@ -154,5 +154,48 @@ public void Test_Constant_Serialization_Succeeds() Assert.That(constants.Contains(Constants.G.ToHex())); } + [Test] + public void Test_EncryptMediator_Hashes_Match() + { + var keypair = ElGamalKeyPair.FromSecret(Constants.TWO_MOD_Q); + var manifest = ManifestGenerator.GetManifestFromFile(); + var internalManifest = new InternalManifest(manifest); + var context = new CiphertextElectionContext( + 1UL, 1UL, keypair.PublicKey, Constants.TWO_MOD_Q, internalManifest.ManifestHash); // make a context with the correct manifesthash + var device = new EncryptionDevice(12345UL, 23456UL, 34567UL, "Location"); + try + { + var mediator = new EncryptionMediator(internalManifest, context, device); + Assert.IsNotNull(mediator); // should not be null if it gets created + } + catch (Exception ex) + { + // if there is an exception then the manifest hash would not be equal + Assert.AreNotEqual(context.ManifestHash.ToHex(), internalManifest.ManifestHash.ToHex()); + } + } + + [Test] + public void Test_EncryptMediator_Hashes_Dont_Match() + { + var keypair = ElGamalKeyPair.FromSecret(Constants.TWO_MOD_Q); + var manifest = ManifestGenerator.GetManifestFromFile(); + var internalManifest = new InternalManifest(manifest); + var context = new CiphertextElectionContext( + 1UL, 1UL, keypair.PublicKey, Constants.TWO_MOD_Q, Constants.ONE_MOD_Q); // make a context with a different manifesthash + var device = new EncryptionDevice(12345UL, 23456UL, 34567UL, "Location"); + try + { + var mediator = new EncryptionMediator(internalManifest, context, device); + Assert.IsNull(mediator); // should not be created, so null at best + } + catch (Exception ex) + { + // if there is an exception then the manifest hash would not be equal + Assert.AreNotEqual(context.ManifestHash.ToHex(), internalManifest.ManifestHash.ToHex()); + } + } + + } } diff --git a/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption/Encrypt.cs b/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption/Encrypt.cs index b1a993ec..a07cd08a 100644 --- a/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption/Encrypt.cs +++ b/bindings/netstandard/ElectionGuard/ElectionGuard.Encryption/Encrypt.cs @@ -110,6 +110,11 @@ public unsafe EncryptionMediator( CiphertextElectionContext context, EncryptionDevice device) { + if (manifest.ManifestHash.ToHex() != context.ManifestHash.ToHex()) + { + Status.ELECTIONGUARD_STATUS_ERROR_INVALID_ARGUMENT.ThrowIfError(); + } + var status = NativeInterface.EncryptionMediator.New( manifest.Handle, context.Handle, device.Handle, out Handle); status.ThrowIfError(); diff --git a/src/electionguard/encrypt.cpp b/src/electionguard/encrypt.cpp index 7fa26831..c156c85b 100644 --- a/src/electionguard/encrypt.cpp +++ b/src/electionguard/encrypt.cpp @@ -5,16 +5,16 @@ #include "electionguard/ballot_code.hpp" #include "electionguard/elgamal.hpp" #include "electionguard/hash.hpp" +#include "electionguard/precompute_buffers.hpp" #include "log.hpp" #include "nonces.hpp" #include "serialize.hpp" #include "utils.hpp" -#include "electionguard/precompute_buffers.hpp" -#include #include #include #include +#include extern "C" { #include "../karamel/Hacl_Bignum4096.h" @@ -90,18 +90,10 @@ namespace electionguard return DeviceSerializer::fromBson(move(data)); } - uint64_t EncryptionDevice::getDeviceUuid() const { - return pimpl->deviceUuid; - } - uint64_t EncryptionDevice::getSessionUuid() const { - return pimpl->sessionUuid; - } - uint64_t EncryptionDevice::getLaunchCode() const { - return pimpl->launchCode; - } - std::string EncryptionDevice::getLocation() const { - return pimpl->location; - } + uint64_t EncryptionDevice::getDeviceUuid() const { return pimpl->deviceUuid; } + uint64_t EncryptionDevice::getSessionUuid() const { return pimpl->sessionUuid; } + uint64_t EncryptionDevice::getLaunchCode() const { return pimpl->launchCode; } + std::string EncryptionDevice::getLocation() const { return pimpl->location; } #pragma endregion @@ -127,6 +119,11 @@ namespace electionguard const EncryptionDevice &encryptionDevice) : pimpl(new Impl(internalManifest, context, encryptionDevice)) { + if (internalManifest.getManifestHash()->toHex() != context.getManifestHash()->toHex()) { + throw invalid_argument("manifest and context do not match hashes manifest:" + + internalManifest.getManifestHash()->toHex() + + " context:" + context.getManifestHash()->toHex()); + } } EncryptionMediator::~EncryptionMediator() = default; @@ -296,8 +293,8 @@ namespace electionguard auto pubkey_to_exp = triple1->get_pubkey_to_exp(); // Generate the encryption using precomputed values - ciphertext = elgamalEncrypt_with_precomputed(selection.getVote(), - *g_to_exp, *pubkey_to_exp); + ciphertext = + elgamalEncrypt_with_precomputed(selection.getVote(), *g_to_exp, *pubkey_to_exp); if (ciphertext == nullptr) { throw runtime_error("encryptSelection:: Error generating ciphertext"); } @@ -309,12 +306,10 @@ namespace electionguard encrypted = CiphertextBallotSelection::make_with_precomputed( selection.getObjectId(), description.getSequenceOrder(), *descriptionHash, move(ciphertext), cryptoExtendedBaseHash, selection.getVote(), - move(precomputedTwoTriplesAndAQuad), - isPlaceholder, true); + move(precomputedTwoTriplesAndAQuad), isPlaceholder, true); } else { // Generate the encryption - ciphertext = - elgamalEncrypt(selection.getVote(), *selectionNonce, elgamalPublicKey); + ciphertext = elgamalEncrypt(selection.getVote(), *selectionNonce, elgamalPublicKey); if (ciphertext == nullptr) { throw runtime_error("encryptSelection:: Error generating ciphertext"); } @@ -336,17 +331,17 @@ namespace electionguard // verify the selection. if (encrypted->isValidEncryption(*descriptionHash, elgamalPublicKey, - cryptoExtendedBaseHash)) { + cryptoExtendedBaseHash)) { return encrypted; } - throw runtime_error("encryptSelection failed validity check"); + throw runtime_error("encryptSelection failed validity check"); } string getOvervoteAndWriteIns(const PlaintextBallotContest &contest, const InternalManifest &internalManifest, eg_valid_contest_return_type_t is_overvote) { - json overvoteAndWriteIns; + json overvoteAndWriteIns; auto selections = contest.getSelections(); // if an overvote is detected then put the selections into json @@ -403,15 +398,14 @@ namespace electionguard string overvoteAndWriteIns_string(""); if (overvoteAndWriteIns.dump() != string("null")) { - overvoteAndWriteIns_string = overvoteAndWriteIns.dump(); + overvoteAndWriteIns_string = overvoteAndWriteIns.dump(); } return overvoteAndWriteIns_string; } unique_ptr - encryptContest(const PlaintextBallotContest &contest, - const InternalManifest &internalManifest, + encryptContest(const PlaintextBallotContest &contest, const InternalManifest &internalManifest, const ContestDescriptionWithPlaceholders &description, const ElementModP &elgamalPublicKey, const ElementModQ &cryptoExtendedBaseHash, const ElementModQ &nonceSeed, bool shouldVerifyProofs /* = true */) @@ -419,8 +413,8 @@ namespace electionguard { // Validate Input bool supportOvervotes = true; - eg_valid_contest_return_type_t is_valid_contest = - contest.isValid(description.getObjectId(), description.getSelections().size(), + eg_valid_contest_return_type_t is_valid_contest = contest.isValid( + description.getObjectId(), description.getSelections().size(), description.getNumberElected(), description.getVotesAllowed(), supportOvervotes); if ((is_valid_contest != SUCCESS) && (is_valid_contest != OVERVOTE)) { throw invalid_argument("the plaintext contest was invalid"); @@ -471,8 +465,8 @@ namespace electionguard // if the is an overvote then we need to make all the selection votes 0 if (is_valid_contest == OVERVOTE) { - duplicate_selection = make_unique( - selection_ptr->getObjectId(), 0, false); + duplicate_selection = + make_unique(selection_ptr->getObjectId(), 0, false); selection_ptr = duplicate_selection.get(); } @@ -480,7 +474,7 @@ namespace electionguard encryptedSelections.push_back(encryptSelection( *selection_ptr, selectionDescription.get(), *elgamalPublicKey_ptr, - *cryptoExtendedBaseHash_ptr, *sharedNonce.get(), false, shouldVerifyProofs)); + *cryptoExtendedBaseHash_ptr, *sharedNonce.get(), false, shouldVerifyProofs)); } else { // Should never happen since the contest is normalized by emplaceMissingValues throw runtime_error("encryptedContest:: Error constructing encrypted selection"); @@ -507,18 +501,17 @@ namespace electionguard auto placeholderSelection = selectionFrom(placeholder, true, selectPlaceholder); encryptedSelections.push_back(encryptSelection( - *placeholderSelection, placeholder, *elgamalPublicKey_ptr, - *cryptoExtendedBaseHash_ptr, *sharedNonce.get(), false, shouldVerifyProofs)); + *placeholderSelection, placeholder, *elgamalPublicKey_ptr, + *cryptoExtendedBaseHash_ptr, *sharedNonce.get(), false, shouldVerifyProofs)); } // Derive the extendedDataNonce from the selection nonce and a constant auto noncesForExtendedData = - make_unique(*sharedNonce->clone(), "constant-extended-data"); + make_unique(*sharedNonce->clone(), "constant-extended-data"); auto extendedDataNonce = noncesForExtendedData->get(0); - vector extendedData_plaintext((uint8_t *)&extendedData.front(), - (uint8_t *)&extendedData.front() + - extendedData.size()); + vector extendedData_plaintext( + (uint8_t *)&extendedData.front(), (uint8_t *)&extendedData.front() + extendedData.size()); // Perform HashedElGamalCiphertext calculation unique_ptr hashedElGamal = @@ -536,8 +529,8 @@ namespace electionguard auto encryptedContest = CiphertextBallotContest::make( contest.getObjectId(), description.getSequenceOrder(), *descriptionHash, move(encryptedSelections), elgamalPublicKey, cryptoExtendedBaseHash, *chaumPedersenNonce, - description.getNumberElected(), sharedNonce->clone(), nullptr, - nullptr, move(hashedElGamal)); + description.getNumberElected(), sharedNonce->clone(), nullptr, nullptr, + move(hashedElGamal)); if (encryptedContest == nullptr || encryptedContest->getProof() == nullptr) { throw runtime_error("encryptedContest:: Error constructing encrypted constest"); @@ -574,8 +567,8 @@ namespace electionguard hasContest = true; auto encrypted = encryptContest( contest.get(), internalManifest, description.get(), - *context.getElGamalPublicKey(), - *context.getCryptoExtendedBaseHash(), nonceSeed, shouldVerifyProofs); + *context.getElGamalPublicKey(), *context.getCryptoExtendedBaseHash(), + nonceSeed, shouldVerifyProofs); encryptedContests.push_back(move(encrypted)); break; diff --git a/test/electionguard/test_encrypt.cpp b/test/electionguard/test_encrypt.cpp index b0052097..9984b027 100644 --- a/test/electionguard/test_encrypt.cpp +++ b/test/electionguard/test_encrypt.cpp @@ -60,7 +60,7 @@ TEST_CASE("Encrypt simple selection using precomputed values succeeds") auto hashContext = metadata->crypto_hash(); auto plaintext = BallotGenerator::selectionFrom(*metadata); - // cause a two triples and a quad to be populated + // cause a two triples and a quad to be populated PrecomputeBufferContext::init(1); PrecomputeBufferContext::populate(*keypair->getPublicKey()); PrecomputeBufferContext::stop_populate(); @@ -132,7 +132,7 @@ TEST_CASE("Encrypt PlaintextBallot with EncryptionMediator against constructed " auto device = make_unique(12345UL, 23456UL, 34567UL, "Location"); auto mediator = make_unique(*internal, *context, *device); - + // Act auto plaintext = BallotGenerator::getFakeBallot(*internal); // Log::debug(plaintext->toJson()); @@ -201,7 +201,7 @@ TEST_CASE("Encrypt PlaintextBallot overvote") CHECK(heg != nullptr); CHECK(heg->getData().size() == (size_t)(BYTES_512 + sizeof(uint16_t))); - unique_ptr new_pad = make_unique(*heg->getPad()); + unique_ptr new_pad = make_unique(*heg->getPad()); unique_ptr newHEG = make_unique(move(new_pad), heg->getData(), heg->getMac()); @@ -210,8 +210,8 @@ TEST_CASE("Encrypt PlaintextBallot overvote") string new_plaintext_string((char *)&new_plaintext.front(), new_plaintext.size()); CHECK(new_plaintext_string == - string("{\"error\":\"overvote\",\"error_data\":[\"benjamin-franklin-selection\"" - ",\"john-adams-selection\"]}")); + string("{\"error\":\"overvote\",\"error_data\":[\"benjamin-franklin-selection\"" + ",\"john-adams-selection\"]}")); } TEST_CASE("Encrypt simple PlaintextBallot with EncryptionMediator succeeds") @@ -260,28 +260,29 @@ TEST_CASE("Encrypt full PlaintextBallot with WriteIn and Overvote with Encryptio auto device = make_unique(12345UL, 23456UL, 34567UL, "Location"); auto mediator = make_unique(*internal, *context, *device); - + // Act - string plaintextBallot_json = string("{\"object_id\": \"03a29d15-667c-4ac8-afd7-549f19b8e4eb\"," - "\"style_id\": \"jefferson-county-ballot-style\", \"contests\": [ {\"object_id\":" - "\"justice-supreme-court\", \"sequence_order\": 0, \"ballot_selections\": [{" - "\"object_id\": \"john-adams-selection\", \"sequence_order\": 0, \"vote\": 1," - "\"is_placeholder_selection\": false, \"extended_data\": null}, {\"object_id\"" - ": \"benjamin-franklin-selection\", \"sequence_order\": 1, \"vote\": 1," - "\"is_placeholder_selection\": false, \"extended_data\": null}, {\"object_id\":" - " \"write-in-selection\", \"sequence_order\": 3, \"vote\": 1, \"is_placeholder_selection\"" - ": false, \"write_in\": \"Susan B. Anthony\"}], \"extended_data\": null}]}"); - // TODO - Once the relevant plaintext ballot file is in the environment then + string plaintextBallot_json = string( + "{\"object_id\": \"03a29d15-667c-4ac8-afd7-549f19b8e4eb\"," + "\"style_id\": \"jefferson-county-ballot-style\", \"contests\": [ {\"object_id\":" + "\"justice-supreme-court\", \"sequence_order\": 0, \"ballot_selections\": [{" + "\"object_id\": \"john-adams-selection\", \"sequence_order\": 0, \"vote\": 1," + "\"is_placeholder_selection\": false, \"extended_data\": null}, {\"object_id\"" + ": \"benjamin-franklin-selection\", \"sequence_order\": 1, \"vote\": 1," + "\"is_placeholder_selection\": false, \"extended_data\": null}, {\"object_id\":" + " \"write-in-selection\", \"sequence_order\": 3, \"vote\": 1, \"is_placeholder_selection\"" + ": false, \"write_in\": \"Susan B. Anthony\"}], \"extended_data\": null}]}"); + // TODO - Once the relevant plaintext ballot file is in the environment then // uncomment the below and stop using the hard coded string. //auto plaintextBallot = // BallotGenerator::getSimpleBallotFromFile(TEST_BALLOT_WITH_WRITEIN_SELECTED); auto plaintextBallot = PlaintextBallot::fromJson(plaintextBallot_json); auto ciphertext = mediator->encrypt(*plaintextBallot); - + // Assert CHECK(ciphertext->isValidEncryption(*context->getManifestHash(), *keypair->getPublicKey(), *context->getCryptoExtendedBaseHash()) == true); - + // check to make sure we have a hashed elgamal ciphertext unique_ptr heg = nullptr; auto contests = ciphertext->getContests(); @@ -307,9 +308,9 @@ TEST_CASE("Encrypt full PlaintextBallot with WriteIn and Overvote with Encryptio Log::debug(new_plaintext_string); CHECK(new_plaintext_string == - string("{\"error\":\"overvote\",\"error_data\":[\"john-adams-selection\"," - "\"benjamin-franklin-selection\",\"write-in-selection\"],\"write_ins\"" - ":{\"write-in-selection\":\"Susan B. Anthony\"}}")); + string("{\"error\":\"overvote\",\"error_data\":[\"john-adams-selection\"," + "\"benjamin-franklin-selection\",\"write-in-selection\"],\"write_ins\"" + ":{\"write-in-selection\":\"Susan B. Anthony\"}}")); } TEST_CASE("Encrypt simple CompactPlaintextBallot with EncryptionMediator succeeds") @@ -438,3 +439,37 @@ TEST_CASE("Encrypt simple ballot from file succeeds with precomputed values") *context->getCryptoExtendedBaseHash()) == true); PrecomputeBufferContext::empty_queues(); } + +TEST_CASE("Create EncryptionMediator with same manifest hash") +{ + auto secret = ElementModQ::fromHex(a_fixed_secret); + auto keypair = ElGamalKeyPair::fromSecret(*secret); + auto manifest = ManifestGenerator::getJeffersonCountyManifest_Minimal(); + auto internal = make_unique(*manifest); + auto context = make_unique( + 1UL, 1UL, keypair->getPublicKey()->clone(), Q().clone(), + internal.get()->getManifestHash()->clone(), Q().clone(), Q().clone()); + auto device = make_unique(12345UL, 23456UL, 34567UL, "Location"); + + auto mediator = make_unique(*internal, *context, *device); + CHECK(mediator != nullptr); +} + +TEST_CASE("Create EncryptionMediator with different manifest hash") +{ + auto secret = ElementModQ::fromHex(a_fixed_secret); + auto keypair = ElGamalKeyPair::fromSecret(*secret); + auto manifest = ManifestGenerator::getJeffersonCountyManifest_Minimal(); + auto internal = make_unique(*manifest); + auto context = + make_unique(1UL, 1UL, keypair->getPublicKey()->clone(), + Q().clone(), Q().clone(), Q().clone(), Q().clone()); + auto device = make_unique(12345UL, 23456UL, 34567UL, "Location"); + + try { + auto mediator = make_unique(*internal, *context, *device); + CHECK(mediator == nullptr); + } catch (const std::exception &e) { + CHECK(internal->getManifestHash()->toHex() != context->getManifestHash()->toHex()); + } +}