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 -- | @('fakeProof')@ returns a 'Proof'
228 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
229 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
230 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
231 -- as a 'Proof' returned by 'prove'.
233 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
234 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
238 RandomGen r => S.StateT r m (Proof c)
240 proof_challenge <- random
241 proof_response <- random
244 -- ** Type 'Commitment'
245 -- | A commitment from the prover to the verifier.
246 -- It's a power of 'groupGen' chosen randomly by the prover
247 -- when making a 'Proof' with 'prove'.
250 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
251 -- from the given 'Proof' with the knowledge of the verifier.
252 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
253 commit Proof{..} base basePowSec =
254 base^proof_response /
255 basePowSec^proof_challenge
256 -- TODO: contrary to some textbook presentations,
257 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
258 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
259 -- this would be compensated by using @('-')@ instead of @('+')@ in 'prove'.
260 {-# INLINE commit #-}
262 -- * Type 'Disjunction'
263 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
264 -- it's used in 'proveEncryption' to generate a 'Proof'
265 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
268 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
269 booleanDisjunctions = List.take 2 groupGenInverses
271 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
272 intervalDisjunctions mini maxi =
273 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
274 List.genericDrop (nat mini) $
278 -- | Index of a 'Disjunction' within a list of them.
279 -- It is encrypted as an 'E'xponent by 'encrypt'.
282 -- ** Type 'DisjProof'
283 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
284 -- is indexing a 'Disjunction' within a list of them,
285 -- without revealing which 'Opinion' it is.
286 newtype DisjProof c = DisjProof [Proof c]
287 deriving (Eq,Show,Generic)
288 deriving newtype NFData
289 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
290 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
292 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
293 -- returns a 'DisjProof' that 'enc' 'encrypt's
294 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
296 -- The prover proves that it knows an 'encNonce', such that:
297 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
299 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
301 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
304 Monad m => RandomGen r =>
305 PublicKey c -> ZKP ->
306 ([Disjunction c],[Disjunction c]) ->
307 (EncryptionNonce c, Encryption c) ->
308 S.StateT r m (DisjProof c)
309 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
310 -- Fake proofs for all 'Disjunction's except the genuine one.
311 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
312 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
313 let fakeChallengeSum =
314 sum (proof_challenge <$> prevFakeProofs) +
315 sum (proof_challenge <$> nextFakeProofs)
316 let statement = encryptionStatement voterZKP enc
317 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
318 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
319 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
320 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
321 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
322 let challenge = hash statement commitments in
323 let genuineChallenge = challenge - fakeChallengeSum in
325 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
326 -- thus (sum (proof_challenge <$> proofs) == challenge)
327 -- as checked in 'verifyEncryption'.
328 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
329 return (DisjProof proofs)
332 Reifies c FFC => Monad m =>
333 PublicKey c -> ZKP ->
334 [Disjunction c] -> (Encryption c, DisjProof c) ->
335 ExceptT ErrorVerifyEncryption m Bool
336 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
337 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
339 throwE $ ErrorVerifyEncryption_InvalidProofLength
340 (fromIntegral $ List.length proofs)
341 (fromIntegral $ List.length disjs)
343 return $ challengeSum ==
344 hash (encryptionStatement voterZKP enc) (join commitments)
346 challengeSum = sum (proof_challenge <$> proofs)
349 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
350 encryptionStatement (ZKP voterZKP) Encryption{..} =
351 "prove|"<>voterZKP<>"|"
352 <> bytesNat encryption_nonce<>","
353 <> bytesNat encryption_vault<>"|"
355 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
356 -- returns the 'Commitment's with only the knowledge of the verifier.
358 -- For the prover the 'Proof' comes from @fakeProof@,
359 -- and for the verifier the 'Proof' comes from the prover.
360 encryptionCommitments ::
362 PublicKey c -> Encryption c ->
363 Disjunction c -> Proof c -> [G c]
364 encryptionCommitments elecPubKey Encryption{..} disj proof =
365 [ commit proof groupGen encryption_nonce
366 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
367 -- base==groupGen, basePowSec==groupGen^encNonce.
368 , commit proof elecPubKey (encryption_vault*disj)
369 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
370 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
371 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
374 -- ** Type 'ErrorVerifyEncryption'
375 -- | Error raised by 'verifyEncryption'.
376 data ErrorVerifyEncryption
377 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
378 -- ^ When the number of proofs is different than
379 -- the number of 'Disjunction's.
383 data Question = Question
384 { question_text :: !Text
385 , question_choices :: ![Text]
386 , question_mini :: !Natural
387 , question_maxi :: !Natural
388 -- , question_blank :: Maybe Bool
389 } deriving (Eq,Show,Generic,NFData)
390 instance ToJSON Question where
391 toJSON Question{..} =
393 [ "question" .= question_text
394 , "answers" .= question_choices
395 , "min" .= question_mini
396 , "max" .= question_maxi
398 toEncoding Question{..} =
400 ( "question" .= question_text
401 <> "answers" .= question_choices
402 <> "min" .= question_mini
403 <> "max" .= question_maxi
405 instance FromJSON Question where
406 parseJSON = JSON.withObject "Question" $ \o -> do
407 question_text <- o .: "question"
408 question_choices <- o .: "answers"
409 question_mini <- o .: "min"
410 question_maxi <- o .: "max"
414 data Answer c = Answer
415 { answer_opinions :: ![(Encryption c, DisjProof c)]
416 -- ^ Encrypted 'Opinion' for each 'question_choices'
417 -- with a 'DisjProof' that they belong to [0,1].
418 , answer_sumProof :: !(DisjProof c)
419 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
420 -- is an element of @[mini..maxi]@.
421 -- , answer_blankProof ::
422 } deriving (Eq,Show,Generic,NFData)
423 instance Reifies c FFC => ToJSON (Answer c) where
425 let (answer_choices, answer_individual_proofs) =
426 List.unzip answer_opinions in
428 [ "choices" .= answer_choices
429 , "individual_proofs" .= answer_individual_proofs
430 , "overall_proof" .= answer_sumProof
432 toEncoding Answer{..} =
433 let (answer_choices, answer_individual_proofs) =
434 List.unzip answer_opinions in
436 ( "choices" .= answer_choices
437 <> "individual_proofs" .= answer_individual_proofs
438 <> "overall_proof" .= answer_sumProof
440 instance Reifies c FFC => FromJSON (Answer c) where
441 parseJSON = JSON.withObject "Answer" $ \o -> do
442 answer_choices <- o .: "choices"
443 answer_individual_proofs <- o .: "individual_proofs"
444 let answer_opinions = List.zip answer_choices answer_individual_proofs
445 answer_sumProof <- o .: "overall_proof"
448 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
449 -- returns an 'Answer' validable by 'verifyAnswer',
450 -- unless an 'ErrorAnswer' is returned.
453 Monad m => RandomGen r =>
454 PublicKey c -> ZKP ->
455 Question -> [Bool] ->
456 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
457 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
458 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
460 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
461 | List.length opinions /= List.length question_choices =
463 ErrorAnswer_WrongNumberOfOpinions
464 (fromIntegral $ List.length opinions)
465 (fromIntegral $ List.length question_choices)
467 encryptions <- encrypt elecPubKey `mapM` opinions
468 individualProofs <- zipWithM
469 (\opinion -> proveEncryption elecPubKey zkp $
471 then (List.init booleanDisjunctions,[])
472 else ([],List.tail booleanDisjunctions))
473 opinionByChoice encryptions
474 sumProof <- proveEncryption elecPubKey zkp
475 (List.tail <$> List.genericSplitAt
476 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
477 (intervalDisjunctions question_mini question_maxi))
478 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
479 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
482 { answer_opinions = List.zip
483 (snd <$> encryptions) -- NOTE: drop encNonce
485 , answer_sumProof = sumProof
488 opinionsSum = sum $ nat <$> opinions
489 opinions = (\o -> if o then one else zero) <$> opinionByChoice
493 PublicKey c -> ZKP ->
494 Question -> Answer c -> Bool
495 verifyAnswer elecPubKey zkp Question{..} Answer{..}
496 | List.length question_choices /= List.length answer_opinions = False
497 | otherwise = either (const False) id $ runExcept $ do
499 verifyEncryption elecPubKey zkp booleanDisjunctions
500 `traverse` answer_opinions
501 validSum <- verifyEncryption elecPubKey zkp
502 (intervalDisjunctions question_mini question_maxi)
503 ( sum (fst <$> answer_opinions)
505 return (and validOpinions && validSum)
507 -- ** Type 'ErrorAnswer'
508 -- | Error raised by 'encryptAnswer'.
510 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
511 -- ^ When the number of opinions is different than
512 -- the number of choices ('question_choices').
513 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
514 -- ^ When the sum of opinions is not within the bounds
515 -- of 'question_mini' and 'question_maxi'.
516 deriving (Eq,Show,Generic,NFData)
519 data Election c = Election
520 { election_name :: !Text
521 , election_description :: !Text
522 , election_crypto :: !(ElectionCrypto c)
523 , election_questions :: ![Question]
524 , election_uuid :: !UUID
525 , election_hash :: !Hash
526 } deriving (Eq,Show,Generic,NFData)
528 instance ToJSON (Election c) where
529 toJSON Election{..} =
531 [ "name" .= election_name
532 , "description" .= election_description
533 , "public_key" .= election_crypto
534 , "questions" .= election_questions
535 , "uuid" .= election_uuid
537 toEncoding Election{..} =
539 ( "name" .= election_name
540 <> "description" .= election_description
541 <> "public_key" .= election_crypto
542 <> "questions" .= election_questions
543 <> "uuid" .= election_uuid
545 instance FromJSON (Election ()) where
546 parseJSON = JSON.withObject "Election" $ \o -> Election
548 <*> o .: "description"
549 <*> o .: "public_key"
552 <*> pure (Base64SHA256 "")
553 -- NOTE: set in 'readElection'.
555 readElection :: FilePath -> ExceptT String IO (Election ())
556 readElection filePath = do
557 fileData <- lift $ BS.readFile filePath
559 (\e -> e{election_hash=base64SHA256 fileData})
560 <$> JSON.eitherDecodeStrict' fileData
562 hashElection :: Election c -> Base64SHA256
563 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
565 -- ** Type 'ElectionCrypto'
566 data ElectionCrypto c =
568 { electionCrypto_FFC_params :: !FFC
569 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
570 } deriving (Eq,Show,Generic,NFData)
572 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
573 reifyElection Election{..} k =
574 case election_crypto of
575 ElectionCrypto_FFC ffc (G (F pubKey)) ->
576 reify ffc $ \(_::Proxy c) -> k @c
577 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
579 instance ToJSON (ElectionCrypto c) where
580 toJSON (ElectionCrypto_FFC ffc pubKey) =
585 toEncoding (ElectionCrypto_FFC ffc pubKey) =
590 instance FromJSON (ElectionCrypto ()) where
591 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
593 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
594 return $ ElectionCrypto_FFC ffc (G (F pubKey))
597 data Ballot c = Ballot
598 { ballot_answers :: ![Answer c]
599 , ballot_signature :: !(Maybe (Signature c))
600 , ballot_election_uuid :: !UUID
601 , ballot_election_hash :: !Base64SHA256
602 } deriving (Generic,NFData)
603 instance Reifies c FFC => ToJSON (Ballot c) where
606 [ "answers" .= ballot_answers
607 , "election_uuid" .= ballot_election_uuid
608 , "election_hash" .= ballot_election_hash
610 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
611 toEncoding Ballot{..} =
613 ( "answers" .= ballot_answers
614 <> "election_uuid" .= ballot_election_uuid
615 <> "election_hash" .= ballot_election_hash
617 maybe mempty (\sig -> "signature" .= sig) ballot_signature
618 instance Reifies c FFC => FromJSON (Ballot c) where
619 parseJSON = JSON.withObject "Ballot" $ \o -> do
620 ballot_answers <- o .: "answers"
621 ballot_signature <- o .:? "signature"
622 ballot_election_uuid <- o .: "election_uuid"
623 ballot_election_hash <- o .: "election_hash"
626 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
627 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
628 -- where 'opinionsByQuest' is a list of 'Opinion's
629 -- on each 'question_choices' of each 'election_questions'.
632 Monad m => RandomGen r =>
634 Maybe (SecretKey c) -> [[Bool]] ->
635 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
636 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
637 | List.length election_questions /= List.length opinionsByQuest =
639 ErrorBallot_WrongNumberOfAnswers
640 (fromIntegral $ List.length opinionsByQuest)
641 (fromIntegral $ List.length election_questions)
643 let (voterKeys, voterZKP) =
644 case ballotSecKeyMay of
645 Nothing -> (Nothing, ZKP "")
647 ( Just (ballotSecKey, ballotPubKey)
648 , ZKP (bytesNat ballotPubKey) )
649 where ballotPubKey = publicKey ballotSecKey
651 S.mapStateT (withExceptT ErrorBallot_Answer) $
652 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
653 election_questions opinionsByQuest
654 ballot_signature <- case voterKeys of
655 Nothing -> return Nothing
656 Just (ballotSecKey, signature_publicKey) -> do
658 prove ballotSecKey (Identity groupGen) $
659 \(Identity commitment) ->
661 -- NOTE: the order is unusual, the commitments are first
662 -- then comes the statement. Best guess is that
663 -- this is easier to code due to their respective types.
664 (signatureCommitments voterZKP commitment)
665 (signatureStatement ballot_answers)
666 return $ Just Signature{..}
669 , ballot_election_hash = election_hash
670 , ballot_election_uuid = election_uuid
674 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
675 verifyBallot Election{..} Ballot{..} =
676 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
677 ballot_election_uuid == election_uuid &&
678 ballot_election_hash == election_hash &&
679 List.length election_questions == List.length ballot_answers &&
680 let (isValidSign, zkpSign) =
681 case ballot_signature of
682 Nothing -> (True, ZKP "")
683 Just Signature{..} ->
684 let zkp = ZKP (bytesNat signature_publicKey) in
686 proof_challenge signature_proof == hash
687 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
688 (signatureStatement ballot_answers)
691 List.zipWith (verifyAnswer elecPubKey zkpSign)
692 election_questions ballot_answers
694 -- ** Type 'Signature'
695 -- | Schnorr-like signature.
697 -- Used by each voter to sign his/her encrypted 'Ballot'
698 -- using his/her 'Credential',
699 -- in order to avoid ballot stuffing.
700 data Signature c = Signature
701 { signature_publicKey :: !(PublicKey c)
702 -- ^ Verification key.
703 , signature_proof :: !(Proof c)
704 } deriving (Generic,NFData)
705 instance Reifies c FFC => ToJSON (Signature c) where
706 toJSON (Signature pubKey Proof{..}) =
708 [ "public_key" .= pubKey
709 , "challenge" .= proof_challenge
710 , "response" .= proof_response
712 toEncoding (Signature pubKey Proof{..}) =
714 ( "public_key" .= pubKey
715 <> "challenge" .= proof_challenge
716 <> "response" .= proof_response
718 instance Reifies c FFC => FromJSON (Signature c) where
719 parseJSON = JSON.withObject "Signature" $ \o -> do
720 signature_publicKey <- o .: "public_key"
721 proof_challenge <- o .: "challenge"
722 proof_response <- o .: "response"
723 let signature_proof = Proof{..}
728 -- | @('signatureStatement' answers)@
729 -- returns the encrypted material to be signed:
730 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
731 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
733 foldMap $ \Answer{..} ->
734 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
735 [encryption_nonce, encryption_vault]
737 -- | @('signatureCommitments' voterZKP commitment)@
738 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
739 signatureCommitments (ZKP voterZKP) commitment =
740 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
741 <> bytesNat commitment<>"|"
743 -- ** Type 'ErrorBallot'
744 -- | Error raised by 'encryptBallot'.
746 = ErrorBallot_WrongNumberOfAnswers Natural Natural
747 -- ^ When the number of answers
748 -- is different than the number of questions.
749 | ErrorBallot_Answer ErrorAnswer
750 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
752 -- ^ TODO: to be more precise.
753 deriving (Eq,Show,Generic,NFData)