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 deriving instance Reifies c FFC => FromJSON (Encryption c)
68 -- | Additive homomorphism.
69 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
70 instance Reifies c FFC => Additive (Encryption c) where
71 zero = Encryption one one
73 (encryption_nonce x * encryption_nonce y)
74 (encryption_vault x * encryption_vault y)
76 -- *** Type 'EncryptionNonce'
77 type EncryptionNonce = E
79 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
81 -- WARNING: the secret encryption nonce (@encNonce@)
82 -- is returned alongside the 'Encryption'
83 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
84 -- but this secret @encNonce@ MUST be forgotten after that,
85 -- as it may be used to decipher the 'Encryption'
86 -- without the 'SecretKey' associated with 'pubKey'.
89 Monad m => RandomGen r =>
91 S.StateT r m (EncryptionNonce c, Encryption c)
92 encrypt pubKey clear = do
94 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
97 { encryption_nonce = groupGen^encNonce
98 , encryption_vault = pubKey ^encNonce * groupGen^clear
102 -- | Non-Interactive Zero-Knowledge 'Proof'
103 -- of knowledge of a discrete logarithm:
104 -- @(secret == logBase base (base^secret))@.
106 { proof_challenge :: Challenge c
107 -- ^ 'Challenge' sent by the verifier to the prover
108 -- to ensure that the prover really has knowledge
109 -- of the secret and is not replaying.
110 -- Actually, 'proof_challenge' is not sent to the prover,
111 -- but derived from the prover's 'Commitment's and statements
112 -- with a collision resistant 'hash'.
113 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
114 , proof_response :: E c
115 -- ^ A discrete logarithm sent by the prover to the verifier,
116 -- as a response to 'proof_challenge'.
118 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
120 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
121 -- * @commitment '==' 'commit' proof base basePowSec '=='
122 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
123 -- * and @basePowSec '==' base'^'sec@,
125 -- then, with overwhelming probability (due to the 'hash' function),
126 -- the prover was not able to choose 'proof_challenge'
127 -- yet was able to compute a 'proof_response' such that
128 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
129 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
130 -- therefore the prover knows 'sec'.
132 -- The prover choses 'commitment' to be a random power of @base@,
133 -- to ensure that each 'prove' does not reveal any information
135 } deriving (Eq,Show,Generic,NFData)
136 deriving instance Reifies c FFC => ToJSON (Proof c)
137 deriving instance Reifies c FFC => FromJSON (Proof c)
140 -- | Zero-knowledge proof.
142 -- A protocol is /zero-knowledge/ if the verifier
143 -- learns nothing from the protocol except that the prover
146 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
147 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
148 newtype ZKP = ZKP BS.ByteString
150 -- ** Type 'Challenge'
154 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
155 -- by 'hash'ing them (eventually with other 'Commitment's).
157 -- Used in 'prove' it enables a Fiat-Shamir transformation
158 -- of an /interactive zero-knowledge/ (IZK) proof
159 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
160 -- That is to say that the verifier does not have
161 -- to send a 'Challenge' to the prover.
162 -- Indeed, the prover now handles the 'Challenge'
163 -- which becomes a (collision resistant) 'hash'
164 -- of the prover's commitments (and statements to be a stronger proof).
165 type Oracle list c = list (Commitment c) -> Challenge c
167 -- | @('prove' sec commitmentBases oracle)@
168 -- returns a 'Proof' that @sec@ is known
169 -- (by proving the knowledge of its discrete logarithm).
171 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
172 -- raised to the power of the secret nonce of the 'Proof',
173 -- as those are the 'Commitment's that the verifier will obtain
174 -- when composing the 'proof_challenge' and 'proof_response' together
177 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
178 -- the statement must be included in the 'hash' (along with the commitments).
180 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
181 -- does not reveal any information regarding the secret @sec@,
182 -- because two 'Proof's using the same 'Commitment'
183 -- can be used to deduce @sec@ (using the special-soundness).
186 Monad m => RandomGen r => Functor list =>
187 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
188 prove sec commitmentBases oracle = do
190 let commitments = (^ nonce) <$> commitmentBases
191 let proof_challenge = oracle commitments
194 , proof_response = nonce - sec*proof_challenge
197 -- | @('fakeProof')@ returns a 'Proof'
198 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
199 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
200 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
201 -- as a 'Proof' returned by 'prove'.
203 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
204 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
208 RandomGen r => S.StateT r m (Proof c)
210 proof_challenge <- random
211 proof_response <- random
214 -- ** Type 'Commitment'
215 -- | A commitment from the prover to the verifier.
216 -- It's a power of 'groupGen' chosen randomly by the prover
217 -- when making a 'Proof' with 'prove'.
220 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
221 -- from the given 'Proof' with the knowledge of the verifier.
222 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
223 commit Proof{..} base basePowSec =
224 base^proof_response *
225 basePowSec^proof_challenge
226 -- NOTE: Contrary to some textbook presentations,
227 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
228 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
229 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
230 {-# INLINE commit #-}
232 -- * Type 'Disjunction'
233 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
234 -- it's used in 'proveEncryption' to generate a 'Proof'
235 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
238 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
239 booleanDisjunctions = List.take 2 groupGenInverses
241 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
242 intervalDisjunctions mini maxi =
243 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
244 List.genericDrop (nat mini) $
248 -- | Index of a 'Disjunction' within a list of them.
249 -- It is encrypted as an 'E'xponent by 'encrypt'.
252 -- ** Type 'DisjProof'
253 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
254 -- is indexing a 'Disjunction' within a list of them,
255 -- without revealing which 'Opinion' it is.
256 newtype DisjProof c = DisjProof [Proof c]
257 deriving (Eq,Show,Generic)
258 deriving newtype NFData
259 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
260 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
262 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
263 -- returns a 'DisjProof' that 'enc' 'encrypt's
264 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
266 -- The prover proves that it knows an 'encNonce', such that:
267 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
269 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
271 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
274 Monad m => RandomGen r =>
275 PublicKey c -> ZKP ->
276 ([Disjunction c],[Disjunction c]) ->
277 (EncryptionNonce c, Encryption c) ->
278 S.StateT r m (DisjProof c)
279 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
280 -- Fake proofs for all 'Disjunction's except the genuine one.
281 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
282 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
283 let fakeChallengeSum =
284 sum (proof_challenge <$> prevFakeProofs) +
285 sum (proof_challenge <$> nextFakeProofs)
286 let statement = encryptionStatement voterZKP enc
287 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
288 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
289 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
290 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
291 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
292 let challenge = hash statement commitments in
293 let genuineChallenge = challenge - fakeChallengeSum in
295 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
296 -- thus (sum (proof_challenge <$> proofs) == challenge)
297 -- as checked in 'verifyEncryption'.
298 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
299 return (DisjProof proofs)
302 Reifies c FFC => Monad m =>
303 PublicKey c -> ZKP ->
304 [Disjunction c] -> (Encryption c, DisjProof c) ->
305 ExceptT ErrorVerifyEncryption m Bool
306 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
307 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
309 throwE $ ErrorVerifyEncryption_InvalidProofLength
310 (fromIntegral $ List.length proofs)
311 (fromIntegral $ List.length disjs)
313 return $ challengeSum ==
314 hash (encryptionStatement voterZKP enc) (join commitments)
316 challengeSum = sum (proof_challenge <$> proofs)
319 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
320 encryptionStatement (ZKP voterZKP) Encryption{..} =
321 "prove|"<>voterZKP<>"|"
322 <> bytesNat encryption_nonce<>","
323 <> bytesNat encryption_vault<>"|"
325 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
326 -- returns the 'Commitment's with only the knowledge of the verifier.
328 -- For the prover the 'Proof' comes from @fakeProof@,
329 -- and for the verifier the 'Proof' comes from the prover.
330 encryptionCommitments ::
332 PublicKey c -> Encryption c ->
333 Disjunction c -> Proof c -> [G c]
334 encryptionCommitments elecPubKey Encryption{..} disj proof =
335 [ commit proof groupGen encryption_nonce
336 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
337 -- base==groupGen, basePowSec==groupGen^encNonce.
338 , commit proof elecPubKey (encryption_vault*disj)
339 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
340 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
341 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
344 -- ** Type 'ErrorVerifyEncryption'
345 -- | Error raised by 'verifyEncryption'.
346 data ErrorVerifyEncryption
347 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
348 -- ^ When the number of proofs is different than
349 -- the number of 'Disjunction's.
353 data Question = Question
354 { question_text :: !Text
355 , question_choices :: ![Text]
356 , question_mini :: !Natural
357 , question_maxi :: !Natural
358 -- , question_blank :: Maybe Bool
359 } deriving (Eq,Show,Generic,NFData,ToJSON,FromJSON)
362 data Answer c = Answer
363 { answer_opinions :: ![(Encryption c, DisjProof c)]
364 -- ^ Encrypted 'Opinion' for each 'question_choices'
365 -- with a 'DisjProof' that they belong to [0,1].
366 , answer_sumProof :: !(DisjProof c)
367 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
368 -- is an element of @[mini..maxi]@.
369 -- , answer_blankProof ::
370 } deriving (Eq,Show,Generic,NFData)
371 deriving instance Reifies c FFC => ToJSON (Answer c)
372 deriving instance Reifies c FFC => FromJSON (Answer c)
374 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
375 -- returns an 'Answer' validable by 'verifyAnswer',
376 -- unless an 'ErrorAnswer' is returned.
379 Monad m => RandomGen r =>
380 PublicKey c -> ZKP ->
381 Question -> [Bool] ->
382 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
383 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
384 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
386 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
387 | List.length opinions /= List.length question_choices =
389 ErrorAnswer_WrongNumberOfOpinions
390 (fromIntegral $ List.length opinions)
391 (fromIntegral $ List.length question_choices)
393 encryptions <- encrypt elecPubKey `mapM` opinions
394 individualProofs <- zipWithM
395 (\opinion -> proveEncryption elecPubKey zkp $
397 then (List.init booleanDisjunctions,[])
398 else ([],List.tail booleanDisjunctions))
399 opinionByChoice encryptions
400 sumProof <- proveEncryption elecPubKey zkp
401 (List.tail <$> List.genericSplitAt
402 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
403 (intervalDisjunctions question_mini question_maxi))
404 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
405 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
408 { answer_opinions = List.zip
409 (snd <$> encryptions) -- NOTE: drop encNonce
411 , answer_sumProof = sumProof
414 opinionsSum = sum $ nat <$> opinions
415 opinions = (\o -> if o then one else zero) <$> opinionByChoice
419 PublicKey c -> ZKP ->
420 Question -> Answer c -> Bool
421 verifyAnswer elecPubKey zkp Question{..} Answer{..}
422 | List.length question_choices /= List.length answer_opinions = False
423 | otherwise = either (const False) id $ runExcept $ do
425 verifyEncryption elecPubKey zkp booleanDisjunctions
426 `traverse` answer_opinions
427 validSum <- verifyEncryption elecPubKey zkp
428 (intervalDisjunctions question_mini question_maxi)
429 ( sum (fst <$> answer_opinions)
431 return (and validOpinions && validSum)
433 -- ** Type 'ErrorAnswer'
434 -- | Error raised by 'encryptAnswer'.
436 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
437 -- ^ When the number of opinions is different than
438 -- the number of choices ('question_choices').
439 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
440 -- ^ When the sum of opinions is not within the bounds
441 -- of 'question_mini' and 'question_maxi'.
442 deriving (Eq,Show,Generic,NFData)
445 data Election c = Election
446 { election_name :: !Text
447 , election_description :: !Text
448 , election_crypto :: !(ElectionCrypto c)
449 , election_questions :: ![Question]
450 , election_uuid :: !UUID
451 , election_hash :: !Hash
452 } deriving (Eq,Show,Generic,NFData)
454 instance ToJSON (Election c) where
455 toJSON Election{..} =
457 [ "name" .= election_name
458 , "description" .= election_description
459 , "public_key" .= election_crypto
460 , "questions" .= election_questions
461 , "uuid" .= election_uuid
463 toEncoding Election{..} =
465 ( "name" .= election_name
466 <> "description" .= election_description
467 <> "public_key" .= election_crypto
468 <> "questions" .= election_questions
469 <> "uuid" .= election_uuid
471 instance FromJSON (Election c) where
472 parseJSON = JSON.withObject "Election" $ \o -> Election
474 <*> o .: "description"
475 <*> o .: "public_key"
478 <*> pure (hashJSON (JSON.Object o))
480 -- ** Type 'ElectionCrypto'
481 data ElectionCrypto c =
483 { electionCrypto_FFC_params :: !FFC
484 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
485 } deriving (Eq,Show,Generic,NFData)
487 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
488 reifyElection Election{..} k =
489 case election_crypto of
490 ElectionCrypto_FFC ffc (G (F pubKey)) ->
491 reify ffc $ \(_::Proxy c) -> k @c
492 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
495 instance ToJSON (ElectionCrypto c) where
496 toJSON (ElectionCrypto_FFC ffc pubKey) =
501 toEncoding (ElectionCrypto_FFC ffc pubKey) =
506 instance FromJSON (ElectionCrypto c) where
507 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
509 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
511 unless (nat ffc_groupGen < ffc_fieldCharac) $
512 JSON.typeMismatch "FFC: groupGen is not lower than fieldCharac" (JSON.Object o)
514 return $ ElectionCrypto_FFC ffc (G (F pubKey))
518 newtype Hash = Hash Text
519 deriving (Eq,Ord,Show,Generic)
520 deriving anyclass (ToJSON,FromJSON)
521 deriving newtype NFData
523 hashJSON :: ToJSON a => a -> Hash
524 hashJSON = Hash . TL.toStrict . TL.decodeUtf8 . BSL64.encode . JSON.encode
526 hashElection :: Election c -> Election c
527 hashElection elec = elec{election_hash=hashJSON elec}
530 data Ballot c = Ballot
531 { ballot_answers :: ![Answer c]
532 , ballot_signature :: !(Maybe (Signature c))
533 , ballot_election_uuid :: !UUID
534 , ballot_election_hash :: !Hash
535 } deriving (Generic,NFData)
536 deriving instance Reifies c FFC => ToJSON (Ballot c)
537 deriving instance Reifies c FFC => FromJSON (Ballot c)
539 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
540 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
541 -- where 'opinionsByQuest' is a list of 'Opinion's
542 -- on each 'question_choices' of each 'election_questions'.
545 Monad m => RandomGen r =>
547 Maybe (SecretKey c) -> [[Bool]] ->
548 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
549 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
550 | List.length election_questions /= List.length opinionsByQuest =
552 ErrorBallot_WrongNumberOfAnswers
553 (fromIntegral $ List.length opinionsByQuest)
554 (fromIntegral $ List.length election_questions)
556 let (voterKeys, voterZKP) =
557 case ballotSecKeyMay of
558 Nothing -> (Nothing, ZKP "")
560 ( Just (ballotSecKey, ballotPubKey)
561 , ZKP (bytesNat ballotPubKey) )
562 where ballotPubKey = publicKey ballotSecKey
564 S.mapStateT (withExceptT ErrorBallot_Answer) $
565 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
566 election_questions opinionsByQuest
567 ballot_signature <- case voterKeys of
568 Nothing -> return Nothing
569 Just (ballotSecKey, signature_publicKey) -> do
571 prove ballotSecKey (Identity groupGen) $
572 \(Identity commitment) ->
574 -- NOTE: the order is unusual, the commitments are first
575 -- then comes the statement. Best guess is that
576 -- this is easier to code due to their respective types.
577 (signatureCommitments voterZKP commitment)
578 (signatureStatement ballot_answers)
579 return $ Just Signature{..}
582 , ballot_election_hash = election_hash
583 , ballot_election_uuid = election_uuid
587 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
588 verifyBallot Election{..} Ballot{..} =
589 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
590 ballot_election_uuid == election_uuid &&
591 ballot_election_hash == election_hash &&
592 List.length election_questions == List.length ballot_answers &&
593 let (isValidSign, zkpSign) =
594 case ballot_signature of
595 Nothing -> (True, ZKP "")
596 Just Signature{..} ->
597 let zkp = ZKP (bytesNat signature_publicKey) in
599 proof_challenge signature_proof == hash
600 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
601 (signatureStatement ballot_answers)
604 List.zipWith (verifyAnswer elecPubKey zkpSign)
605 election_questions ballot_answers
607 -- ** Type 'Signature'
608 -- | Schnorr-like signature.
610 -- Used by each voter to sign his/her encrypted 'Ballot'
611 -- using his/her 'Credential',
612 -- in order to avoid ballot stuffing.
613 data Signature c = Signature
614 { signature_publicKey :: !(PublicKey c)
615 -- ^ Verification key.
616 , signature_proof :: !(Proof c)
617 } deriving (Generic,NFData)
618 deriving instance Reifies c FFC => ToJSON (Signature c)
619 deriving instance Reifies c FFC => FromJSON (Signature c)
623 -- | @('signatureStatement' answers)@
624 -- returns the encrypted material to be signed:
625 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
626 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
628 foldMap $ \Answer{..} ->
629 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
630 [encryption_nonce, encryption_vault]
632 -- | @('signatureCommitments' voterZKP commitment)@
633 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
634 signatureCommitments (ZKP voterZKP) commitment =
635 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
636 <> bytesNat commitment<>"|"
638 -- ** Type 'ErrorBallot'
639 -- | Error raised by 'encryptBallot'.
641 = ErrorBallot_WrongNumberOfAnswers Natural Natural
642 -- ^ When the number of answers
643 -- is different than the number of questions.
644 | ErrorBallot_Answer ErrorAnswer
645 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
647 -- ^ TODO: to be more precise.
648 deriving (Eq,Show,Generic,NFData)