]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: align Proof on Belenios' (slower) calculus
[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.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
40
41 import Voting.Protocol.Utils
42 import Voting.Protocol.FFC
43 import Voting.Protocol.Credential
44
45 -- * Type 'Encryption'
46 -- | ElGamal-like encryption.
47 -- Its security relies on the /Discrete Logarithm problem/.
48 --
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.
54 --
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{..} =
67 JSON.object
68 [ "alpha" .= encryption_nonce
69 , "beta" .= encryption_vault
70 ]
71 toEncoding Encryption{..} =
72 JSON.pairs
73 ( "alpha" .= encryption_nonce
74 <> "beta" .= encryption_vault
75 )
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"
80 return Encryption{..}
81
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
86 x+y = Encryption
87 (encryption_nonce x * encryption_nonce y)
88 (encryption_vault x * encryption_vault y)
89
90 -- *** Type 'EncryptionNonce'
91 type EncryptionNonce = E
92
93 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
94 --
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'.
101 encrypt ::
102 Reifies c FFC =>
103 Monad m => RandomGen r =>
104 PublicKey c -> E c ->
105 S.StateT r m (EncryptionNonce c, Encryption c)
106 encrypt pubKey clear = do
107 encNonce <- random
108 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
109 return $ (encNonce,)
110 Encryption
111 { encryption_nonce = groupGen^encNonce
112 , encryption_vault = pubKey ^encNonce * groupGen^clear
113 }
114
115 -- * Type 'Proof'
116 -- | Non-Interactive Zero-Knowledge 'Proof'
117 -- of knowledge of a discrete logarithm:
118 -- @(secret == logBase base (base^secret))@.
119 data Proof c = Proof
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'.
131 --
132 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
133 --
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@,
138 --
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'.
145 --
146 -- The prover choses 'commitment' to be a random power of @base@,
147 -- to ensure that each 'prove' does not reveal any information
148 -- about its secret.
149 } deriving (Eq,Show,Generic,NFData)
150 instance ToJSON (Proof c) where
151 toJSON Proof{..} =
152 JSON.object
153 [ "challenge" .= proof_challenge
154 , "response" .= proof_response
155 ]
156 toEncoding Proof{..} =
157 JSON.pairs
158 ( "challenge" .= proof_challenge
159 <> "response" .= proof_response
160 )
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"
165 return Proof{..}
166
167 -- ** Type 'ZKP'
168 -- | Zero-knowledge proof.
169 --
170 -- A protocol is /zero-knowledge/ if the verifier
171 -- learns nothing from the protocol except that the prover
172 -- knows the secret.
173 --
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
177
178 -- ** Type 'Challenge'
179 type Challenge = E
180
181 -- ** Type 'Oracle'
182 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
183 -- by 'hash'ing them (eventually with other 'Commitment's).
184 --
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
194
195 -- | @('prove' sec commitmentBases oracle)@
196 -- returns a 'Proof' that @sec@ is known
197 -- (by proving the knowledge of its discrete logarithm).
198 --
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
203 -- (with 'commit').
204 --
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).
207 --
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).
212 prove ::
213 Reifies c FFC =>
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
217 nonce <- random
218 let commitments = (^ nonce) <$> commitmentBases
219 let proof_challenge = oracle commitments
220 return Proof
221 { proof_challenge
222 , proof_response = nonce + sec*proof_challenge
223 -- TODO: switch (+) to (-) when 'commit' will switch (/) to (*).
224 }
225
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'.
231 --
232 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
233 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
234 fakeProof ::
235 Reifies c FFC =>
236 Monad m =>
237 RandomGen r => S.StateT r m (Proof c)
238 fakeProof = do
239 proof_challenge <- random
240 proof_response <- random
241 return Proof{..}
242
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'.
247 type Commitment = G
248
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 #-}
260
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)@,
265 type Disjunction = G
266
267 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
268 booleanDisjunctions = List.take 2 groupGenInverses
269
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) $
274 groupGenInverses
275
276 -- ** Type 'Opinion'
277 -- | Index of a 'Disjunction' within a list of them.
278 -- It is encrypted as an 'E'xponent by 'encrypt'.
279 type Opinion = E
280
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)
290
291 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
292 -- returns a 'DisjProof' that 'enc' 'encrypt's
293 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
294 --
295 -- The prover proves that it knows an 'encNonce', such that:
296 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
297 --
298 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
299 --
300 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
301 proveEncryption ::
302 Reifies c FFC =>
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
323 genuineChallenge
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)
329
330 verifyEncryption ::
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
337 Nothing ->
338 throwE $ ErrorVerifyEncryption_InvalidProofLength
339 (fromIntegral $ List.length proofs)
340 (fromIntegral $ List.length disjs)
341 Just commitments ->
342 return $ challengeSum ==
343 hash (encryptionStatement voterZKP enc) (join commitments)
344 where
345 challengeSum = sum (proof_challenge <$> proofs)
346
347 -- ** Hashing
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<>"|"
353
354 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
355 -- returns the 'Commitment's with only the knowledge of the verifier.
356 --
357 -- For the prover the 'Proof' comes from @fakeProof@,
358 -- and for the verifier the 'Proof' comes from the prover.
359 encryptionCommitments ::
360 Reifies c FFC =>
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.
371 ]
372
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.
379 deriving (Eq,Show)
380
381 -- * Type 'Question'
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{..} =
391 JSON.object
392 [ "question" .= question_text
393 , "answers" .= question_choices
394 , "min" .= question_mini
395 , "max" .= question_maxi
396 ]
397 toEncoding Question{..} =
398 JSON.pairs
399 ( "question" .= question_text
400 <> "answers" .= question_choices
401 <> "min" .= question_mini
402 <> "max" .= question_maxi
403 )
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"
410 return Question{..}
411
412 -- * Type 'Answer'
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
423 toJSON Answer{..} =
424 let (answer_choices, answer_individual_proofs) =
425 List.unzip answer_opinions in
426 JSON.object
427 [ "choices" .= answer_choices
428 , "individual_proofs" .= answer_individual_proofs
429 , "overall_proof" .= answer_sumProof
430 ]
431 toEncoding Answer{..} =
432 let (answer_choices, answer_individual_proofs) =
433 List.unzip answer_opinions in
434 JSON.pairs
435 ( "choices" .= answer_choices
436 <> "individual_proofs" .= answer_individual_proofs
437 <> "overall_proof" .= answer_sumProof
438 )
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"
445 return Answer{..}
446
447 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
448 -- returns an 'Answer' validable by 'verifyAnswer',
449 -- unless an 'ErrorAnswer' is returned.
450 encryptAnswer ::
451 Reifies c FFC =>
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) =
458 lift $ throwE $
459 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
460 | List.length opinions /= List.length question_choices =
461 lift $ throwE $
462 ErrorAnswer_WrongNumberOfOpinions
463 (fromIntegral $ List.length opinions)
464 (fromIntegral $ List.length question_choices)
465 | otherwise = do
466 encryptions <- encrypt elecPubKey `mapM` opinions
467 individualProofs <- zipWithM
468 (\opinion -> proveEncryption elecPubKey zkp $
469 if opinion
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
479 )
480 return $ Answer
481 { answer_opinions = List.zip
482 (snd <$> encryptions) -- NOTE: drop encNonce
483 individualProofs
484 , answer_sumProof = sumProof
485 }
486 where
487 opinionsSum = sum $ nat <$> opinions
488 opinions = (\o -> if o then one else zero) <$> opinionByChoice
489
490 verifyAnswer ::
491 Reifies c FFC =>
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
497 validOpinions <-
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)
503 , answer_sumProof )
504 return (and validOpinions && validSum)
505
506 -- ** Type 'ErrorAnswer'
507 -- | Error raised by 'encryptAnswer'.
508 data ErrorAnswer
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)
516
517 -- * Type 'Election'
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)
526
527 instance ToJSON (Election c) where
528 toJSON Election{..} =
529 JSON.object
530 [ "name" .= election_name
531 , "description" .= election_description
532 , "public_key" .= election_crypto
533 , "questions" .= election_questions
534 , "uuid" .= election_uuid
535 ]
536 toEncoding Election{..} =
537 JSON.pairs
538 ( "name" .= election_name
539 <> "description" .= election_description
540 <> "public_key" .= election_crypto
541 <> "questions" .= election_questions
542 <> "uuid" .= election_uuid
543 )
544 instance FromJSON (Election ()) where
545 parseJSON = JSON.withObject "Election" $ \o -> Election
546 <$> o .: "name"
547 <*> o .: "description"
548 <*> o .: "public_key"
549 <*> o .: "questions"
550 <*> o .: "uuid"
551 <*> pure (hashJSON (JSON.Object o))
552
553 -- ** Type 'ElectionCrypto'
554 data ElectionCrypto c =
555 ElectionCrypto_FFC
556 { electionCrypto_FFC_params :: !FFC
557 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
558 } deriving (Eq,Show,Generic,NFData)
559
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)), ..}
566
567 instance ToJSON (ElectionCrypto c) where
568 toJSON (ElectionCrypto_FFC ffc pubKey) =
569 JSON.object
570 [ "group" .= ffc
571 , "y" .= pubKey
572 ]
573 toEncoding (ElectionCrypto_FFC ffc pubKey) =
574 JSON.pairs
575 ( "group" .= ffc
576 <> "y" .= pubKey
577 )
578 instance FromJSON (ElectionCrypto ()) where
579 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
580 ffc <- o .: "group"
581 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
582 return $ ElectionCrypto_FFC ffc (G (F pubKey))
583
584
585 -- ** Type 'Hash'
586 newtype Hash = Hash Text
587 deriving (Eq,Ord,Show,Generic)
588 deriving anyclass (ToJSON,FromJSON)
589 deriving newtype NFData
590
591 hashJSON :: ToJSON a => a -> Hash
592 hashJSON = Hash . hexHash . BSL.toStrict . BSL64.encode . JSON.encode
593
594 hashElection :: Election c -> Election c
595 hashElection elec = elec{election_hash=hashJSON elec}
596
597 -- * Type 'Ballot'
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
605 toJSON Ballot{..} =
606 JSON.object $
607 [ "answers" .= ballot_answers
608 , "election_uuid" .= ballot_election_uuid
609 , "election_hash" .= ballot_election_hash
610 ] <>
611 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
612 toEncoding Ballot{..} =
613 JSON.pairs $
614 ( "answers" .= ballot_answers
615 <> "election_uuid" .= ballot_election_uuid
616 <> "election_hash" .= ballot_election_hash
617 ) <>
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"
625 return Ballot{..}
626
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'.
631 encryptBallot ::
632 Reifies c FFC =>
633 Monad m => RandomGen r =>
634 Election c ->
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 =
639 lift $ throwE $
640 ErrorBallot_WrongNumberOfAnswers
641 (fromIntegral $ List.length opinionsByQuest)
642 (fromIntegral $ List.length election_questions)
643 | otherwise = do
644 let (voterKeys, voterZKP) =
645 case ballotSecKeyMay of
646 Nothing -> (Nothing, ZKP "")
647 Just ballotSecKey ->
648 ( Just (ballotSecKey, ballotPubKey)
649 , ZKP (bytesNat ballotPubKey) )
650 where ballotPubKey = publicKey ballotSecKey
651 ballot_answers <-
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
658 signature_proof <-
659 prove ballotSecKey (Identity groupGen) $
660 \(Identity commitment) ->
661 hash
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{..}
668 return Ballot
669 { ballot_answers
670 , ballot_election_hash = election_hash
671 , ballot_election_uuid = election_uuid
672 , ballot_signature
673 }
674
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
686 (, zkp) $
687 proof_challenge signature_proof == hash
688 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
689 (signatureStatement ballot_answers)
690 in
691 and $ isValidSign :
692 List.zipWith (verifyAnswer elecPubKey zkpSign)
693 election_questions ballot_answers
694
695 -- ** Type 'Signature'
696 -- | Schnorr-like signature.
697 --
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{..}) =
708 JSON.object
709 [ "public_key" .= pubKey
710 , "challenge" .= proof_challenge
711 , "response" .= proof_response
712 ]
713 toEncoding (Signature pubKey Proof{..}) =
714 JSON.pairs
715 ( "public_key" .= pubKey
716 <> "challenge" .= proof_challenge
717 <> "response" .= proof_response
718 )
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{..}
725 return Signature{..}
726
727 -- *** Hashing
728
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]
733 signatureStatement =
734 foldMap $ \Answer{..} ->
735 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
736 [encryption_nonce, encryption_vault]
737
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<>"|"
743
744 -- ** Type 'ErrorBallot'
745 -- | Error raised by 'encryptBallot'.
746 data ErrorBallot
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'.
752 | ErrorBallot_Wrong
753 -- ^ TODO: to be more precise.
754 deriving (Eq,Show,Generic,NFData)