]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: bump versions
[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 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
228 -- when Helios-C specifications will be fixed.
229 proveQuicker ::
230 Reifies c FFC =>
231 Monad m => RandomGen r => Functor list =>
232 E c -> list (G c) -> Oracle list c -> S.StateT r m (Proof c)
233 proveQuicker sec commitmentBases oracle = do
234 nonce <- random
235 let commitments = (^ nonce) <$> commitmentBases
236 let proof_challenge = oracle commitments
237 return Proof
238 { proof_challenge
239 , proof_response = nonce - sec*proof_challenge
240 }
241
242 -- | @('fakeProof')@ returns a 'Proof'
243 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
244 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
245 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
246 -- as a 'Proof' returned by 'prove'.
247 --
248 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
249 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
250 fakeProof ::
251 Reifies c FFC =>
252 Monad m =>
253 RandomGen r => S.StateT r m (Proof c)
254 fakeProof = do
255 proof_challenge <- random
256 proof_response <- random
257 return Proof{..}
258
259 -- ** Type 'Commitment'
260 -- | A commitment from the prover to the verifier.
261 -- It's a power of 'groupGen' chosen randomly by the prover
262 -- when making a 'Proof' with 'prove'.
263 type Commitment = G
264
265 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
266 -- from the given 'Proof' with the knowledge of the verifier.
267 commit :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
268 commit Proof{..} base basePowSec =
269 base^proof_response /
270 basePowSec^proof_challenge
271 -- TODO: contrary to some textbook presentations,
272 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
273 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
274 -- this would be compensated by using @('-')@ instead of @('+')@ in 'prove'.
275 {-# INLINE commit #-}
276
277 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
278 -- when Helios-C specifications will be fixed.
279 commitQuicker :: Reifies c FFC => Proof c -> G c -> G c -> Commitment c
280 commitQuicker Proof{..} base basePowSec =
281 base^proof_response *
282 basePowSec^proof_challenge
283
284 -- * Type 'Disjunction'
285 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
286 -- it's used in 'proveEncryption' to generate a 'Proof'
287 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
288 type Disjunction = G
289
290 booleanDisjunctions :: Reifies c FFC => [Disjunction c]
291 booleanDisjunctions = List.take 2 groupGenInverses
292
293 intervalDisjunctions :: Reifies c FFC => Natural -> Natural -> [Disjunction c]
294 intervalDisjunctions mini maxi =
295 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
296 List.genericDrop (nat mini) $
297 groupGenInverses
298
299 -- ** Type 'Opinion'
300 -- | Index of a 'Disjunction' within a list of them.
301 -- It is encrypted as an 'E'xponent by 'encrypt'.
302 type Opinion = E
303
304 -- ** Type 'DisjProof'
305 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
306 -- is indexing a 'Disjunction' within a list of them,
307 -- without revealing which 'Opinion' it is.
308 newtype DisjProof c = DisjProof [Proof c]
309 deriving (Eq,Show,Generic)
310 deriving newtype NFData
311 deriving newtype instance Reifies c FFC => ToJSON (DisjProof c)
312 deriving newtype instance Reifies c FFC => FromJSON (DisjProof c)
313
314 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
315 -- returns a 'DisjProof' that 'enc' 'encrypt's
316 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
317 --
318 -- The prover proves that it knows an 'encNonce', such that:
319 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
320 --
321 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
322 --
323 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
324 proveEncryption ::
325 Reifies c FFC =>
326 Monad m => RandomGen r =>
327 PublicKey c -> ZKP ->
328 ([Disjunction c],[Disjunction c]) ->
329 (EncryptionNonce c, Encryption c) ->
330 S.StateT r m (DisjProof c)
331 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
332 -- Fake proofs for all 'Disjunction's except the genuine one.
333 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
334 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
335 let fakeChallengeSum =
336 sum (proof_challenge <$> prevFakeProofs) +
337 sum (proof_challenge <$> nextFakeProofs)
338 let statement = encryptionStatement voterZKP enc
339 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
340 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
341 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
342 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
343 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
344 let challenge = hash statement commitments in
345 let genuineChallenge = challenge - fakeChallengeSum in
346 genuineChallenge
347 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
348 -- thus (sum (proof_challenge <$> proofs) == challenge)
349 -- as checked in 'verifyEncryption'.
350 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
351 return (DisjProof proofs)
352
353 verifyEncryption ::
354 Reifies c FFC => Monad m =>
355 PublicKey c -> ZKP ->
356 [Disjunction c] -> (Encryption c, DisjProof c) ->
357 ExceptT ErrorVerifyEncryption m Bool
358 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
359 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
360 Nothing ->
361 throwE $ ErrorVerifyEncryption_InvalidProofLength
362 (fromIntegral $ List.length proofs)
363 (fromIntegral $ List.length disjs)
364 Just commitments ->
365 return $ challengeSum ==
366 hash (encryptionStatement voterZKP enc) (join commitments)
367 where
368 challengeSum = sum (proof_challenge <$> proofs)
369
370 -- ** Hashing
371 encryptionStatement :: Reifies c FFC => ZKP -> Encryption c -> BS.ByteString
372 encryptionStatement (ZKP voterZKP) Encryption{..} =
373 "prove|"<>voterZKP<>"|"
374 <> bytesNat encryption_nonce<>","
375 <> bytesNat encryption_vault<>"|"
376
377 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
378 -- returns the 'Commitment's with only the knowledge of the verifier.
379 --
380 -- For the prover the 'Proof' comes from @fakeProof@,
381 -- and for the verifier the 'Proof' comes from the prover.
382 encryptionCommitments ::
383 Reifies c FFC =>
384 PublicKey c -> Encryption c ->
385 Disjunction c -> Proof c -> [G c]
386 encryptionCommitments elecPubKey Encryption{..} disj proof =
387 [ commit proof groupGen encryption_nonce
388 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
389 -- base==groupGen, basePowSec==groupGen^encNonce.
390 , commit proof elecPubKey (encryption_vault*disj)
391 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
392 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
393 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
394 ]
395
396 -- ** Type 'ErrorVerifyEncryption'
397 -- | Error raised by 'verifyEncryption'.
398 data ErrorVerifyEncryption
399 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
400 -- ^ When the number of proofs is different than
401 -- the number of 'Disjunction's.
402 deriving (Eq,Show)
403
404 -- * Type 'Question'
405 data Question = Question
406 { question_text :: !Text
407 , question_choices :: ![Text]
408 , question_mini :: !Natural
409 , question_maxi :: !Natural
410 -- , question_blank :: Maybe Bool
411 } deriving (Eq,Show,Generic,NFData)
412 instance ToJSON Question where
413 toJSON Question{..} =
414 JSON.object
415 [ "question" .= question_text
416 , "answers" .= question_choices
417 , "min" .= question_mini
418 , "max" .= question_maxi
419 ]
420 toEncoding Question{..} =
421 JSON.pairs
422 ( "question" .= question_text
423 <> "answers" .= question_choices
424 <> "min" .= question_mini
425 <> "max" .= question_maxi
426 )
427 instance FromJSON Question where
428 parseJSON = JSON.withObject "Question" $ \o -> do
429 question_text <- o .: "question"
430 question_choices <- o .: "answers"
431 question_mini <- o .: "min"
432 question_maxi <- o .: "max"
433 return Question{..}
434
435 -- * Type 'Answer'
436 data Answer c = Answer
437 { answer_opinions :: ![(Encryption c, DisjProof c)]
438 -- ^ Encrypted 'Opinion' for each 'question_choices'
439 -- with a 'DisjProof' that they belong to [0,1].
440 , answer_sumProof :: !(DisjProof c)
441 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
442 -- is an element of @[mini..maxi]@.
443 -- , answer_blankProof ::
444 } deriving (Eq,Show,Generic,NFData)
445 instance Reifies c FFC => ToJSON (Answer c) where
446 toJSON Answer{..} =
447 let (answer_choices, answer_individual_proofs) =
448 List.unzip answer_opinions in
449 JSON.object
450 [ "choices" .= answer_choices
451 , "individual_proofs" .= answer_individual_proofs
452 , "overall_proof" .= answer_sumProof
453 ]
454 toEncoding Answer{..} =
455 let (answer_choices, answer_individual_proofs) =
456 List.unzip answer_opinions in
457 JSON.pairs
458 ( "choices" .= answer_choices
459 <> "individual_proofs" .= answer_individual_proofs
460 <> "overall_proof" .= answer_sumProof
461 )
462 instance Reifies c FFC => FromJSON (Answer c) where
463 parseJSON = JSON.withObject "Answer" $ \o -> do
464 answer_choices <- o .: "choices"
465 answer_individual_proofs <- o .: "individual_proofs"
466 let answer_opinions = List.zip answer_choices answer_individual_proofs
467 answer_sumProof <- o .: "overall_proof"
468 return Answer{..}
469
470 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
471 -- returns an 'Answer' validable by 'verifyAnswer',
472 -- unless an 'ErrorAnswer' is returned.
473 encryptAnswer ::
474 Reifies c FFC =>
475 Monad m => RandomGen r =>
476 PublicKey c -> ZKP ->
477 Question -> [Bool] ->
478 S.StateT r (ExceptT ErrorAnswer m) (Answer c)
479 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
480 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
481 lift $ throwE $
482 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
483 | List.length opinions /= List.length question_choices =
484 lift $ throwE $
485 ErrorAnswer_WrongNumberOfOpinions
486 (fromIntegral $ List.length opinions)
487 (fromIntegral $ List.length question_choices)
488 | otherwise = do
489 encryptions <- encrypt elecPubKey `mapM` opinions
490 individualProofs <- zipWithM
491 (\opinion -> proveEncryption elecPubKey zkp $
492 if opinion
493 then (List.init booleanDisjunctions,[])
494 else ([],List.tail booleanDisjunctions))
495 opinionByChoice encryptions
496 sumProof <- proveEncryption elecPubKey zkp
497 (List.tail <$> List.genericSplitAt
498 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
499 (intervalDisjunctions question_mini question_maxi))
500 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
501 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
502 )
503 return $ Answer
504 { answer_opinions = List.zip
505 (snd <$> encryptions) -- NOTE: drop encNonce
506 individualProofs
507 , answer_sumProof = sumProof
508 }
509 where
510 opinionsSum = sum $ nat <$> opinions
511 opinions = (\o -> if o then one else zero) <$> opinionByChoice
512
513 verifyAnswer ::
514 Reifies c FFC =>
515 PublicKey c -> ZKP ->
516 Question -> Answer c -> Bool
517 verifyAnswer elecPubKey zkp Question{..} Answer{..}
518 | List.length question_choices /= List.length answer_opinions = False
519 | otherwise = either (const False) id $ runExcept $ do
520 validOpinions <-
521 verifyEncryption elecPubKey zkp booleanDisjunctions
522 `traverse` answer_opinions
523 validSum <- verifyEncryption elecPubKey zkp
524 (intervalDisjunctions question_mini question_maxi)
525 ( sum (fst <$> answer_opinions)
526 , answer_sumProof )
527 return (and validOpinions && validSum)
528
529 -- ** Type 'ErrorAnswer'
530 -- | Error raised by 'encryptAnswer'.
531 data ErrorAnswer
532 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
533 -- ^ When the number of opinions is different than
534 -- the number of choices ('question_choices').
535 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
536 -- ^ When the sum of opinions is not within the bounds
537 -- of 'question_mini' and 'question_maxi'.
538 deriving (Eq,Show,Generic,NFData)
539
540 -- * Type 'Election'
541 data Election c = Election
542 { election_name :: !Text
543 , election_description :: !Text
544 , election_crypto :: !(ElectionCrypto c)
545 , election_questions :: ![Question]
546 , election_uuid :: !UUID
547 , election_hash :: Base64SHA256
548 } deriving (Eq,Show,Generic,NFData)
549
550 instance ToJSON (Election c) where
551 toJSON Election{..} =
552 JSON.object
553 [ "name" .= election_name
554 , "description" .= election_description
555 , "public_key" .= election_crypto
556 , "questions" .= election_questions
557 , "uuid" .= election_uuid
558 ]
559 toEncoding Election{..} =
560 JSON.pairs
561 ( "name" .= election_name
562 <> "description" .= election_description
563 <> "public_key" .= election_crypto
564 <> "questions" .= election_questions
565 <> "uuid" .= election_uuid
566 )
567 instance FromJSON (Election ()) where
568 parseJSON = JSON.withObject "Election" $ \o -> Election
569 <$> o .: "name"
570 <*> o .: "description"
571 <*> o .: "public_key"
572 <*> o .: "questions"
573 <*> o .: "uuid"
574 <*> pure (Base64SHA256 "")
575 -- NOTE: set in 'readElection'.
576
577 readElection :: FilePath -> ExceptT String IO (Election ())
578 readElection filePath = do
579 fileData <- lift $ BS.readFile filePath
580 ExceptT $ return $
581 (\e -> e{election_hash=base64SHA256 fileData})
582 <$> JSON.eitherDecodeStrict' fileData
583
584 hashElection :: Election c -> Base64SHA256
585 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
586
587 -- ** Type 'ElectionCrypto'
588 data ElectionCrypto c =
589 ElectionCrypto_FFC
590 { electionCrypto_FFC_params :: !FFC
591 , electionCrypto_FFC_PublicKey :: !(PublicKey c)
592 } deriving (Eq,Show,Generic,NFData)
593
594 reifyElection :: Election () -> (forall c. Reifies c FFC => Election c -> k) -> k
595 reifyElection Election{..} k =
596 case election_crypto of
597 ElectionCrypto_FFC ffc (G (F pubKey)) ->
598 reify ffc $ \(_::Proxy c) -> k @c
599 Election{election_crypto = ElectionCrypto_FFC ffc (G (F pubKey)), ..}
600
601 instance ToJSON (ElectionCrypto c) where
602 toJSON (ElectionCrypto_FFC ffc pubKey) =
603 JSON.object
604 [ "group" .= ffc
605 , "y" .= pubKey
606 ]
607 toEncoding (ElectionCrypto_FFC ffc pubKey) =
608 JSON.pairs
609 ( "group" .= ffc
610 <> "y" .= pubKey
611 )
612 instance FromJSON (ElectionCrypto ()) where
613 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
614 ffc <- o .: "group"
615 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
616 return $ ElectionCrypto_FFC ffc (G (F pubKey))
617
618 -- * Type 'Ballot'
619 data Ballot c = Ballot
620 { ballot_answers :: ![Answer c]
621 , ballot_signature :: !(Maybe (Signature c))
622 , ballot_election_uuid :: !UUID
623 , ballot_election_hash :: !Base64SHA256
624 } deriving (Generic,NFData)
625 instance Reifies c FFC => ToJSON (Ballot c) where
626 toJSON Ballot{..} =
627 JSON.object $
628 [ "answers" .= ballot_answers
629 , "election_uuid" .= ballot_election_uuid
630 , "election_hash" .= ballot_election_hash
631 ] <>
632 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
633 toEncoding Ballot{..} =
634 JSON.pairs $
635 ( "answers" .= ballot_answers
636 <> "election_uuid" .= ballot_election_uuid
637 <> "election_hash" .= ballot_election_hash
638 ) <>
639 maybe mempty (\sig -> "signature" .= sig) ballot_signature
640 instance Reifies c FFC => FromJSON (Ballot c) where
641 parseJSON = JSON.withObject "Ballot" $ \o -> do
642 ballot_answers <- o .: "answers"
643 ballot_signature <- o .:? "signature"
644 ballot_election_uuid <- o .: "election_uuid"
645 ballot_election_hash <- o .: "election_hash"
646 return Ballot{..}
647
648 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
649 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
650 -- where 'opinionsByQuest' is a list of 'Opinion's
651 -- on each 'question_choices' of each 'election_questions'.
652 encryptBallot ::
653 Reifies c FFC =>
654 Monad m => RandomGen r =>
655 Election c ->
656 Maybe (SecretKey c) -> [[Bool]] ->
657 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
658 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
659 | List.length election_questions /= List.length opinionsByQuest =
660 lift $ throwE $
661 ErrorBallot_WrongNumberOfAnswers
662 (fromIntegral $ List.length opinionsByQuest)
663 (fromIntegral $ List.length election_questions)
664 | otherwise = do
665 let (voterKeys, voterZKP) =
666 case ballotSecKeyMay of
667 Nothing -> (Nothing, ZKP "")
668 Just ballotSecKey ->
669 ( Just (ballotSecKey, ballotPubKey)
670 , ZKP (bytesNat ballotPubKey) )
671 where ballotPubKey = publicKey ballotSecKey
672 ballot_answers <-
673 S.mapStateT (withExceptT ErrorBallot_Answer) $
674 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
675 election_questions opinionsByQuest
676 ballot_signature <- case voterKeys of
677 Nothing -> return Nothing
678 Just (ballotSecKey, signature_publicKey) -> do
679 signature_proof <-
680 proveQuicker ballotSecKey (Identity groupGen) $
681 \(Identity commitment) ->
682 hash
683 -- NOTE: the order is unusual, the commitments are first
684 -- then comes the statement. Best guess is that
685 -- this is easier to code due to their respective types.
686 (signatureCommitments voterZKP commitment)
687 (signatureStatement ballot_answers)
688 return $ Just Signature{..}
689 return Ballot
690 { ballot_answers
691 , ballot_election_hash = election_hash
692 , ballot_election_uuid = election_uuid
693 , ballot_signature
694 }
695
696 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
697 verifyBallot Election{..} Ballot{..} =
698 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
699 ballot_election_uuid == election_uuid &&
700 ballot_election_hash == election_hash &&
701 List.length election_questions == List.length ballot_answers &&
702 let (isValidSign, zkpSign) =
703 case ballot_signature of
704 Nothing -> (True, ZKP "")
705 Just Signature{..} ->
706 let zkp = ZKP (bytesNat signature_publicKey) in
707 (, zkp) $
708 proof_challenge signature_proof == hash
709 (signatureCommitments zkp (commitQuicker signature_proof groupGen signature_publicKey))
710 (signatureStatement ballot_answers)
711 in
712 and $ isValidSign :
713 List.zipWith (verifyAnswer elecPubKey zkpSign)
714 election_questions ballot_answers
715
716 -- ** Type 'Signature'
717 -- | Schnorr-like signature.
718 --
719 -- Used by each voter to sign his/her encrypted 'Ballot'
720 -- using his/her 'Credential',
721 -- in order to avoid ballot stuffing.
722 data Signature c = Signature
723 { signature_publicKey :: !(PublicKey c)
724 -- ^ Verification key.
725 , signature_proof :: !(Proof c)
726 } deriving (Generic,NFData)
727 instance Reifies c FFC => ToJSON (Signature c) where
728 toJSON (Signature pubKey Proof{..}) =
729 JSON.object
730 [ "public_key" .= pubKey
731 , "challenge" .= proof_challenge
732 , "response" .= proof_response
733 ]
734 toEncoding (Signature pubKey Proof{..}) =
735 JSON.pairs
736 ( "public_key" .= pubKey
737 <> "challenge" .= proof_challenge
738 <> "response" .= proof_response
739 )
740 instance Reifies c FFC => FromJSON (Signature c) where
741 parseJSON = JSON.withObject "Signature" $ \o -> do
742 signature_publicKey <- o .: "public_key"
743 proof_challenge <- o .: "challenge"
744 proof_response <- o .: "response"
745 let signature_proof = Proof{..}
746 return Signature{..}
747
748 -- *** Hashing
749
750 -- | @('signatureStatement' answers)@
751 -- returns the encrypted material to be signed:
752 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
753 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
754 signatureStatement =
755 foldMap $ \Answer{..} ->
756 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
757 [encryption_nonce, encryption_vault]
758
759 -- | @('signatureCommitments' voterZKP commitment)@
760 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
761 signatureCommitments (ZKP voterZKP) commitment =
762 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
763 <> bytesNat commitment<>"|"
764
765 -- ** Type 'ErrorBallot'
766 -- | Error raised by 'encryptBallot'.
767 data ErrorBallot
768 = ErrorBallot_WrongNumberOfAnswers Natural Natural
769 -- ^ When the number of answers
770 -- is different than the number of questions.
771 | ErrorBallot_Answer ErrorAnswer
772 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
773 | ErrorBallot_Wrong
774 -- ^ TODO: to be more precise.
775 deriving (Eq,Show,Generic,NFData)