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