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(..), fromJust)
23 import Data.Ord (Ord(..))
24 import Data.Semigroup (Semigroup(..))
25 import Data.Text (Text)
26 import Data.Traversable (Traversable(..))
27 import Data.Tuple (fst, snd)
28 import GHC.Generics (Generic)
29 import GHC.Natural (minusNaturalMaybe)
30 import Numeric.Natural (Natural)
31 import Prelude (fromIntegral)
32 import Text.Show (Show(..))
33 import qualified Control.Monad.Trans.State.Strict as S
34 import qualified Data.Aeson as JSON
35 import qualified Data.ByteString as BS
36 import qualified Data.ByteString.Base64.Lazy as BSL64
37 import qualified Data.List as List
38 import qualified Data.Text.Lazy as TL
39 import qualified Data.Text.Lazy.Encoding as TL
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 deriving instance Reifies c FFC => ToJSON (Encryption c)
66 instance Reifies c FFC => FromJSON (Encryption c) where
67 parseJSON = JSON.withObject "Encryption" $ \o -> do
68 encryption_nonce <- o .: "alpha"
69 encryption_vault <- o .: "beta"
72 -- | Additive homomorphism.
73 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
74 instance Reifies c FFC => Additive (Encryption c) where
75 zero = Encryption one one
77 (encryption_nonce x * encryption_nonce y)
78 (encryption_vault x * encryption_vault y)
80 -- *** Type 'EncryptionNonce'
81 type EncryptionNonce = E
83 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
85 -- WARNING: the secret encryption nonce (@encNonce@)
86 -- is returned alongside the 'Encryption'
87 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
88 -- but this secret @encNonce@ MUST be forgotten after that,
89 -- as it may be used to decipher the 'Encryption'
90 -- without the 'SecretKey' associated with 'pubKey'.
93 Monad m => RandomGen r =>
95 S.StateT r m (EncryptionNonce c, Encryption c)
96 encrypt pubKey clear = do
98 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
101 { encryption_nonce = groupGen^encNonce
102 , encryption_vault = pubKey ^encNonce * groupGen^clear
106 -- | Non-Interactive Zero-Knowledge 'Proof'
107 -- of knowledge of a discrete logarithm:
108 -- @(secret == logBase base (base^secret))@.
110 { proof_challenge :: Challenge c
111 -- ^ 'Challenge' sent by the verifier to the prover
112 -- to ensure that the prover really has knowledge
113 -- of the secret and is not replaying.
114 -- Actually, 'proof_challenge' is not sent to the prover,
115 -- but derived from the prover's 'Commitment's and statements
116 -- with a collision resistant 'hash'.
117 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
118 , proof_response :: E c
119 -- ^ A discrete logarithm sent by the prover to the verifier,
120 -- as a response to 'proof_challenge'.
122 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
124 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
125 -- * @commitment '==' 'commit' proof base basePowSec '=='
126 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
127 -- * and @basePowSec '==' base'^'sec@,
129 -- then, with overwhelming probability (due to the 'hash' function),
130 -- the prover was not able to choose 'proof_challenge'
131 -- yet was able to compute a 'proof_response' such that
132 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
133 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
134 -- therefore the prover knows 'sec'.
136 -- The prover choses 'commitment' to be a random power of @base@,
137 -- to ensure that each 'prove' does not reveal any information
139 } deriving (Eq,Show,Generic,NFData)
140 instance ToJSON (Proof c) where
143 [ "challenge" .= proof_challenge
144 , "response" .= proof_response
146 toEncoding Proof{..} =
148 ( "challenge" .= proof_challenge
149 <> "response" .= proof_response
151 instance Reifies c FFC => FromJSON (Proof c) where
152 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
153 proof_challenge <- o .: "challenge"
154 proof_response <- o .: "response"
158 -- | Zero-knowledge proof.
160 -- A protocol is /zero-knowledge/ if the verifier
161 -- learns nothing from the protocol except that the prover
164 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
165 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
166 newtype ZKP = ZKP BS.ByteString
168 -- ** Type 'Challenge'
172 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
173 -- by 'hash'ing them (eventually with other 'Commitment's).
175 -- Used in 'prove' it enables a Fiat-Shamir transformation
176 -- of an /interactive zero-knowledge/ (IZK) proof
177 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
178 -- That is to say that the verifier does not have
179 -- to send a 'Challenge' to the prover.
180 -- Indeed, the prover now handles the 'Challenge'
181 -- which becomes a (collision resistant) 'hash'
182 -- of the prover's commitments (and statements to be a stronger proof).
183 type Oracle list c = list (Commitment c) -> Challenge c
185 -- | @('prove' sec commitmentBases oracle)@
186 -- returns a 'Proof' that @sec@ is known
187 -- (by proving the knowledge of its discrete logarithm).
189 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
190 -- raised to the power of the secret nonce of the 'Proof',
191 -- as those are the 'Commitment's that the verifier will obtain
192 -- when composing the 'proof_challenge' and 'proof_response' together
195 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
196 -- the statement must be included in the 'hash' (along with the commitments).
198 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
199 -- does not reveal any information regarding the secret @sec@,
200 -- because two 'Proof's using the same 'Commitment'
201 -- can be used to deduce @sec@ (using the special-soundness).
204 Monad m => RandomGen r => Functor list =>
205 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
206 prove sec commitmentBases oracle = do
208 let commitments = (^ nonce) <$> commitmentBases
209 let proof_challenge = oracle commitments
212 , proof_response = nonce - sec*proof_challenge
215 -- | @('fakeProof')@ returns a 'Proof'
216 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
217 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
218 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
219 -- as a 'Proof' returned by 'prove'.
221 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
222 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
226 RandomGen r => S.StateT r m (Proof c)
228 proof_challenge <- random
229 proof_response <- random
232 -- ** Type 'Commitment'
233 -- | A commitment from the prover to the verifier.
234 -- It's a power of 'groupGen' chosen randomly by the prover
235 -- when making a 'Proof' with 'prove'.
238 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
239 -- from the given 'Proof' with the knowledge of the verifier.
240 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
241 commit Proof{..} base basePowSec =
242 base^proof_response *
243 basePowSec^proof_challenge
244 -- NOTE: Contrary to some textbook presentations,
245 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
246 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
247 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
248 {-# INLINE commit #-}
250 -- * Type 'Disjunction'
251 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
252 -- it's used in 'proveEncryption' to generate a 'Proof'
253 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
256 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
257 booleanDisjunctions = List.take 2 groupGenInverses
259 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
260 intervalDisjunctions mini maxi =
261 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
262 List.genericDrop (nat mini) $
266 -- | Index of a 'Disjunction' within a list of them.
267 -- It is encrypted as an 'E'xponent by 'encrypt'.
270 -- ** Type 'DisjProof'
271 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
272 -- is indexing a 'Disjunction' within a list of them,
273 -- without revealing which 'Opinion' it is.
274 newtype DisjProof c = DisjProof [Proof c]
275 deriving (Eq,Show,Generic)
276 deriving newtype NFData
277 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
278 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
280 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
281 -- returns a 'DisjProof' that 'enc' 'encrypt's
282 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
284 -- The prover proves that it knows an 'encNonce', such that:
285 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
287 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
289 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
292 Monad m => RandomGen r =>
293 PublicKey c -> ZKP ->
294 ([Disjunction c],[Disjunction c]) ->
295 (EncryptionNonce c, Encryption c) ->
296 S.StateT r m (DisjProof c)
297 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
298 -- Fake proofs for all 'Disjunction's except the genuine one.
299 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
300 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
301 let fakeChallengeSum =
302 sum (proof_challenge <$> prevFakeProofs) +
303 sum (proof_challenge <$> nextFakeProofs)
304 let statement = encryptionStatement voterZKP enc
305 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
306 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
307 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
308 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
309 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
310 let challenge = hash statement commitments in
311 let genuineChallenge = challenge - fakeChallengeSum in
313 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
314 -- thus (sum (proof_challenge <$> proofs) == challenge)
315 -- as checked in 'verifyEncryption'.
316 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
317 return (DisjProof proofs)
320 Reifies c FFC => Monad m =>
321 PublicKey c -> ZKP ->
322 [Disjunction c] -> (Encryption c, DisjProof c) ->
323 ExceptT ErrorVerifyEncryption m Bool
324 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
325 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
327 throwE $ ErrorVerifyEncryption_InvalidProofLength
328 (fromIntegral $ List.length proofs)
329 (fromIntegral $ List.length disjs)
331 return $ challengeSum ==
332 hash (encryptionStatement voterZKP enc) (join commitments)
334 challengeSum = sum (proof_challenge <$> proofs)
337 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
338 encryptionStatement (ZKP voterZKP) Encryption{..} =
339 "prove|"<>voterZKP<>"|"
340 <> bytesNat encryption_nonce<>","
341 <> bytesNat encryption_vault<>"|"
343 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
344 -- returns the 'Commitment's with only the knowledge of the verifier.
346 -- For the prover the 'Proof' comes from @fakeProof@,
347 -- and for the verifier the 'Proof' comes from the prover.
348 encryptionCommitments ::
350 PublicKey c -> Encryption c ->
351 Disjunction c -> Proof c -> [G c]
352 encryptionCommitments elecPubKey Encryption{..} disj proof =
353 [ commit proof groupGen encryption_nonce
354 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
355 -- base==groupGen, basePowSec==groupGen^encNonce.
356 , commit proof elecPubKey (encryption_vault*disj)
357 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
358 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
359 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
362 -- ** Type 'ErrorVerifyEncryption'
363 -- | Error raised by 'verifyEncryption'.
364 data ErrorVerifyEncryption
365 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
366 -- ^ When the number of proofs is different than
367 -- the number of 'Disjunction's.
371 data Question = Question
372 { question_text :: !Text
373 , question_choices :: ![Text]
374 , question_mini :: !Natural
375 , question_maxi :: !Natural
376 -- , question_blank :: Maybe Bool
377 } deriving (Eq,Show,Generic,NFData,ToJSON,FromJSON)
380 data Answer c = Answer
381 { answer_opinions :: ![(Encryption c, DisjProof c)]
382 -- ^ Encrypted 'Opinion' for each 'question_choices'
383 -- with a 'DisjProof' that they belong to [0,1].
384 , answer_sumProof :: !(DisjProof c)
385 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
386 -- is an element of @[mini..maxi]@.
387 -- , answer_blankProof ::
388 } deriving (Eq,Show,Generic,NFData)
389 deriving instance Reifies c FFC => ToJSON (Answer c)
390 instance Reifies c FFC => FromJSON (Answer c) where
391 parseJSON = JSON.withObject "Answer" $ \o -> do
392 answer_choices <- o .: "choices"
393 answer_individual_proofs <- o .: "individual_proofs"
394 let answer_opinions = List.zip answer_choices answer_individual_proofs
395 answer_sumProof <- o .: "overall_proof"
398 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
399 -- returns an 'Answer' validable by 'verifyAnswer',
400 -- unless an 'ErrorAnswer' is returned.
403 Monad m => RandomGen r =>
404 PublicKey c -> ZKP ->
405 Question -> [Bool] ->
406 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
407 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
408 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
410 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
411 | List.length opinions /= List.length question_choices =
413 ErrorAnswer_WrongNumberOfOpinions
414 (fromIntegral $ List.length opinions)
415 (fromIntegral $ List.length question_choices)
417 encryptions <- encrypt elecPubKey `mapM` opinions
418 individualProofs <- zipWithM
419 (\opinion -> proveEncryption elecPubKey zkp $
421 then (List.init booleanDisjunctions,[])
422 else ([],List.tail booleanDisjunctions))
423 opinionByChoice encryptions
424 sumProof <- proveEncryption elecPubKey zkp
425 (List.tail <$> List.genericSplitAt
426 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
427 (intervalDisjunctions question_mini question_maxi))
428 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
429 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
432 { answer_opinions = List.zip
433 (snd <$> encryptions) -- NOTE: drop encNonce
435 , answer_sumProof = sumProof
438 opinionsSum = sum $ nat <$> opinions
439 opinions = (\o -> if o then one else zero) <$> opinionByChoice
443 PublicKey c -> ZKP ->
444 Question -> Answer c -> Bool
445 verifyAnswer elecPubKey zkp Question{..} Answer{..}
446 | List.length question_choices /= List.length answer_opinions = False
447 | otherwise = either (const False) id $ runExcept $ do
449 verifyEncryption elecPubKey zkp booleanDisjunctions
450 `traverse` answer_opinions
451 validSum <- verifyEncryption elecPubKey zkp
452 (intervalDisjunctions question_mini question_maxi)
453 ( sum (fst <$> answer_opinions)
455 return (and validOpinions && validSum)
457 -- ** Type 'ErrorAnswer'
458 -- | Error raised by 'encryptAnswer'.
460 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
461 -- ^ When the number of opinions is different than
462 -- the number of choices ('question_choices').
463 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
464 -- ^ When the sum of opinions is not within the bounds
465 -- of 'question_mini' and 'question_maxi'.
466 deriving (Eq,Show,Generic,NFData)
469 data Election c = Election
470 { election_name :: !Text
471 , election_description :: !Text
472 , election_crypto :: !(ElectionCrypto c)
473 , election_questions :: ![Question]
474 , election_uuid :: !UUID
475 , election_hash :: !Hash
476 } deriving (Eq,Show,Generic,NFData)
478 instance ToJSON (Election c) where
479 toJSON Election{..} =
481 [ "name" .= election_name
482 , "description" .= election_description
483 , "public_key" .= election_crypto
484 , "questions" .= election_questions
485 , "uuid" .= election_uuid
487 toEncoding Election{..} =
489 ( "name" .= election_name
490 <> "description" .= election_description
491 <> "public_key" .= election_crypto
492 <> "questions" .= election_questions
493 <> "uuid" .= election_uuid
495 instance FromJSON (Election ()) where
496 parseJSON = JSON.withObject "Election" $ \o -> Election
498 <*> o .: "description"
499 <*> o .: "public_key"
502 <*> pure (hashJSON (JSON.Object o))
504 -- ** Type 'ElectionCrypto'
505 data ElectionCrypto c =
507 { electionCrypto_FFC_params :: !FFC
508 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
509 } deriving (Eq,Show,Generic,NFData)
511 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
512 reifyElection Election{..} k =
513 case election_crypto of
514 ElectionCrypto_FFC ffc (G (F pubKey)) ->
515 reify ffc $ \(_::Proxy c) -> k @c
516 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
518 instance ToJSON (ElectionCrypto c) where
519 toJSON (ElectionCrypto_FFC ffc pubKey) =
524 toEncoding (ElectionCrypto_FFC ffc pubKey) =
529 instance FromJSON (ElectionCrypto ()) where
530 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
532 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
533 return $ ElectionCrypto_FFC ffc (G (F pubKey))
537 newtype Hash = Hash Text
538 deriving (Eq,Ord,Show,Generic)
539 deriving anyclass (ToJSON,FromJSON)
540 deriving newtype NFData
542 hashJSON :: ToJSON a => a -> Hash
543 hashJSON = Hash . TL.toStrict . TL.decodeUtf8 . BSL64.encode . JSON.encode
545 hashElection :: Election c -> Election c
546 hashElection elec = elec{election_hash=hashJSON elec}
549 data Ballot c = Ballot
550 { ballot_answers :: ![Answer c]
551 , ballot_signature :: !(Maybe (Signature c))
552 , ballot_election_uuid :: !UUID
553 , ballot_election_hash :: !Hash
554 } deriving (Generic,NFData)
555 deriving instance Reifies c FFC => ToJSON (Ballot c)
556 instance Reifies c FFC => FromJSON (Ballot c) where
557 parseJSON = JSON.withObject "Ballot" $ \o -> do
558 ballot_answers <- o .: "answers"
559 ballot_signature <- o .:? "signature"
560 ballot_election_uuid <- o .: "election_uuid"
561 ballot_election_hash <- o .: "election_hash"
564 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
565 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
566 -- where 'opinionsByQuest' is a list of 'Opinion's
567 -- on each 'question_choices' of each 'election_questions'.
570 Monad m => RandomGen r =>
572 Maybe (SecretKey c) -> [[Bool]] ->
573 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
574 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
575 | List.length election_questions /= List.length opinionsByQuest =
577 ErrorBallot_WrongNumberOfAnswers
578 (fromIntegral $ List.length opinionsByQuest)
579 (fromIntegral $ List.length election_questions)
581 let (voterKeys, voterZKP) =
582 case ballotSecKeyMay of
583 Nothing -> (Nothing, ZKP "")
585 ( Just (ballotSecKey, ballotPubKey)
586 , ZKP (bytesNat ballotPubKey) )
587 where ballotPubKey = publicKey ballotSecKey
589 S.mapStateT (withExceptT ErrorBallot_Answer) $
590 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
591 election_questions opinionsByQuest
592 ballot_signature <- case voterKeys of
593 Nothing -> return Nothing
594 Just (ballotSecKey, signature_publicKey) -> do
596 prove ballotSecKey (Identity groupGen) $
597 \(Identity commitment) ->
599 -- NOTE: the order is unusual, the commitments are first
600 -- then comes the statement. Best guess is that
601 -- this is easier to code due to their respective types.
602 (signatureCommitments voterZKP commitment)
603 (signatureStatement ballot_answers)
604 return $ Just Signature{..}
607 , ballot_election_hash = election_hash
608 , ballot_election_uuid = election_uuid
612 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
613 verifyBallot Election{..} Ballot{..} =
614 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
615 ballot_election_uuid == election_uuid &&
616 ballot_election_hash == election_hash &&
617 List.length election_questions == List.length ballot_answers &&
618 let (isValidSign, zkpSign) =
619 case ballot_signature of
620 Nothing -> (True, ZKP "")
621 Just Signature{..} ->
622 let zkp = ZKP (bytesNat signature_publicKey) in
624 proof_challenge signature_proof == hash
625 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
626 (signatureStatement ballot_answers)
629 List.zipWith (verifyAnswer elecPubKey zkpSign)
630 election_questions ballot_answers
632 -- ** Type 'Signature'
633 -- | Schnorr-like signature.
635 -- Used by each voter to sign his/her encrypted 'Ballot'
636 -- using his/her 'Credential',
637 -- in order to avoid ballot stuffing.
638 data Signature c = Signature
639 { signature_publicKey :: !(PublicKey c)
640 -- ^ Verification key.
641 , signature_proof :: !(Proof c)
642 } deriving (Generic,NFData)
643 deriving instance Reifies c FFC => ToJSON (Signature c)
644 instance Reifies c FFC => FromJSON (Signature c) where
645 parseJSON = JSON.withObject "Signature" $ \o -> do
646 signature_publicKey <- o .: "public_key"
647 proof_challenge <- o .: "challenge"
648 proof_response <- o .: "response"
649 let signature_proof = Proof{..}
654 -- | @('signatureStatement' answers)@
655 -- returns the encrypted material to be signed:
656 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
657 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
659 foldMap $ \Answer{..} ->
660 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
661 [encryption_nonce, encryption_vault]
663 -- | @('signatureCommitments' voterZKP commitment)@
664 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
665 signatureCommitments (ZKP voterZKP) commitment =
666 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
667 <> bytesNat commitment<>"|"
669 -- ** Type 'ErrorBallot'
670 -- | Error raised by 'encryptBallot'.
672 = ErrorBallot_WrongNumberOfAnswers Natural Natural
673 -- ^ When the number of answers
674 -- is different than the number of questions.
675 | ErrorBallot_Answer ErrorAnswer
676 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
678 -- ^ TODO: to be more precise.
679 deriving (Eq,Show,Generic,NFData)