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 c) 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 c) where
530 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
532 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
534 unless (nat ffc_groupGen < ffc_fieldCharac) $
535 JSON.typeMismatch "FFC: groupGen is not lower than fieldCharac" (JSON.Object o)
537 return $ ElectionCrypto_FFC ffc (G (F pubKey))
541 newtype Hash = Hash Text
542 deriving (Eq,Ord,Show,Generic)
543 deriving anyclass (ToJSON,FromJSON)
544 deriving newtype NFData
546 hashJSON :: ToJSON a => a -> Hash
547 hashJSON = Hash . TL.toStrict . TL.decodeUtf8 . BSL64.encode . JSON.encode
549 hashElection :: Election c -> Election c
550 hashElection elec = elec{election_hash=hashJSON elec}
553 data Ballot c = Ballot
554 { ballot_answers :: ![Answer c]
555 , ballot_signature :: !(Maybe (Signature c))
556 , ballot_election_uuid :: !UUID
557 , ballot_election_hash :: !Hash
558 } deriving (Generic,NFData)
559 deriving instance Reifies c FFC => ToJSON (Ballot c)
560 instance Reifies c FFC => FromJSON (Ballot c) where
561 parseJSON = JSON.withObject "Ballot" $ \o -> do
562 ballot_answers <- o .: "answers"
563 ballot_signature <- o .:? "signature"
564 ballot_election_uuid <- o .: "election_uuid"
565 ballot_election_hash <- o .: "election_hash"
568 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
569 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
570 -- where 'opinionsByQuest' is a list of 'Opinion's
571 -- on each 'question_choices' of each 'election_questions'.
574 Monad m => RandomGen r =>
576 Maybe (SecretKey c) -> [[Bool]] ->
577 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
578 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
579 | List.length election_questions /= List.length opinionsByQuest =
581 ErrorBallot_WrongNumberOfAnswers
582 (fromIntegral $ List.length opinionsByQuest)
583 (fromIntegral $ List.length election_questions)
585 let (voterKeys, voterZKP) =
586 case ballotSecKeyMay of
587 Nothing -> (Nothing, ZKP "")
589 ( Just (ballotSecKey, ballotPubKey)
590 , ZKP (bytesNat ballotPubKey) )
591 where ballotPubKey = publicKey ballotSecKey
593 S.mapStateT (withExceptT ErrorBallot_Answer) $
594 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
595 election_questions opinionsByQuest
596 ballot_signature <- case voterKeys of
597 Nothing -> return Nothing
598 Just (ballotSecKey, signature_publicKey) -> do
600 prove ballotSecKey (Identity groupGen) $
601 \(Identity commitment) ->
603 -- NOTE: the order is unusual, the commitments are first
604 -- then comes the statement. Best guess is that
605 -- this is easier to code due to their respective types.
606 (signatureCommitments voterZKP commitment)
607 (signatureStatement ballot_answers)
608 return $ Just Signature{..}
611 , ballot_election_hash = election_hash
612 , ballot_election_uuid = election_uuid
616 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
617 verifyBallot Election{..} Ballot{..} =
618 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
619 ballot_election_uuid == election_uuid &&
620 ballot_election_hash == election_hash &&
621 List.length election_questions == List.length ballot_answers &&
622 let (isValidSign, zkpSign) =
623 case ballot_signature of
624 Nothing -> (True, ZKP "")
625 Just Signature{..} ->
626 let zkp = ZKP (bytesNat signature_publicKey) in
628 proof_challenge signature_proof == hash
629 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
630 (signatureStatement ballot_answers)
633 List.zipWith (verifyAnswer elecPubKey zkpSign)
634 election_questions ballot_answers
636 -- ** Type 'Signature'
637 -- | Schnorr-like signature.
639 -- Used by each voter to sign his/her encrypted 'Ballot'
640 -- using his/her 'Credential',
641 -- in order to avoid ballot stuffing.
642 data Signature c = Signature
643 { signature_publicKey :: !(PublicKey c)
644 -- ^ Verification key.
645 , signature_proof :: !(Proof c)
646 } deriving (Generic,NFData)
647 deriving instance Reifies c FFC => ToJSON (Signature c)
648 instance Reifies c FFC => FromJSON (Signature c) where
649 parseJSON = JSON.withObject "Signature" $ \o -> do
650 signature_publicKey <- o .: "public_key"
651 proof_challenge <- o .: "challenge"
652 proof_response <- o .: "response"
653 let signature_proof = Proof{..}
658 -- | @('signatureStatement' answers)@
659 -- returns the encrypted material to be signed:
660 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
661 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
663 foldMap $ \Answer{..} ->
664 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
665 [encryption_nonce, encryption_vault]
667 -- | @('signatureCommitments' voterZKP commitment)@
668 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
669 signatureCommitments (ZKP voterZKP) commitment =
670 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
671 <> bytesNat commitment<>"|"
673 -- ** Type 'ErrorBallot'
674 -- | Error raised by 'encryptBallot'.
676 = ErrorBallot_WrongNumberOfAnswers Natural Natural
677 -- ^ When the number of answers
678 -- is different than the number of questions.
679 | ErrorBallot_Answer ErrorAnswer
680 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
682 -- ^ TODO: to be more precise.
683 deriving (Eq,Show,Generic,NFData)