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.Text (Text)
27 import Data.Traversable (Traversable(..))
28 import Data.Tuple (fst, snd)
29 import GHC.Generics (Generic)
30 import GHC.Natural (minusNaturalMaybe)
31 import Numeric.Natural (Natural)
32 import Prelude (fromIntegral)
33 import Text.Show (Show(..))
34 import qualified Control.Monad.Trans.State.Strict as S
35 import qualified Data.Aeson as JSON
36 import qualified Data.ByteString as BS
37 import qualified Data.ByteString.Base64.Lazy as BSL64
38 import qualified Data.ByteString.Lazy as BSL
39 import qualified Data.List as List
41 import Voting.Protocol.Utils
42 import Voting.Protocol.FFC
43 import Voting.Protocol.Credential
45 -- * Type 'Encryption'
46 -- | ElGamal-like encryption.
47 -- Its security relies on the /Discrete Logarithm problem/.
49 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
50 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
51 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
52 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
53 -- to enable the additive homomorphism.
55 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
56 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
57 data Encryption c = Encryption
58 { encryption_nonce :: !(G c)
59 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
60 -- equal to @('groupGen' '^'encNonce)@
61 , encryption_vault :: !(G c)
62 -- ^ Encrypted 'clear' text,
63 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
64 } deriving (Eq,Show,Generic,NFData)
65 instance Reifies c FFC => ToJSON (Encryption c) where
66 toJSON Encryption{..} =
68 [ "alpha" .= encryption_nonce
69 , "beta" .= encryption_vault
71 toEncoding Encryption{..} =
73 ( "alpha" .= encryption_nonce
74 <> "beta" .= encryption_vault
76 instance Reifies c FFC => FromJSON (Encryption c) where
77 parseJSON = JSON.withObject "Encryption" $ \o -> do
78 encryption_nonce <- o .: "alpha"
79 encryption_vault <- o .: "beta"
82 -- | Additive homomorphism.
83 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
84 instance Reifies c FFC => Additive (Encryption c) where
85 zero = Encryption one one
87 (encryption_nonce x * encryption_nonce y)
88 (encryption_vault x * encryption_vault y)
90 -- *** Type 'EncryptionNonce'
91 type EncryptionNonce = E
93 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
95 -- WARNING: the secret encryption nonce (@encNonce@)
96 -- is returned alongside the 'Encryption'
97 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
98 -- but this secret @encNonce@ MUST be forgotten after that,
99 -- as it may be used to decipher the 'Encryption'
100 -- without the 'SecretKey' associated with 'pubKey'.
103 Monad m => RandomGen r =>
104 PublicKey c -> E c ->
105 S.StateT r m (EncryptionNonce c, Encryption c)
106 encrypt pubKey clear = do
108 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
111 { encryption_nonce = groupGen^encNonce
112 , encryption_vault = pubKey ^encNonce * groupGen^clear
116 -- | Non-Interactive Zero-Knowledge 'Proof'
117 -- of knowledge of a discrete logarithm:
118 -- @(secret == logBase base (base^secret))@.
120 { proof_challenge :: Challenge c
121 -- ^ 'Challenge' sent by the verifier to the prover
122 -- to ensure that the prover really has knowledge
123 -- of the secret and is not replaying.
124 -- Actually, 'proof_challenge' is not sent to the prover,
125 -- but derived from the prover's 'Commitment's and statements
126 -- with a collision resistant 'hash'.
127 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
128 , proof_response :: E c
129 -- ^ A discrete logarithm sent by the prover to the verifier,
130 -- as a response to 'proof_challenge'.
132 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
134 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
135 -- * @commitment '==' 'commit' proof base basePowSec '=='
136 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
137 -- * and @basePowSec '==' base'^'sec@,
139 -- then, with overwhelming probability (due to the 'hash' function),
140 -- the prover was not able to choose 'proof_challenge'
141 -- yet was able to compute a 'proof_response' such that
142 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
143 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
144 -- therefore the prover knows 'sec'.
146 -- The prover choses 'commitment' to be a random power of @base@,
147 -- to ensure that each 'prove' does not reveal any information
149 } deriving (Eq,Show,Generic,NFData)
150 instance ToJSON (Proof c) where
153 [ "challenge" .= proof_challenge
154 , "response" .= proof_response
156 toEncoding Proof{..} =
158 ( "challenge" .= proof_challenge
159 <> "response" .= proof_response
161 instance Reifies c FFC => FromJSON (Proof c) where
162 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
163 proof_challenge <- o .: "challenge"
164 proof_response <- o .: "response"
168 -- | Zero-knowledge proof.
170 -- A protocol is /zero-knowledge/ if the verifier
171 -- learns nothing from the protocol except that the prover
174 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
175 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
176 newtype ZKP = ZKP BS.ByteString
178 -- ** Type 'Challenge'
182 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
183 -- by 'hash'ing them (eventually with other 'Commitment's).
185 -- Used in 'prove' it enables a Fiat-Shamir transformation
186 -- of an /interactive zero-knowledge/ (IZK) proof
187 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
188 -- That is to say that the verifier does not have
189 -- to send a 'Challenge' to the prover.
190 -- Indeed, the prover now handles the 'Challenge'
191 -- which becomes a (collision resistant) 'hash'
192 -- of the prover's commitments (and statements to be a stronger proof).
193 type Oracle list c = list (Commitment c) -> Challenge c
195 -- | @('prove' sec commitmentBases oracle)@
196 -- returns a 'Proof' that @sec@ is known
197 -- (by proving the knowledge of its discrete logarithm).
199 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
200 -- raised to the power of the secret nonce of the 'Proof',
201 -- as those are the 'Commitment's that the verifier will obtain
202 -- when composing the 'proof_challenge' and 'proof_response' together
205 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
206 -- the statement must be included in the 'hash' (along with the commitments).
208 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
209 -- does not reveal any information regarding the secret @sec@,
210 -- because two 'Proof's using the same 'Commitment'
211 -- can be used to deduce @sec@ (using the special-soundness).
214 Monad m => RandomGen r => Functor list =>
215 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
216 prove sec commitmentBases oracle = do
218 let commitments = (^ nonce) <$> commitmentBases
219 let proof_challenge = oracle commitments
222 , proof_response = nonce - sec*proof_challenge
225 -- | @('fakeProof')@ returns a 'Proof'
226 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
227 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
228 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
229 -- as a 'Proof' returned by 'prove'.
231 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
232 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
236 RandomGen r => S.StateT r m (Proof c)
238 proof_challenge <- random
239 proof_response <- random
242 -- ** Type 'Commitment'
243 -- | A commitment from the prover to the verifier.
244 -- It's a power of 'groupGen' chosen randomly by the prover
245 -- when making a 'Proof' with 'prove'.
248 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
249 -- from the given 'Proof' with the knowledge of the verifier.
250 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
251 commit Proof{..} base basePowSec =
252 base^proof_response *
253 basePowSec^proof_challenge
254 -- NOTE: Contrary to some textbook presentations,
255 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
256 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
257 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
258 {-# INLINE commit #-}
260 -- * Type 'Disjunction'
261 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
262 -- it's used in 'proveEncryption' to generate a 'Proof'
263 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
266 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
267 booleanDisjunctions = List.take 2 groupGenInverses
269 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
270 intervalDisjunctions mini maxi =
271 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
272 List.genericDrop (nat mini) $
276 -- | Index of a 'Disjunction' within a list of them.
277 -- It is encrypted as an 'E'xponent by 'encrypt'.
280 -- ** Type 'DisjProof'
281 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
282 -- is indexing a 'Disjunction' within a list of them,
283 -- without revealing which 'Opinion' it is.
284 newtype DisjProof c = DisjProof [Proof c]
285 deriving (Eq,Show,Generic)
286 deriving newtype NFData
287 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
288 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
290 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
291 -- returns a 'DisjProof' that 'enc' 'encrypt's
292 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
294 -- The prover proves that it knows an 'encNonce', such that:
295 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
297 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
299 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
302 Monad m => RandomGen r =>
303 PublicKey c -> ZKP ->
304 ([Disjunction c],[Disjunction c]) ->
305 (EncryptionNonce c, Encryption c) ->
306 S.StateT r m (DisjProof c)
307 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
308 -- Fake proofs for all 'Disjunction's except the genuine one.
309 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
310 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
311 let fakeChallengeSum =
312 sum (proof_challenge <$> prevFakeProofs) +
313 sum (proof_challenge <$> nextFakeProofs)
314 let statement = encryptionStatement voterZKP enc
315 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
316 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
317 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
318 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
319 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
320 let challenge = hash statement commitments in
321 let genuineChallenge = challenge - fakeChallengeSum in
323 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
324 -- thus (sum (proof_challenge <$> proofs) == challenge)
325 -- as checked in 'verifyEncryption'.
326 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
327 return (DisjProof proofs)
330 Reifies c FFC => Monad m =>
331 PublicKey c -> ZKP ->
332 [Disjunction c] -> (Encryption c, DisjProof c) ->
333 ExceptT ErrorVerifyEncryption m Bool
334 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
335 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
337 throwE $ ErrorVerifyEncryption_InvalidProofLength
338 (fromIntegral $ List.length proofs)
339 (fromIntegral $ List.length disjs)
341 return $ challengeSum ==
342 hash (encryptionStatement voterZKP enc) (join commitments)
344 challengeSum = sum (proof_challenge <$> proofs)
347 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
348 encryptionStatement (ZKP voterZKP) Encryption{..} =
349 "prove|"<>voterZKP<>"|"
350 <> bytesNat encryption_nonce<>","
351 <> bytesNat encryption_vault<>"|"
353 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
354 -- returns the 'Commitment's with only the knowledge of the verifier.
356 -- For the prover the 'Proof' comes from @fakeProof@,
357 -- and for the verifier the 'Proof' comes from the prover.
358 encryptionCommitments ::
360 PublicKey c -> Encryption c ->
361 Disjunction c -> Proof c -> [G c]
362 encryptionCommitments elecPubKey Encryption{..} disj proof =
363 [ commit proof groupGen encryption_nonce
364 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
365 -- base==groupGen, basePowSec==groupGen^encNonce.
366 , commit proof elecPubKey (encryption_vault*disj)
367 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
368 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
369 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
372 -- ** Type 'ErrorVerifyEncryption'
373 -- | Error raised by 'verifyEncryption'.
374 data ErrorVerifyEncryption
375 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
376 -- ^ When the number of proofs is different than
377 -- the number of 'Disjunction's.
381 data Question = Question
382 { question_text :: !Text
383 , question_choices :: ![Text]
384 , question_mini :: !Natural
385 , question_maxi :: !Natural
386 -- , question_blank :: Maybe Bool
387 } deriving (Eq,Show,Generic,NFData)
388 instance ToJSON Question where
389 toJSON Question{..} =
391 [ "question" .= question_text
392 , "answers" .= question_choices
393 , "min" .= question_mini
394 , "max" .= question_maxi
396 toEncoding Question{..} =
398 ( "question" .= question_text
399 <> "answers" .= question_choices
400 <> "min" .= question_mini
401 <> "max" .= question_maxi
403 instance FromJSON Question where
404 parseJSON = JSON.withObject "Question" $ \o -> do
405 question_text <- o .: "question"
406 question_choices <- o .: "answers"
407 question_mini <- o .: "min"
408 question_maxi <- o .: "max"
412 data Answer c = Answer
413 { answer_opinions :: ![(Encryption c, DisjProof c)]
414 -- ^ Encrypted 'Opinion' for each 'question_choices'
415 -- with a 'DisjProof' that they belong to [0,1].
416 , answer_sumProof :: !(DisjProof c)
417 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
418 -- is an element of @[mini..maxi]@.
419 -- , answer_blankProof ::
420 } deriving (Eq,Show,Generic,NFData)
421 instance Reifies c FFC => ToJSON (Answer c) where
423 let (answer_choices, answer_individual_proofs) =
424 List.unzip answer_opinions in
426 [ "choices" .= answer_choices
427 , "individual_proofs" .= answer_individual_proofs
428 , "overall_proof" .= answer_sumProof
430 toEncoding Answer{..} =
431 let (answer_choices, answer_individual_proofs) =
432 List.unzip answer_opinions in
434 ( "choices" .= answer_choices
435 <> "individual_proofs" .= answer_individual_proofs
436 <> "overall_proof" .= answer_sumProof
438 instance Reifies c FFC => FromJSON (Answer c) where
439 parseJSON = JSON.withObject "Answer" $ \o -> do
440 answer_choices <- o .: "choices"
441 answer_individual_proofs <- o .: "individual_proofs"
442 let answer_opinions = List.zip answer_choices answer_individual_proofs
443 answer_sumProof <- o .: "overall_proof"
446 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
447 -- returns an 'Answer' validable by 'verifyAnswer',
448 -- unless an 'ErrorAnswer' is returned.
451 Monad m => RandomGen r =>
452 PublicKey c -> ZKP ->
453 Question -> [Bool] ->
454 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
455 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
456 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
458 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
459 | List.length opinions /= List.length question_choices =
461 ErrorAnswer_WrongNumberOfOpinions
462 (fromIntegral $ List.length opinions)
463 (fromIntegral $ List.length question_choices)
465 encryptions <- encrypt elecPubKey `mapM` opinions
466 individualProofs <- zipWithM
467 (\opinion -> proveEncryption elecPubKey zkp $
469 then (List.init booleanDisjunctions,[])
470 else ([],List.tail booleanDisjunctions))
471 opinionByChoice encryptions
472 sumProof <- proveEncryption elecPubKey zkp
473 (List.tail <$> List.genericSplitAt
474 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
475 (intervalDisjunctions question_mini question_maxi))
476 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
477 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
480 { answer_opinions = List.zip
481 (snd <$> encryptions) -- NOTE: drop encNonce
483 , answer_sumProof = sumProof
486 opinionsSum = sum $ nat <$> opinions
487 opinions = (\o -> if o then one else zero) <$> opinionByChoice
491 PublicKey c -> ZKP ->
492 Question -> Answer c -> Bool
493 verifyAnswer elecPubKey zkp Question{..} Answer{..}
494 | List.length question_choices /= List.length answer_opinions = False
495 | otherwise = either (const False) id $ runExcept $ do
497 verifyEncryption elecPubKey zkp booleanDisjunctions
498 `traverse` answer_opinions
499 validSum <- verifyEncryption elecPubKey zkp
500 (intervalDisjunctions question_mini question_maxi)
501 ( sum (fst <$> answer_opinions)
503 return (and validOpinions && validSum)
505 -- ** Type 'ErrorAnswer'
506 -- | Error raised by 'encryptAnswer'.
508 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
509 -- ^ When the number of opinions is different than
510 -- the number of choices ('question_choices').
511 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
512 -- ^ When the sum of opinions is not within the bounds
513 -- of 'question_mini' and 'question_maxi'.
514 deriving (Eq,Show,Generic,NFData)
517 data Election c = Election
518 { election_name :: !Text
519 , election_description :: !Text
520 , election_crypto :: !(ElectionCrypto c)
521 , election_questions :: ![Question]
522 , election_uuid :: !UUID
523 , election_hash :: !Hash
524 } deriving (Eq,Show,Generic,NFData)
526 instance ToJSON (Election c) where
527 toJSON Election{..} =
529 [ "name" .= election_name
530 , "description" .= election_description
531 , "public_key" .= election_crypto
532 , "questions" .= election_questions
533 , "uuid" .= election_uuid
535 toEncoding Election{..} =
537 ( "name" .= election_name
538 <> "description" .= election_description
539 <> "public_key" .= election_crypto
540 <> "questions" .= election_questions
541 <> "uuid" .= election_uuid
543 instance FromJSON (Election ()) where
544 parseJSON = JSON.withObject "Election" $ \o -> Election
546 <*> o .: "description"
547 <*> o .: "public_key"
550 <*> pure (hashJSON (JSON.Object o))
552 -- ** Type 'ElectionCrypto'
553 data ElectionCrypto c =
555 { electionCrypto_FFC_params :: !FFC
556 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
557 } deriving (Eq,Show,Generic,NFData)
559 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
560 reifyElection Election{..} k =
561 case election_crypto of
562 ElectionCrypto_FFC ffc (G (F pubKey)) ->
563 reify ffc $ \(_::Proxy c) -> k @c
564 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
566 instance ToJSON (ElectionCrypto c) where
567 toJSON (ElectionCrypto_FFC ffc pubKey) =
572 toEncoding (ElectionCrypto_FFC ffc pubKey) =
577 instance FromJSON (ElectionCrypto ()) where
578 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
580 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
581 return $ ElectionCrypto_FFC ffc (G (F pubKey))
585 newtype Hash = Hash Text
586 deriving (Eq,Ord,Show,Generic)
587 deriving anyclass (ToJSON,FromJSON)
588 deriving newtype NFData
590 hashJSON :: ToJSON a => a -> Hash
591 hashJSON = Hash . hexHash . BSL.toStrict . BSL64.encode . JSON.encode
593 hashElection :: Election c -> Election c
594 hashElection elec = elec{election_hash=hashJSON elec}
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 :: !Hash
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)