]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: fix election_hash
[majurity.git] / hjugement-protocol / src / Voting / Protocol / Election.hs
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
8
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(..),(.:),(.:?),(.=))
15 import Data.Bool
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.String (String)
27 import Data.Text (Text)
28 import Data.Traversable (Traversable(..))
29 import Data.Tuple (fst, snd)
30 import GHC.Generics (Generic)
31 import GHC.Natural (minusNaturalMaybe)
32 import Numeric.Natural (Natural)
33 import Prelude (fromIntegral)
34 import System.IO (IO, FilePath)
35 import Text.Show (Show(..))
36 import qualified Control.Monad.Trans.State.Strict as S
37 import qualified Data.Aeson as JSON
38 import qualified Data.ByteString as BS
39 import qualified Data.ByteString.Lazy as BSL
40 import qualified Data.List as List
41
42 import Voting.Protocol.Utils
43 import Voting.Protocol.FFC
44 import Voting.Protocol.Credential
45
46 -- * Type 'Encryption'
47 -- | ElGamal-like encryption.
48 -- Its security relies on the /Discrete Logarithm problem/.
49 --
50 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
51 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
52 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
53 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
54 -- to enable the additive homomorphism.
55 --
56 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
57 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
58 data Encryption c = Encryption
59 { encryption_nonce :: !(G c)
60 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
61 -- equal to @('groupGen' '^'encNonce)@
62 , encryption_vault :: !(G c)
63 -- ^ Encrypted 'clear' text,
64 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
65 } deriving (Eq,Show,Generic,NFData)
66 instance Reifies c FFC => ToJSON (Encryption c) where
67 toJSON Encryption{..} =
68 JSON.object
69 [ "alpha" .= encryption_nonce
70 , "beta" .= encryption_vault
71 ]
72 toEncoding Encryption{..} =
73 JSON.pairs
74 ( "alpha" .= encryption_nonce
75 <> "beta" .= encryption_vault
76 )
77 instance Reifies c FFC => FromJSON (Encryption c) where
78 parseJSON = JSON.withObject "Encryption" $ \o -> do
79 encryption_nonce <- o .: "alpha"
80 encryption_vault <- o .: "beta"
81 return Encryption{..}
82
83 -- | Additive homomorphism.
84 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
85 instance Reifies c FFC => Additive (Encryption c) where
86 zero = Encryption one one
87 x+y = Encryption
88 (encryption_nonce x * encryption_nonce y)
89 (encryption_vault x * encryption_vault y)
90
91 -- *** Type 'EncryptionNonce'
92 type EncryptionNonce = E
93
94 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
95 --
96 -- WARNING: the secret encryption nonce (@encNonce@)
97 -- is returned alongside the 'Encryption'
98 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
99 -- but this secret @encNonce@ MUST be forgotten after that,
100 -- as it may be used to decipher the 'Encryption'
101 -- without the 'SecretKey' associated with 'pubKey'.
102 encrypt ::
103 Reifies c FFC =>
104 Monad m => RandomGen r =>
105 PublicKey c -> E c ->
106 S.StateT r m (EncryptionNonce c, Encryption c)
107 encrypt pubKey clear = do
108 encNonce <- random
109 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
110 return $ (encNonce,)
111 Encryption
112 { encryption_nonce = groupGen^encNonce
113 , encryption_vault = pubKey ^encNonce * groupGen^clear
114 }
115
116 -- * Type 'Proof'
117 -- | Non-Interactive Zero-Knowledge 'Proof'
118 -- of knowledge of a discrete logarithm:
119 -- @(secret == logBase base (base^secret))@.
120 data Proof c = Proof
121 { proof_challenge :: Challenge c
122 -- ^ 'Challenge' sent by the verifier to the prover
123 -- to ensure that the prover really has knowledge
124 -- of the secret and is not replaying.
125 -- Actually, 'proof_challenge' is not sent to the prover,
126 -- but derived from the prover's 'Commitment's and statements
127 -- with a collision resistant 'hash'.
128 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
129 , proof_response :: E c
130 -- ^ A discrete logarithm sent by the prover to the verifier,
131 -- as a response to 'proof_challenge'.
132 --
133 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
134 --
135 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
136 -- * @commitment '==' 'commit' proof base basePowSec '=='
137 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
138 -- * and @basePowSec '==' base'^'sec@,
139 --
140 -- then, with overwhelming probability (due to the 'hash' function),
141 -- the prover was not able to choose 'proof_challenge'
142 -- yet was able to compute a 'proof_response' such that
143 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
144 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
145 -- therefore the prover knows 'sec'.
146 --
147 -- The prover choses 'commitment' to be a random power of @base@,
148 -- to ensure that each 'prove' does not reveal any information
149 -- about its secret.
150 } deriving (Eq,Show,Generic,NFData)
151 instance ToJSON (Proof c) where
152 toJSON Proof{..} =
153 JSON.object
154 [ "challenge" .= proof_challenge
155 , "response" .= proof_response
156 ]
157 toEncoding Proof{..} =
158 JSON.pairs
159 ( "challenge" .= proof_challenge
160 <> "response" .= proof_response
161 )
162 instance Reifies c FFC => FromJSON (Proof c) where
163 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
164 proof_challenge <- o .: "challenge"
165 proof_response <- o .: "response"
166 return Proof{..}
167
168 -- ** Type 'ZKP'
169 -- | Zero-knowledge proof.
170 --
171 -- A protocol is /zero-knowledge/ if the verifier
172 -- learns nothing from the protocol except that the prover
173 -- knows the secret.
174 --
175 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
176 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
177 newtype ZKP = ZKP BS.ByteString
178
179 -- ** Type 'Challenge'
180 type Challenge = E
181
182 -- ** Type 'Oracle'
183 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
184 -- by 'hash'ing them (eventually with other 'Commitment's).
185 --
186 -- Used in 'prove' it enables a Fiat-Shamir transformation
187 -- of an /interactive zero-knowledge/ (IZK) proof
188 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
189 -- That is to say that the verifier does not have
190 -- to send a 'Challenge' to the prover.
191 -- Indeed, the prover now handles the 'Challenge'
192 -- which becomes a (collision resistant) 'hash'
193 -- of the prover's commitments (and statements to be a stronger proof).
194 type Oracle list c = list (Commitment c) -> Challenge c
195
196 -- | @('prove' sec commitmentBases oracle)@
197 -- returns a 'Proof' that @sec@ is known
198 -- (by proving the knowledge of its discrete logarithm).
199 --
200 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
201 -- raised to the power of the secret nonce of the 'Proof',
202 -- as those are the 'Commitment's that the verifier will obtain
203 -- when composing the 'proof_challenge' and 'proof_response' together
204 -- (with 'commit').
205 --
206 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
207 -- the statement must be included in the 'hash' (along with the commitments).
208 --
209 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
210 -- does not reveal any information regarding the secret @sec@,
211 -- because two 'Proof's using the same 'Commitment'
212 -- can be used to deduce @sec@ (using the special-soundness).
213 prove ::
214 Reifies c FFC =>
215 Monad m => RandomGen r => Functor list =>
216 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
217 prove sec commitmentBases oracle = do
218 nonce <- random
219 let commitments = (^ nonce) <$> commitmentBases
220 let proof_challenge = oracle commitments
221 return Proof
222 { proof_challenge
223 , proof_response = nonce + sec*proof_challenge
224 -- TODO: switch (+) to (-) when 'commit' will switch (/) to (*).
225 }
226
227 -- | @('fakeProof')@ returns a 'Proof'
228 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
229 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
230 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
231 -- as a 'Proof' returned by 'prove'.
232 --
233 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
234 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
235 fakeProof ::
236 Reifies c FFC =>
237 Monad m =>
238 RandomGen r => S.StateT r m (Proof c)
239 fakeProof = do
240 proof_challenge <- random
241 proof_response <- random
242 return Proof{..}
243
244 -- ** Type 'Commitment'
245 -- | A commitment from the prover to the verifier.
246 -- It's a power of 'groupGen' chosen randomly by the prover
247 -- when making a 'Proof' with 'prove'.
248 type Commitment = G
249
250 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
251 -- from the given 'Proof' with the knowledge of the verifier.
252 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
253 commit Proof{..} base basePowSec =
254 base^proof_response /
255 basePowSec^proof_challenge
256 -- TODO: contrary to some textbook presentations,
257 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
258 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
259 -- this would be compensated by using @('-')@ instead of @('+')@ in 'prove'.
260 {-# INLINE commit #-}
261
262 -- * Type 'Disjunction'
263 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
264 -- it's used in 'proveEncryption' to generate a 'Proof'
265 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
266 type Disjunction = G
267
268 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
269 booleanDisjunctions = List.take 2 groupGenInverses
270
271 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
272 intervalDisjunctions mini maxi =
273 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
274 List.genericDrop (nat mini) $
275 groupGenInverses
276
277 -- ** Type 'Opinion'
278 -- | Index of a 'Disjunction' within a list of them.
279 -- It is encrypted as an 'E'xponent by 'encrypt'.
280 type Opinion = E
281
282 -- ** Type 'DisjProof'
283 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
284 -- is indexing a 'Disjunction' within a list of them,
285 -- without revealing which 'Opinion' it is.
286 newtype DisjProof c = DisjProof [Proof c]
287 deriving (Eq,Show,Generic)
288 deriving newtype NFData
289 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
290 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
291
292 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
293 -- returns a 'DisjProof' that 'enc' 'encrypt's
294 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
295 --
296 -- The prover proves that it knows an 'encNonce', such that:
297 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
298 --
299 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
300 --
301 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
302 proveEncryption ::
303 Reifies c FFC =>
304 Monad m => RandomGen r =>
305 PublicKey c -> ZKP ->
306 ([Disjunction c],[Disjunction c]) ->
307 (EncryptionNonce c, Encryption c) ->
308 S.StateT r m (DisjProof c)
309 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
310 -- Fake proofs for all 'Disjunction's except the genuine one.
311 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
312 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
313 let fakeChallengeSum =
314 sum (proof_challenge <$> prevFakeProofs) +
315 sum (proof_challenge <$> nextFakeProofs)
316 let statement = encryptionStatement voterZKP enc
317 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
318 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
319 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
320 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
321 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
322 let challenge = hash statement commitments in
323 let genuineChallenge = challenge - fakeChallengeSum in
324 genuineChallenge
325 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
326 -- thus (sum (proof_challenge <$> proofs) == challenge)
327 -- as checked in 'verifyEncryption'.
328 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
329 return (DisjProof proofs)
330
331 verifyEncryption ::
332 Reifies c FFC => Monad m =>
333 PublicKey c -> ZKP ->
334 [Disjunction c] -> (Encryption c, DisjProof c) ->
335 ExceptT ErrorVerifyEncryption m Bool
336 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
337 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
338 Nothing ->
339 throwE $ ErrorVerifyEncryption_InvalidProofLength
340 (fromIntegral $ List.length proofs)
341 (fromIntegral $ List.length disjs)
342 Just commitments ->
343 return $ challengeSum ==
344 hash (encryptionStatement voterZKP enc) (join commitments)
345 where
346 challengeSum = sum (proof_challenge <$> proofs)
347
348 -- ** Hashing
349 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
350 encryptionStatement (ZKP voterZKP) Encryption{..} =
351 "prove|"<>voterZKP<>"|"
352 <> bytesNat encryption_nonce<>","
353 <> bytesNat encryption_vault<>"|"
354
355 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
356 -- returns the 'Commitment's with only the knowledge of the verifier.
357 --
358 -- For the prover the 'Proof' comes from @fakeProof@,
359 -- and for the verifier the 'Proof' comes from the prover.
360 encryptionCommitments ::
361 Reifies c FFC =>
362 PublicKey c -> Encryption c ->
363 Disjunction c -> Proof c -> [G c]
364 encryptionCommitments elecPubKey Encryption{..} disj proof =
365 [ commit proof groupGen encryption_nonce
366 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
367 -- base==groupGen, basePowSec==groupGen^encNonce.
368 , commit proof elecPubKey (encryption_vault*disj)
369 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
370 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
371 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
372 ]
373
374 -- ** Type 'ErrorVerifyEncryption'
375 -- | Error raised by 'verifyEncryption'.
376 data ErrorVerifyEncryption
377 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
378 -- ^ When the number of proofs is different than
379 -- the number of 'Disjunction's.
380 deriving (Eq,Show)
381
382 -- * Type 'Question'
383 data Question = Question
384 { question_text :: !Text
385 , question_choices :: ![Text]
386 , question_mini :: !Natural
387 , question_maxi :: !Natural
388 -- , question_blank :: Maybe Bool
389 } deriving (Eq,Show,Generic,NFData)
390 instance ToJSON Question where
391 toJSON Question{..} =
392 JSON.object
393 [ "question" .= question_text
394 , "answers" .= question_choices
395 , "min" .= question_mini
396 , "max" .= question_maxi
397 ]
398 toEncoding Question{..} =
399 JSON.pairs
400 ( "question" .= question_text
401 <> "answers" .= question_choices
402 <> "min" .= question_mini
403 <> "max" .= question_maxi
404 )
405 instance FromJSON Question where
406 parseJSON = JSON.withObject "Question" $ \o -> do
407 question_text <- o .: "question"
408 question_choices <- o .: "answers"
409 question_mini <- o .: "min"
410 question_maxi <- o .: "max"
411 return Question{..}
412
413 -- * Type 'Answer'
414 data Answer c = Answer
415 { answer_opinions :: ![(Encryption c, DisjProof c)]
416 -- ^ Encrypted 'Opinion' for each 'question_choices'
417 -- with a 'DisjProof' that they belong to [0,1].
418 , answer_sumProof :: !(DisjProof c)
419 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
420 -- is an element of @[mini..maxi]@.
421 -- , answer_blankProof ::
422 } deriving (Eq,Show,Generic,NFData)
423 instance Reifies c FFC => ToJSON (Answer c) where
424 toJSON Answer{..} =
425 let (answer_choices, answer_individual_proofs) =
426 List.unzip answer_opinions in
427 JSON.object
428 [ "choices" .= answer_choices
429 , "individual_proofs" .= answer_individual_proofs
430 , "overall_proof" .= answer_sumProof
431 ]
432 toEncoding Answer{..} =
433 let (answer_choices, answer_individual_proofs) =
434 List.unzip answer_opinions in
435 JSON.pairs
436 ( "choices" .= answer_choices
437 <> "individual_proofs" .= answer_individual_proofs
438 <> "overall_proof" .= answer_sumProof
439 )
440 instance Reifies c FFC => FromJSON (Answer c) where
441 parseJSON = JSON.withObject "Answer" $ \o -> do
442 answer_choices <- o .: "choices"
443 answer_individual_proofs <- o .: "individual_proofs"
444 let answer_opinions = List.zip answer_choices answer_individual_proofs
445 answer_sumProof <- o .: "overall_proof"
446 return Answer{..}
447
448 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
449 -- returns an 'Answer' validable by 'verifyAnswer',
450 -- unless an 'ErrorAnswer' is returned.
451 encryptAnswer ::
452 Reifies c FFC =>
453 Monad m => RandomGen r =>
454 PublicKey c -> ZKP ->
455 Question -> [Bool] ->
456 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
457 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
458 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
459 lift $ throwE $
460 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
461 | List.length opinions /= List.length question_choices =
462 lift $ throwE $
463 ErrorAnswer_WrongNumberOfOpinions
464 (fromIntegral $ List.length opinions)
465 (fromIntegral $ List.length question_choices)
466 | otherwise = do
467 encryptions <- encrypt elecPubKey `mapM` opinions
468 individualProofs <- zipWithM
469 (\opinion -> proveEncryption elecPubKey zkp $
470 if opinion
471 then (List.init booleanDisjunctions,[])
472 else ([],List.tail booleanDisjunctions))
473 opinionByChoice encryptions
474 sumProof <- proveEncryption elecPubKey zkp
475 (List.tail <$> List.genericSplitAt
476 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
477 (intervalDisjunctions question_mini question_maxi))
478 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
479 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
480 )
481 return $ Answer
482 { answer_opinions = List.zip
483 (snd <$> encryptions) -- NOTE: drop encNonce
484 individualProofs
485 , answer_sumProof = sumProof
486 }
487 where
488 opinionsSum = sum $ nat <$> opinions
489 opinions = (\o -> if o then one else zero) <$> opinionByChoice
490
491 verifyAnswer ::
492 Reifies c FFC =>
493 PublicKey c -> ZKP ->
494 Question -> Answer c -> Bool
495 verifyAnswer elecPubKey zkp Question{..} Answer{..}
496 | List.length question_choices /= List.length answer_opinions = False
497 | otherwise = either (const False) id $ runExcept $ do
498 validOpinions <-
499 verifyEncryption elecPubKey zkp booleanDisjunctions
500 `traverse` answer_opinions
501 validSum <- verifyEncryption elecPubKey zkp
502 (intervalDisjunctions question_mini question_maxi)
503 ( sum (fst <$> answer_opinions)
504 , answer_sumProof )
505 return (and validOpinions && validSum)
506
507 -- ** Type 'ErrorAnswer'
508 -- | Error raised by 'encryptAnswer'.
509 data ErrorAnswer
510 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
511 -- ^ When the number of opinions is different than
512 -- the number of choices ('question_choices').
513 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
514 -- ^ When the sum of opinions is not within the bounds
515 -- of 'question_mini' and 'question_maxi'.
516 deriving (Eq,Show,Generic,NFData)
517
518 -- * Type 'Election'
519 data Election c = Election
520 { election_name :: !Text
521 , election_description :: !Text
522 , election_crypto :: !(ElectionCrypto c)
523 , election_questions :: ![Question]
524 , election_uuid :: !UUID
525 , election_hash :: !Hash
526 } deriving (Eq,Show,Generic,NFData)
527
528 instance ToJSON (Election c) where
529 toJSON Election{..} =
530 JSON.object
531 [ "name" .= election_name
532 , "description" .= election_description
533 , "public_key" .= election_crypto
534 , "questions" .= election_questions
535 , "uuid" .= election_uuid
536 ]
537 toEncoding Election{..} =
538 JSON.pairs
539 ( "name" .= election_name
540 <> "description" .= election_description
541 <> "public_key" .= election_crypto
542 <> "questions" .= election_questions
543 <> "uuid" .= election_uuid
544 )
545 instance FromJSON (Election ()) where
546 parseJSON = JSON.withObject "Election" $ \o -> Election
547 <$> o .: "name"
548 <*> o .: "description"
549 <*> o .: "public_key"
550 <*> o .: "questions"
551 <*> o .: "uuid"
552 <*> pure (Base64SHA256 "")
553 -- NOTE: set in 'readElection'.
554
555 readElection :: FilePath -> ExceptT String IO (Election ())
556 readElection filePath = do
557 fileData <- lift $ BS.readFile filePath
558 ExceptT $ return $
559 (\e -> e{election_hash=base64SHA256 fileData})
560 <$> JSON.eitherDecodeStrict' fileData
561
562 hashElection :: Election c -> Base64SHA256
563 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
564
565 -- ** Type 'ElectionCrypto'
566 data ElectionCrypto c =
567 ElectionCrypto_FFC
568 { electionCrypto_FFC_params :: !FFC
569 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
570 } deriving (Eq,Show,Generic,NFData)
571
572 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
573 reifyElection Election{..} k =
574 case election_crypto of
575 ElectionCrypto_FFC ffc (G (F pubKey)) ->
576 reify ffc $ \(_::Proxy c) -> k @c
577 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
578
579 instance ToJSON (ElectionCrypto c) where
580 toJSON (ElectionCrypto_FFC ffc pubKey) =
581 JSON.object
582 [ "group" .= ffc
583 , "y" .= pubKey
584 ]
585 toEncoding (ElectionCrypto_FFC ffc pubKey) =
586 JSON.pairs
587 ( "group" .= ffc
588 <> "y" .= pubKey
589 )
590 instance FromJSON (ElectionCrypto ()) where
591 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
592 ffc <- o .: "group"
593 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
594 return $ ElectionCrypto_FFC ffc (G (F pubKey))
595
596 -- * Type 'Ballot'
597 data Ballot c = Ballot
598 { ballot_answers :: ![Answer c]
599 , ballot_signature :: !(Maybe (Signature c))
600 , ballot_election_uuid :: !UUID
601 , ballot_election_hash :: !Base64SHA256
602 } deriving (Generic,NFData)
603 instance Reifies c FFC => ToJSON (Ballot c) where
604 toJSON Ballot{..} =
605 JSON.object $
606 [ "answers" .= ballot_answers
607 , "election_uuid" .= ballot_election_uuid
608 , "election_hash" .= ballot_election_hash
609 ] <>
610 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
611 toEncoding Ballot{..} =
612 JSON.pairs $
613 ( "answers" .= ballot_answers
614 <> "election_uuid" .= ballot_election_uuid
615 <> "election_hash" .= ballot_election_hash
616 ) <>
617 maybe mempty (\sig -> "signature" .= sig) ballot_signature
618 instance Reifies c FFC => FromJSON (Ballot c) where
619 parseJSON = JSON.withObject "Ballot" $ \o -> do
620 ballot_answers <- o .: "answers"
621 ballot_signature <- o .:? "signature"
622 ballot_election_uuid <- o .: "election_uuid"
623 ballot_election_hash <- o .: "election_hash"
624 return Ballot{..}
625
626 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
627 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
628 -- where 'opinionsByQuest' is a list of 'Opinion's
629 -- on each 'question_choices' of each 'election_questions'.
630 encryptBallot ::
631 Reifies c FFC =>
632 Monad m => RandomGen r =>
633 Election c ->
634 Maybe (SecretKey c) -> [[Bool]] ->
635 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
636 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
637 | List.length election_questions /= List.length opinionsByQuest =
638 lift $ throwE $
639 ErrorBallot_WrongNumberOfAnswers
640 (fromIntegral $ List.length opinionsByQuest)
641 (fromIntegral $ List.length election_questions)
642 | otherwise = do
643 let (voterKeys, voterZKP) =
644 case ballotSecKeyMay of
645 Nothing -> (Nothing, ZKP "")
646 Just ballotSecKey ->
647 ( Just (ballotSecKey, ballotPubKey)
648 , ZKP (bytesNat ballotPubKey) )
649 where ballotPubKey = publicKey ballotSecKey
650 ballot_answers <-
651 S.mapStateT (withExceptT ErrorBallot_Answer) $
652 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
653 election_questions opinionsByQuest
654 ballot_signature <- case voterKeys of
655 Nothing -> return Nothing
656 Just (ballotSecKey, signature_publicKey) -> do
657 signature_proof <-
658 prove ballotSecKey (Identity groupGen) $
659 \(Identity commitment) ->
660 hash
661 -- NOTE: the order is unusual, the commitments are first
662 -- then comes the statement. Best guess is that
663 -- this is easier to code due to their respective types.
664 (signatureCommitments voterZKP commitment)
665 (signatureStatement ballot_answers)
666 return $ Just Signature{..}
667 return Ballot
668 { ballot_answers
669 , ballot_election_hash = election_hash
670 , ballot_election_uuid = election_uuid
671 , ballot_signature
672 }
673
674 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
675 verifyBallot Election{..} Ballot{..} =
676 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
677 ballot_election_uuid == election_uuid &&
678 ballot_election_hash == election_hash &&
679 List.length election_questions == List.length ballot_answers &&
680 let (isValidSign, zkpSign) =
681 case ballot_signature of
682 Nothing -> (True, ZKP "")
683 Just Signature{..} ->
684 let zkp = ZKP (bytesNat signature_publicKey) in
685 (, zkp) $
686 proof_challenge signature_proof == hash
687 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
688 (signatureStatement ballot_answers)
689 in
690 and $ isValidSign :
691 List.zipWith (verifyAnswer elecPubKey zkpSign)
692 election_questions ballot_answers
693
694 -- ** Type 'Signature'
695 -- | Schnorr-like signature.
696 --
697 -- Used by each voter to sign his/her encrypted 'Ballot'
698 -- using his/her 'Credential',
699 -- in order to avoid ballot stuffing.
700 data Signature c = Signature
701 { signature_publicKey :: !(PublicKey c)
702 -- ^ Verification key.
703 , signature_proof :: !(Proof c)
704 } deriving (Generic,NFData)
705 instance Reifies c FFC => ToJSON (Signature c) where
706 toJSON (Signature pubKey Proof{..}) =
707 JSON.object
708 [ "public_key" .= pubKey
709 , "challenge" .= proof_challenge
710 , "response" .= proof_response
711 ]
712 toEncoding (Signature pubKey Proof{..}) =
713 JSON.pairs
714 ( "public_key" .= pubKey
715 <> "challenge" .= proof_challenge
716 <> "response" .= proof_response
717 )
718 instance Reifies c FFC => FromJSON (Signature c) where
719 parseJSON = JSON.withObject "Signature" $ \o -> do
720 signature_publicKey <- o .: "public_key"
721 proof_challenge <- o .: "challenge"
722 proof_response <- o .: "response"
723 let signature_proof = Proof{..}
724 return Signature{..}
725
726 -- *** Hashing
727
728 -- | @('signatureStatement' answers)@
729 -- returns the encrypted material to be signed:
730 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
731 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
732 signatureStatement =
733 foldMap $ \Answer{..} ->
734 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
735 [encryption_nonce, encryption_vault]
736
737 -- | @('signatureCommitments' voterZKP commitment)@
738 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
739 signatureCommitments (ZKP voterZKP) commitment =
740 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
741 <> bytesNat commitment<>"|"
742
743 -- ** Type 'ErrorBallot'
744 -- | Error raised by 'encryptBallot'.
745 data ErrorBallot
746 = ErrorBallot_WrongNumberOfAnswers Natural Natural
747 -- ^ When the number of answers
748 -- is different than the number of questions.
749 | ErrorBallot_Answer ErrorAnswer
750 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
751 | ErrorBallot_Wrong
752 -- ^ TODO: to be more precise.
753 deriving (Eq,Show,Generic,NFData)