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