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
223 -- TODO: switch (+) to (-) when 'commit' will switch (/) to (*).
226 -- | @('fakeProof')@ returns a 'Proof'
227 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
228 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
229 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
230 -- as a 'Proof' returned by 'prove'.
232 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
233 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
237 RandomGen r => S.StateT r m (Proof c)
239 proof_challenge <- random
240 proof_response <- random
243 -- ** Type 'Commitment'
244 -- | A commitment from the prover to the verifier.
245 -- It's a power of 'groupGen' chosen randomly by the prover
246 -- when making a 'Proof' with 'prove'.
249 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
250 -- from the given 'Proof' with the knowledge of the verifier.
251 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
252 commit Proof{..} base basePowSec =
253 base^proof_response /
254 basePowSec^proof_challenge
255 -- TODO: contrary to some textbook presentations,
256 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
257 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
258 -- this would be compensated by using @('-')@ instead of @('+')@ in 'prove'.
259 {-# INLINE commit #-}
261 -- * Type 'Disjunction'
262 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
263 -- it's used in 'proveEncryption' to generate a 'Proof'
264 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
267 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
268 booleanDisjunctions = List.take 2 groupGenInverses
270 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
271 intervalDisjunctions mini maxi =
272 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
273 List.genericDrop (nat mini) $
277 -- | Index of a 'Disjunction' within a list of them.
278 -- It is encrypted as an 'E'xponent by 'encrypt'.
281 -- ** Type 'DisjProof'
282 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
283 -- is indexing a 'Disjunction' within a list of them,
284 -- without revealing which 'Opinion' it is.
285 newtype DisjProof c = DisjProof [Proof c]
286 deriving (Eq,Show,Generic)
287 deriving newtype NFData
288 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
289 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
291 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
292 -- returns a 'DisjProof' that 'enc' 'encrypt's
293 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
295 -- The prover proves that it knows an 'encNonce', such that:
296 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
298 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
300 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
303 Monad m => RandomGen r =>
304 PublicKey c -> ZKP ->
305 ([Disjunction c],[Disjunction c]) ->
306 (EncryptionNonce c, Encryption c) ->
307 S.StateT r m (DisjProof c)
308 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
309 -- Fake proofs for all 'Disjunction's except the genuine one.
310 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
311 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
312 let fakeChallengeSum =
313 sum (proof_challenge <$> prevFakeProofs) +
314 sum (proof_challenge <$> nextFakeProofs)
315 let statement = encryptionStatement voterZKP enc
316 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
317 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
318 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
319 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
320 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
321 let challenge = hash statement commitments in
322 let genuineChallenge = challenge - fakeChallengeSum in
324 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
325 -- thus (sum (proof_challenge <$> proofs) == challenge)
326 -- as checked in 'verifyEncryption'.
327 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
328 return (DisjProof proofs)
331 Reifies c FFC => Monad m =>
332 PublicKey c -> ZKP ->
333 [Disjunction c] -> (Encryption c, DisjProof c) ->
334 ExceptT ErrorVerifyEncryption m Bool
335 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
336 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
338 throwE $ ErrorVerifyEncryption_InvalidProofLength
339 (fromIntegral $ List.length proofs)
340 (fromIntegral $ List.length disjs)
342 return $ challengeSum ==
343 hash (encryptionStatement voterZKP enc) (join commitments)
345 challengeSum = sum (proof_challenge <$> proofs)
348 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
349 encryptionStatement (ZKP voterZKP) Encryption{..} =
350 "prove|"<>voterZKP<>"|"
351 <> bytesNat encryption_nonce<>","
352 <> bytesNat encryption_vault<>"|"
354 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
355 -- returns the 'Commitment's with only the knowledge of the verifier.
357 -- For the prover the 'Proof' comes from @fakeProof@,
358 -- and for the verifier the 'Proof' comes from the prover.
359 encryptionCommitments ::
361 PublicKey c -> Encryption c ->
362 Disjunction c -> Proof c -> [G c]
363 encryptionCommitments elecPubKey Encryption{..} disj proof =
364 [ commit proof groupGen encryption_nonce
365 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
366 -- base==groupGen, basePowSec==groupGen^encNonce.
367 , commit proof elecPubKey (encryption_vault*disj)
368 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
369 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
370 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
373 -- ** Type 'ErrorVerifyEncryption'
374 -- | Error raised by 'verifyEncryption'.
375 data ErrorVerifyEncryption
376 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
377 -- ^ When the number of proofs is different than
378 -- the number of 'Disjunction's.
382 data Question = Question
383 { question_text :: !Text
384 , question_choices :: ![Text]
385 , question_mini :: !Natural
386 , question_maxi :: !Natural
387 -- , question_blank :: Maybe Bool
388 } deriving (Eq,Show,Generic,NFData)
389 instance ToJSON Question where
390 toJSON Question{..} =
392 [ "question" .= question_text
393 , "answers" .= question_choices
394 , "min" .= question_mini
395 , "max" .= question_maxi
397 toEncoding Question{..} =
399 ( "question" .= question_text
400 <> "answers" .= question_choices
401 <> "min" .= question_mini
402 <> "max" .= question_maxi
404 instance FromJSON Question where
405 parseJSON = JSON.withObject "Question" $ \o -> do
406 question_text <- o .: "question"
407 question_choices <- o .: "answers"
408 question_mini <- o .: "min"
409 question_maxi <- o .: "max"
413 data Answer c = Answer
414 { answer_opinions :: ![(Encryption c, DisjProof c)]
415 -- ^ Encrypted 'Opinion' for each 'question_choices'
416 -- with a 'DisjProof' that they belong to [0,1].
417 , answer_sumProof :: !(DisjProof c)
418 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
419 -- is an element of @[mini..maxi]@.
420 -- , answer_blankProof ::
421 } deriving (Eq,Show,Generic,NFData)
422 instance Reifies c FFC => ToJSON (Answer c) where
424 let (answer_choices, answer_individual_proofs) =
425 List.unzip answer_opinions in
427 [ "choices" .= answer_choices
428 , "individual_proofs" .= answer_individual_proofs
429 , "overall_proof" .= answer_sumProof
431 toEncoding Answer{..} =
432 let (answer_choices, answer_individual_proofs) =
433 List.unzip answer_opinions in
435 ( "choices" .= answer_choices
436 <> "individual_proofs" .= answer_individual_proofs
437 <> "overall_proof" .= answer_sumProof
439 instance Reifies c FFC => FromJSON (Answer c) where
440 parseJSON = JSON.withObject "Answer" $ \o -> do
441 answer_choices <- o .: "choices"
442 answer_individual_proofs <- o .: "individual_proofs"
443 let answer_opinions = List.zip answer_choices answer_individual_proofs
444 answer_sumProof <- o .: "overall_proof"
447 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
448 -- returns an 'Answer' validable by 'verifyAnswer',
449 -- unless an 'ErrorAnswer' is returned.
452 Monad m => RandomGen r =>
453 PublicKey c -> ZKP ->
454 Question -> [Bool] ->
455 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
456 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
457 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
459 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
460 | List.length opinions /= List.length question_choices =
462 ErrorAnswer_WrongNumberOfOpinions
463 (fromIntegral $ List.length opinions)
464 (fromIntegral $ List.length question_choices)
466 encryptions <- encrypt elecPubKey `mapM` opinions
467 individualProofs <- zipWithM
468 (\opinion -> proveEncryption elecPubKey zkp $
470 then (List.init booleanDisjunctions,[])
471 else ([],List.tail booleanDisjunctions))
472 opinionByChoice encryptions
473 sumProof <- proveEncryption elecPubKey zkp
474 (List.tail <$> List.genericSplitAt
475 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
476 (intervalDisjunctions question_mini question_maxi))
477 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
478 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
481 { answer_opinions = List.zip
482 (snd <$> encryptions) -- NOTE: drop encNonce
484 , answer_sumProof = sumProof
487 opinionsSum = sum $ nat <$> opinions
488 opinions = (\o -> if o then one else zero) <$> opinionByChoice
492 PublicKey c -> ZKP ->
493 Question -> Answer c -> Bool
494 verifyAnswer elecPubKey zkp Question{..} Answer{..}
495 | List.length question_choices /= List.length answer_opinions = False
496 | otherwise = either (const False) id $ runExcept $ do
498 verifyEncryption elecPubKey zkp booleanDisjunctions
499 `traverse` answer_opinions
500 validSum <- verifyEncryption elecPubKey zkp
501 (intervalDisjunctions question_mini question_maxi)
502 ( sum (fst <$> answer_opinions)
504 return (and validOpinions && validSum)
506 -- ** Type 'ErrorAnswer'
507 -- | Error raised by 'encryptAnswer'.
509 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
510 -- ^ When the number of opinions is different than
511 -- the number of choices ('question_choices').
512 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
513 -- ^ When the sum of opinions is not within the bounds
514 -- of 'question_mini' and 'question_maxi'.
515 deriving (Eq,Show,Generic,NFData)
518 data Election c = Election
519 { election_name :: !Text
520 , election_description :: !Text
521 , election_crypto :: !(ElectionCrypto c)
522 , election_questions :: ![Question]
523 , election_uuid :: !UUID
524 , election_hash :: !Hash
525 } deriving (Eq,Show,Generic,NFData)
527 instance ToJSON (Election c) where
528 toJSON Election{..} =
530 [ "name" .= election_name
531 , "description" .= election_description
532 , "public_key" .= election_crypto
533 , "questions" .= election_questions
534 , "uuid" .= election_uuid
536 toEncoding Election{..} =
538 ( "name" .= election_name
539 <> "description" .= election_description
540 <> "public_key" .= election_crypto
541 <> "questions" .= election_questions
542 <> "uuid" .= election_uuid
544 instance FromJSON (Election ()) where
545 parseJSON = JSON.withObject "Election" $ \o -> Election
547 <*> o .: "description"
548 <*> o .: "public_key"
551 <*> pure (hashJSON (JSON.Object o))
553 -- ** Type 'ElectionCrypto'
554 data ElectionCrypto c =
556 { electionCrypto_FFC_params :: !FFC
557 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
558 } deriving (Eq,Show,Generic,NFData)
560 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
561 reifyElection Election{..} k =
562 case election_crypto of
563 ElectionCrypto_FFC ffc (G (F pubKey)) ->
564 reify ffc $ \(_::Proxy c) -> k @c
565 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
567 instance ToJSON (ElectionCrypto c) where
568 toJSON (ElectionCrypto_FFC ffc pubKey) =
573 toEncoding (ElectionCrypto_FFC ffc pubKey) =
578 instance FromJSON (ElectionCrypto ()) where
579 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
581 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
582 return $ ElectionCrypto_FFC ffc (G (F pubKey))
586 newtype Hash = Hash Text
587 deriving (Eq,Ord,Show,Generic)
588 deriving anyclass (ToJSON,FromJSON)
589 deriving newtype NFData
591 hashJSON :: ToJSON a => a -> Hash
592 hashJSON = Hash . hexHash . BSL.toStrict . BSL64.encode . JSON.encode
594 hashElection :: Election c -> Election c
595 hashElection elec = elec{election_hash=hashJSON elec}
598 data Ballot c = Ballot
599 { ballot_answers :: ![Answer c]
600 , ballot_signature :: !(Maybe (Signature c))
601 , ballot_election_uuid :: !UUID
602 , ballot_election_hash :: !Hash
603 } deriving (Generic,NFData)
604 instance Reifies c FFC => ToJSON (Ballot c) where
607 [ "answers" .= ballot_answers
608 , "election_uuid" .= ballot_election_uuid
609 , "election_hash" .= ballot_election_hash
611 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
612 toEncoding Ballot{..} =
614 ( "answers" .= ballot_answers
615 <> "election_uuid" .= ballot_election_uuid
616 <> "election_hash" .= ballot_election_hash
618 maybe mempty (\sig -> "signature" .= sig) ballot_signature
619 instance Reifies c FFC => FromJSON (Ballot c) where
620 parseJSON = JSON.withObject "Ballot" $ \o -> do
621 ballot_answers <- o .: "answers"
622 ballot_signature <- o .:? "signature"
623 ballot_election_uuid <- o .: "election_uuid"
624 ballot_election_hash <- o .: "election_hash"
627 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
628 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
629 -- where 'opinionsByQuest' is a list of 'Opinion's
630 -- on each 'question_choices' of each 'election_questions'.
633 Monad m => RandomGen r =>
635 Maybe (SecretKey c) -> [[Bool]] ->
636 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
637 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
638 | List.length election_questions /= List.length opinionsByQuest =
640 ErrorBallot_WrongNumberOfAnswers
641 (fromIntegral $ List.length opinionsByQuest)
642 (fromIntegral $ List.length election_questions)
644 let (voterKeys, voterZKP) =
645 case ballotSecKeyMay of
646 Nothing -> (Nothing, ZKP "")
648 ( Just (ballotSecKey, ballotPubKey)
649 , ZKP (bytesNat ballotPubKey) )
650 where ballotPubKey = publicKey ballotSecKey
652 S.mapStateT (withExceptT ErrorBallot_Answer) $
653 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
654 election_questions opinionsByQuest
655 ballot_signature <- case voterKeys of
656 Nothing -> return Nothing
657 Just (ballotSecKey, signature_publicKey) -> do
659 prove ballotSecKey (Identity groupGen) $
660 \(Identity commitment) ->
662 -- NOTE: the order is unusual, the commitments are first
663 -- then comes the statement. Best guess is that
664 -- this is easier to code due to their respective types.
665 (signatureCommitments voterZKP commitment)
666 (signatureStatement ballot_answers)
667 return $ Just Signature{..}
670 , ballot_election_hash = election_hash
671 , ballot_election_uuid = election_uuid
675 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
676 verifyBallot Election{..} Ballot{..} =
677 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
678 ballot_election_uuid == election_uuid &&
679 ballot_election_hash == election_hash &&
680 List.length election_questions == List.length ballot_answers &&
681 let (isValidSign, zkpSign) =
682 case ballot_signature of
683 Nothing -> (True, ZKP "")
684 Just Signature{..} ->
685 let zkp = ZKP (bytesNat signature_publicKey) in
687 proof_challenge signature_proof == hash
688 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
689 (signatureStatement ballot_answers)
692 List.zipWith (verifyAnswer elecPubKey zkpSign)
693 election_questions ballot_answers
695 -- ** Type 'Signature'
696 -- | Schnorr-like signature.
698 -- Used by each voter to sign his/her encrypted 'Ballot'
699 -- using his/her 'Credential',
700 -- in order to avoid ballot stuffing.
701 data Signature c = Signature
702 { signature_publicKey :: !(PublicKey c)
703 -- ^ Verification key.
704 , signature_proof :: !(Proof c)
705 } deriving (Generic,NFData)
706 instance Reifies c FFC => ToJSON (Signature c) where
707 toJSON (Signature pubKey Proof{..}) =
709 [ "public_key" .= pubKey
710 , "challenge" .= proof_challenge
711 , "response" .= proof_response
713 toEncoding (Signature pubKey Proof{..}) =
715 ( "public_key" .= pubKey
716 <> "challenge" .= proof_challenge
717 <> "response" .= proof_response
719 instance Reifies c FFC => FromJSON (Signature c) where
720 parseJSON = JSON.withObject "Signature" $ \o -> do
721 signature_publicKey <- o .: "public_key"
722 proof_challenge <- o .: "challenge"
723 proof_response <- o .: "response"
724 let signature_proof = Proof{..}
729 -- | @('signatureStatement' answers)@
730 -- returns the encrypted material to be signed:
731 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
732 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
734 foldMap $ \Answer{..} ->
735 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
736 [encryption_nonce, encryption_vault]
738 -- | @('signatureCommitments' voterZKP commitment)@
739 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
740 signatureCommitments (ZKP voterZKP) commitment =
741 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
742 <> bytesNat commitment<>"|"
744 -- ** Type 'ErrorBallot'
745 -- | Error raised by 'encryptBallot'.
747 = ErrorBallot_WrongNumberOfAnswers Natural Natural
748 -- ^ When the number of answers
749 -- is different than the number of questions.
750 | ErrorBallot_Answer ErrorAnswer
751 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
753 -- ^ TODO: to be more precise.
754 deriving (Eq,Show,Generic,NFData)