1 {-# LANGUAGE DeriveAnyClass #-}
2 {-# LANGUAGE DeriveGeneric #-}
3 {-# LANGUAGE DerivingStrategies #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE Rank2Types #-} -- for reifyElection
6 {-# LANGUAGE UndecidableInstances #-} -- for Reifies instances
7 module Voting.Protocol.Election where
9 import Control.Applicative (Applicative(..))
10 import Control.DeepSeq (NFData)
11 import Control.Monad (Monad(..), join, mapM, replicateM, zipWithM)
12 import Control.Monad.Trans.Class (MonadTrans(..))
13 import Control.Monad.Trans.Except (ExceptT(..), runExcept, throwE, withExceptT)
14 import Data.Aeson (ToJSON(..),FromJSON(..),(.:),(.:?),(.=))
16 import Data.Either (either)
17 import Data.Eq (Eq(..))
18 import Data.Foldable (Foldable, foldMap, and)
19 import Data.Function (($), (.), id, const)
20 import Data.Functor (Functor, (<$>))
21 import Data.Functor.Identity (Identity(..))
22 import Data.Maybe (Maybe(..), maybe, fromJust)
23 import Data.Monoid (Monoid(..))
24 import Data.Ord (Ord(..))
25 import Data.Semigroup (Semigroup(..))
26 import Data.String (String)
27 import Data.Text (Text)
28 import Data.Traversable (Traversable(..))
29 import Data.Tuple (fst, snd)
30 import GHC.Generics (Generic)
31 import GHC.Natural (minusNaturalMaybe)
32 import Numeric.Natural (Natural)
33 import Prelude (fromIntegral)
34 import System.IO (IO, FilePath)
35 import Text.Show (Show(..))
36 import qualified Control.Monad.Trans.State.Strict as S
37 import qualified Data.Aeson as JSON
38 import qualified Data.ByteString as BS
39 import qualified Data.ByteString.Lazy as BSL
40 import qualified Data.List as List
42 import Voting.Protocol.Utils
43 import Voting.Protocol.FFC
44 import Voting.Protocol.Credential
46 -- * Type 'Encryption'
47 -- | ElGamal-like encryption.
48 -- Its security relies on the /Discrete Logarithm problem/.
50 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
51 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
52 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
53 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
54 -- to enable the additive homomorphism.
56 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
57 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
58 data Encryption c = Encryption
59 { encryption_nonce :: !(G c)
60 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
61 -- equal to @('groupGen' '^'encNonce)@
62 , encryption_vault :: !(G c)
63 -- ^ Encrypted 'clear' text,
64 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
65 } deriving (Eq,Show,Generic,NFData)
66 instance Reifies c FFC => ToJSON (Encryption c) where
67 toJSON Encryption{..} =
69 [ "alpha" .= encryption_nonce
70 , "beta" .= encryption_vault
72 toEncoding Encryption{..} =
74 ( "alpha" .= encryption_nonce
75 <> "beta" .= encryption_vault
77 instance Reifies c FFC => FromJSON (Encryption c) where
78 parseJSON = JSON.withObject "Encryption" $ \o -> do
79 encryption_nonce <- o .: "alpha"
80 encryption_vault <- o .: "beta"
83 -- | Additive homomorphism.
84 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
85 instance Reifies c FFC => Additive (Encryption c) where
86 zero = Encryption one one
88 (encryption_nonce x * encryption_nonce y)
89 (encryption_vault x * encryption_vault y)
91 -- *** Type 'EncryptionNonce'
92 type EncryptionNonce = E
94 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
96 -- WARNING: the secret encryption nonce (@encNonce@)
97 -- is returned alongside the 'Encryption'
98 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
99 -- but this secret @encNonce@ MUST be forgotten after that,
100 -- as it may be used to decipher the 'Encryption'
101 -- without the 'SecretKey' associated with 'pubKey'.
104 Monad m => RandomGen r =>
105 PublicKey c -> E c ->
106 S.StateT r m (EncryptionNonce c, Encryption c)
107 encrypt pubKey clear = do
109 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
112 { encryption_nonce = groupGen^encNonce
113 , encryption_vault = pubKey ^encNonce * groupGen^clear
117 -- | Non-Interactive Zero-Knowledge 'Proof'
118 -- of knowledge of a discrete logarithm:
119 -- @(secret == logBase base (base^secret))@.
121 { proof_challenge :: Challenge c
122 -- ^ 'Challenge' sent by the verifier to the prover
123 -- to ensure that the prover really has knowledge
124 -- of the secret and is not replaying.
125 -- Actually, 'proof_challenge' is not sent to the prover,
126 -- but derived from the prover's 'Commitment's and statements
127 -- with a collision resistant 'hash'.
128 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
129 , proof_response :: E c
130 -- ^ A discrete logarithm sent by the prover to the verifier,
131 -- as a response to 'proof_challenge'.
133 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
135 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
136 -- * @commitment '==' 'commit' proof base basePowSec '=='
137 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
138 -- * and @basePowSec '==' base'^'sec@,
140 -- then, with overwhelming probability (due to the 'hash' function),
141 -- the prover was not able to choose 'proof_challenge'
142 -- yet was able to compute a 'proof_response' such that
143 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
144 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
145 -- therefore the prover knows 'sec'.
147 -- The prover choses 'commitment' to be a random power of @base@,
148 -- to ensure that each 'prove' does not reveal any information
150 } deriving (Eq,Show,Generic,NFData)
151 instance ToJSON (Proof c) where
154 [ "challenge" .= proof_challenge
155 , "response" .= proof_response
157 toEncoding Proof{..} =
159 ( "challenge" .= proof_challenge
160 <> "response" .= proof_response
162 instance Reifies c FFC => FromJSON (Proof c) where
163 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
164 proof_challenge <- o .: "challenge"
165 proof_response <- o .: "response"
169 -- | Zero-knowledge proof.
171 -- A protocol is /zero-knowledge/ if the verifier
172 -- learns nothing from the protocol except that the prover
175 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
176 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
177 newtype ZKP = ZKP BS.ByteString
179 -- ** Type 'Challenge'
183 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
184 -- by 'hash'ing them (eventually with other 'Commitment's).
186 -- Used in 'prove' it enables a Fiat-Shamir transformation
187 -- of an /interactive zero-knowledge/ (IZK) proof
188 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
189 -- That is to say that the verifier does not have
190 -- to send a 'Challenge' to the prover.
191 -- Indeed, the prover now handles the 'Challenge'
192 -- which becomes a (collision resistant) 'hash'
193 -- of the prover's commitments (and statements to be a stronger proof).
194 type Oracle list c = list (Commitment c) -> Challenge c
196 -- | @('prove' sec commitmentBases oracle)@
197 -- returns a 'Proof' that @sec@ is known
198 -- (by proving the knowledge of its discrete logarithm).
200 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
201 -- raised to the power of the secret nonce of the 'Proof',
202 -- as those are the 'Commitment's that the verifier will obtain
203 -- when composing the 'proof_challenge' and 'proof_response' together
206 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
207 -- the statement must be included in the 'hash' (along with the commitments).
209 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
210 -- does not reveal any information regarding the secret @sec@,
211 -- because two 'Proof's using the same 'Commitment'
212 -- can be used to deduce @sec@ (using the special-soundness).
215 Monad m => RandomGen r => Functor list =>
216 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
217 prove sec commitmentBases oracle = do
219 let commitments = (^ nonce) <$> commitmentBases
220 let proof_challenge = oracle commitments
223 , proof_response = nonce + sec*proof_challenge
224 -- TODO: switch (+) to (-) when 'commit' will switch (/) to (*).
227 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
228 -- when Helios-C specifications will be fixed.
231 Monad m => RandomGen r => Functor list =>
232 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
233 proveQuicker sec commitmentBases oracle = do
235 let commitments = (^ nonce) <$> commitmentBases
236 let proof_challenge = oracle commitments
239 , proof_response = nonce - sec*proof_challenge
242 -- | @('fakeProof')@ returns a 'Proof'
243 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
244 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
245 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
246 -- as a 'Proof' returned by 'prove'.
248 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
249 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
253 RandomGen r => S.StateT r m (Proof c)
255 proof_challenge <- random
256 proof_response <- random
259 -- ** Type 'Commitment'
260 -- | A commitment from the prover to the verifier.
261 -- It's a power of 'groupGen' chosen randomly by the prover
262 -- when making a 'Proof' with 'prove'.
265 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
266 -- from the given 'Proof' with the knowledge of the verifier.
267 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
268 commit Proof{..} base basePowSec =
269 base^proof_response /
270 basePowSec^proof_challenge
271 -- TODO: contrary to some textbook presentations,
272 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
273 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
274 -- this would be compensated by using @('-')@ instead of @('+')@ in 'prove'.
275 {-# INLINE commit #-}
277 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
278 -- when Helios-C specifications will be fixed.
279 commitQuicker :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
280 commitQuicker Proof{..} base basePowSec =
281 base^proof_response *
282 basePowSec^proof_challenge
284 -- * Type 'Disjunction'
285 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
286 -- it's used in 'proveEncryption' to generate a 'Proof'
287 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
290 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
291 booleanDisjunctions = List.take 2 groupGenInverses
293 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
294 intervalDisjunctions mini maxi =
295 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
296 List.genericDrop (nat mini) $
300 -- | Index of a 'Disjunction' within a list of them.
301 -- It is encrypted as an 'E'xponent by 'encrypt'.
304 -- ** Type 'DisjProof'
305 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
306 -- is indexing a 'Disjunction' within a list of them,
307 -- without revealing which 'Opinion' it is.
308 newtype DisjProof c = DisjProof [Proof c]
309 deriving (Eq,Show,Generic)
310 deriving newtype NFData
311 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
312 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
314 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
315 -- returns a 'DisjProof' that 'enc' 'encrypt's
316 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
318 -- The prover proves that it knows an 'encNonce', such that:
319 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
321 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
323 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
326 Monad m => RandomGen r =>
327 PublicKey c -> ZKP ->
328 ([Disjunction c],[Disjunction c]) ->
329 (EncryptionNonce c, Encryption c) ->
330 S.StateT r m (DisjProof c)
331 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
332 -- Fake proofs for all 'Disjunction's except the genuine one.
333 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
334 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
335 let fakeChallengeSum =
336 sum (proof_challenge <$> prevFakeProofs) +
337 sum (proof_challenge <$> nextFakeProofs)
338 let statement = encryptionStatement voterZKP enc
339 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
340 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
341 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
342 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
343 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
344 let challenge = hash statement commitments in
345 let genuineChallenge = challenge - fakeChallengeSum in
347 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
348 -- thus (sum (proof_challenge <$> proofs) == challenge)
349 -- as checked in 'verifyEncryption'.
350 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
351 return (DisjProof proofs)
354 Reifies c FFC => Monad m =>
355 PublicKey c -> ZKP ->
356 [Disjunction c] -> (Encryption c, DisjProof c) ->
357 ExceptT ErrorVerifyEncryption m Bool
358 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
359 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
361 throwE $ ErrorVerifyEncryption_InvalidProofLength
362 (fromIntegral $ List.length proofs)
363 (fromIntegral $ List.length disjs)
365 return $ challengeSum ==
366 hash (encryptionStatement voterZKP enc) (join commitments)
368 challengeSum = sum (proof_challenge <$> proofs)
371 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
372 encryptionStatement (ZKP voterZKP) Encryption{..} =
373 "prove|"<>voterZKP<>"|"
374 <> bytesNat encryption_nonce<>","
375 <> bytesNat encryption_vault<>"|"
377 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
378 -- returns the 'Commitment's with only the knowledge of the verifier.
380 -- For the prover the 'Proof' comes from @fakeProof@,
381 -- and for the verifier the 'Proof' comes from the prover.
382 encryptionCommitments ::
384 PublicKey c -> Encryption c ->
385 Disjunction c -> Proof c -> [G c]
386 encryptionCommitments elecPubKey Encryption{..} disj proof =
387 [ commit proof groupGen encryption_nonce
388 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
389 -- base==groupGen, basePowSec==groupGen^encNonce.
390 , commit proof elecPubKey (encryption_vault*disj)
391 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
392 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
393 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
396 -- ** Type 'ErrorVerifyEncryption'
397 -- | Error raised by 'verifyEncryption'.
398 data ErrorVerifyEncryption
399 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
400 -- ^ When the number of proofs is different than
401 -- the number of 'Disjunction's.
405 data Question = Question
406 { question_text :: !Text
407 , question_choices :: ![Text]
408 , question_mini :: !Natural
409 , question_maxi :: !Natural
410 -- , question_blank :: Maybe Bool
411 } deriving (Eq,Show,Generic,NFData)
412 instance ToJSON Question where
413 toJSON Question{..} =
415 [ "question" .= question_text
416 , "answers" .= question_choices
417 , "min" .= question_mini
418 , "max" .= question_maxi
420 toEncoding Question{..} =
422 ( "question" .= question_text
423 <> "answers" .= question_choices
424 <> "min" .= question_mini
425 <> "max" .= question_maxi
427 instance FromJSON Question where
428 parseJSON = JSON.withObject "Question" $ \o -> do
429 question_text <- o .: "question"
430 question_choices <- o .: "answers"
431 question_mini <- o .: "min"
432 question_maxi <- o .: "max"
436 data Answer c = Answer
437 { answer_opinions :: ![(Encryption c, DisjProof c)]
438 -- ^ Encrypted 'Opinion' for each 'question_choices'
439 -- with a 'DisjProof' that they belong to [0,1].
440 , answer_sumProof :: !(DisjProof c)
441 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
442 -- is an element of @[mini..maxi]@.
443 -- , answer_blankProof ::
444 } deriving (Eq,Show,Generic,NFData)
445 instance Reifies c FFC => ToJSON (Answer c) where
447 let (answer_choices, answer_individual_proofs) =
448 List.unzip answer_opinions in
450 [ "choices" .= answer_choices
451 , "individual_proofs" .= answer_individual_proofs
452 , "overall_proof" .= answer_sumProof
454 toEncoding Answer{..} =
455 let (answer_choices, answer_individual_proofs) =
456 List.unzip answer_opinions in
458 ( "choices" .= answer_choices
459 <> "individual_proofs" .= answer_individual_proofs
460 <> "overall_proof" .= answer_sumProof
462 instance Reifies c FFC => FromJSON (Answer c) where
463 parseJSON = JSON.withObject "Answer" $ \o -> do
464 answer_choices <- o .: "choices"
465 answer_individual_proofs <- o .: "individual_proofs"
466 let answer_opinions = List.zip answer_choices answer_individual_proofs
467 answer_sumProof <- o .: "overall_proof"
470 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
471 -- returns an 'Answer' validable by 'verifyAnswer',
472 -- unless an 'ErrorAnswer' is returned.
475 Monad m => RandomGen r =>
476 PublicKey c -> ZKP ->
477 Question -> [Bool] ->
478 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
479 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
480 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
482 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
483 | List.length opinions /= List.length question_choices =
485 ErrorAnswer_WrongNumberOfOpinions
486 (fromIntegral $ List.length opinions)
487 (fromIntegral $ List.length question_choices)
489 encryptions <- encrypt elecPubKey `mapM` opinions
490 individualProofs <- zipWithM
491 (\opinion -> proveEncryption elecPubKey zkp $
493 then (List.init booleanDisjunctions,[])
494 else ([],List.tail booleanDisjunctions))
495 opinionByChoice encryptions
496 sumProof <- proveEncryption elecPubKey zkp
497 (List.tail <$> List.genericSplitAt
498 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
499 (intervalDisjunctions question_mini question_maxi))
500 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
501 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
504 { answer_opinions = List.zip
505 (snd <$> encryptions) -- NOTE: drop encNonce
507 , answer_sumProof = sumProof
510 opinionsSum = sum $ nat <$> opinions
511 opinions = (\o -> if o then one else zero) <$> opinionByChoice
515 PublicKey c -> ZKP ->
516 Question -> Answer c -> Bool
517 verifyAnswer elecPubKey zkp Question{..} Answer{..}
518 | List.length question_choices /= List.length answer_opinions = False
519 | otherwise = either (const False) id $ runExcept $ do
521 verifyEncryption elecPubKey zkp booleanDisjunctions
522 `traverse` answer_opinions
523 validSum <- verifyEncryption elecPubKey zkp
524 (intervalDisjunctions question_mini question_maxi)
525 ( sum (fst <$> answer_opinions)
527 return (and validOpinions && validSum)
529 -- ** Type 'ErrorAnswer'
530 -- | Error raised by 'encryptAnswer'.
532 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
533 -- ^ When the number of opinions is different than
534 -- the number of choices ('question_choices').
535 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
536 -- ^ When the sum of opinions is not within the bounds
537 -- of 'question_mini' and 'question_maxi'.
538 deriving (Eq,Show,Generic,NFData)
541 data Election c = Election
542 { election_name :: !Text
543 , election_description :: !Text
544 , election_crypto :: !(ElectionCrypto c)
545 , election_questions :: ![Question]
546 , election_uuid :: !UUID
547 , election_hash :: Base64SHA256
548 } deriving (Eq,Show,Generic,NFData)
550 instance ToJSON (Election c) where
551 toJSON Election{..} =
553 [ "name" .= election_name
554 , "description" .= election_description
555 , "public_key" .= election_crypto
556 , "questions" .= election_questions
557 , "uuid" .= election_uuid
559 toEncoding Election{..} =
561 ( "name" .= election_name
562 <> "description" .= election_description
563 <> "public_key" .= election_crypto
564 <> "questions" .= election_questions
565 <> "uuid" .= election_uuid
567 instance FromJSON (Election ()) where
568 parseJSON = JSON.withObject "Election" $ \o -> Election
570 <*> o .: "description"
571 <*> o .: "public_key"
574 <*> pure (Base64SHA256 "")
575 -- NOTE: set in 'readElection'.
577 readElection :: FilePath -> ExceptT String IO (Election ())
578 readElection filePath = do
579 fileData <- lift $ BS.readFile filePath
581 (\e -> e{election_hash=base64SHA256 fileData})
582 <$> JSON.eitherDecodeStrict' fileData
584 hashElection :: Election c -> Base64SHA256
585 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
587 -- ** Type 'ElectionCrypto'
588 data ElectionCrypto c =
590 { electionCrypto_FFC_params :: !FFC
591 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
592 } deriving (Eq,Show,Generic,NFData)
594 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
595 reifyElection Election{..} k =
596 case election_crypto of
597 ElectionCrypto_FFC ffc (G (F pubKey)) ->
598 reify ffc $ \(_::Proxy c) -> k @c
599 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
601 instance ToJSON (ElectionCrypto c) where
602 toJSON (ElectionCrypto_FFC ffc pubKey) =
607 toEncoding (ElectionCrypto_FFC ffc pubKey) =
612 instance FromJSON (ElectionCrypto ()) where
613 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
615 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
616 return $ ElectionCrypto_FFC ffc (G (F pubKey))
619 data Ballot c = Ballot
620 { ballot_answers :: ![Answer c]
621 , ballot_signature :: !(Maybe (Signature c))
622 , ballot_election_uuid :: !UUID
623 , ballot_election_hash :: !Base64SHA256
624 } deriving (Generic,NFData)
625 instance Reifies c FFC => ToJSON (Ballot c) where
628 [ "answers" .= ballot_answers
629 , "election_uuid" .= ballot_election_uuid
630 , "election_hash" .= ballot_election_hash
632 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
633 toEncoding Ballot{..} =
635 ( "answers" .= ballot_answers
636 <> "election_uuid" .= ballot_election_uuid
637 <> "election_hash" .= ballot_election_hash
639 maybe mempty (\sig -> "signature" .= sig) ballot_signature
640 instance Reifies c FFC => FromJSON (Ballot c) where
641 parseJSON = JSON.withObject "Ballot" $ \o -> do
642 ballot_answers <- o .: "answers"
643 ballot_signature <- o .:? "signature"
644 ballot_election_uuid <- o .: "election_uuid"
645 ballot_election_hash <- o .: "election_hash"
648 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
649 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
650 -- where 'opinionsByQuest' is a list of 'Opinion's
651 -- on each 'question_choices' of each 'election_questions'.
654 Monad m => RandomGen r =>
656 Maybe (SecretKey c) -> [[Bool]] ->
657 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
658 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
659 | List.length election_questions /= List.length opinionsByQuest =
661 ErrorBallot_WrongNumberOfAnswers
662 (fromIntegral $ List.length opinionsByQuest)
663 (fromIntegral $ List.length election_questions)
665 let (voterKeys, voterZKP) =
666 case ballotSecKeyMay of
667 Nothing -> (Nothing, ZKP "")
669 ( Just (ballotSecKey, ballotPubKey)
670 , ZKP (bytesNat ballotPubKey) )
671 where ballotPubKey = publicKey ballotSecKey
673 S.mapStateT (withExceptT ErrorBallot_Answer) $
674 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
675 election_questions opinionsByQuest
676 ballot_signature <- case voterKeys of
677 Nothing -> return Nothing
678 Just (ballotSecKey, signature_publicKey) -> do
680 proveQuicker ballotSecKey (Identity groupGen) $
681 \(Identity commitment) ->
683 -- NOTE: the order is unusual, the commitments are first
684 -- then comes the statement. Best guess is that
685 -- this is easier to code due to their respective types.
686 (signatureCommitments voterZKP commitment)
687 (signatureStatement ballot_answers)
688 return $ Just Signature{..}
691 , ballot_election_hash = election_hash
692 , ballot_election_uuid = election_uuid
696 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
697 verifyBallot Election{..} Ballot{..} =
698 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
699 ballot_election_uuid == election_uuid &&
700 ballot_election_hash == election_hash &&
701 List.length election_questions == List.length ballot_answers &&
702 let (isValidSign, zkpSign) =
703 case ballot_signature of
704 Nothing -> (True, ZKP "")
705 Just Signature{..} ->
706 let zkp = ZKP (bytesNat signature_publicKey) in
708 proof_challenge signature_proof == hash
709 (signatureCommitments zkp (commitQuicker signature_proof groupGen signature_publicKey))
710 (signatureStatement ballot_answers)
713 List.zipWith (verifyAnswer elecPubKey zkpSign)
714 election_questions ballot_answers
716 -- ** Type 'Signature'
717 -- | Schnorr-like signature.
719 -- Used by each voter to sign his/her encrypted 'Ballot'
720 -- using his/her 'Credential',
721 -- in order to avoid ballot stuffing.
722 data Signature c = Signature
723 { signature_publicKey :: !(PublicKey c)
724 -- ^ Verification key.
725 , signature_proof :: !(Proof c)
726 } deriving (Generic,NFData)
727 instance Reifies c FFC => ToJSON (Signature c) where
728 toJSON (Signature pubKey Proof{..}) =
730 [ "public_key" .= pubKey
731 , "challenge" .= proof_challenge
732 , "response" .= proof_response
734 toEncoding (Signature pubKey Proof{..}) =
736 ( "public_key" .= pubKey
737 <> "challenge" .= proof_challenge
738 <> "response" .= proof_response
740 instance Reifies c FFC => FromJSON (Signature c) where
741 parseJSON = JSON.withObject "Signature" $ \o -> do
742 signature_publicKey <- o .: "public_key"
743 proof_challenge <- o .: "challenge"
744 proof_response <- o .: "response"
745 let signature_proof = Proof{..}
750 -- | @('signatureStatement' answers)@
751 -- returns the encrypted material to be signed:
752 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
753 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
755 foldMap $ \Answer{..} ->
756 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
757 [encryption_nonce, encryption_vault]
759 -- | @('signatureCommitments' voterZKP commitment)@
760 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
761 signatureCommitments (ZKP voterZKP) commitment =
762 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
763 <> bytesNat commitment<>"|"
765 -- ** Type 'ErrorBallot'
766 -- | Error raised by 'encryptBallot'.
768 = ErrorBallot_WrongNumberOfAnswers Natural Natural
769 -- ^ When the number of answers
770 -- is different than the number of questions.
771 | ErrorBallot_Answer ErrorAnswer
772 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
774 -- ^ TODO: to be more precise.
775 deriving (Eq,Show,Generic,NFData)