]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: use Belenios' JSON schema
[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 c) 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" .= nat pubKey
523 ]
524 toEncoding (ElectionCrypto_FFC ffc pubKey) =
525 JSON.pairs
526 ( "group" .= ffc
527 <> "y" .= nat pubKey
528 )
529 instance FromJSON (ElectionCrypto c) where
530 parseJSON = JSON.withObject "ElectionCrypto" $ \o -> do
531 ffc <- o .: "group"
532 pubKey <- reify ffc $ \(_::Proxy s) -> nat <$> ((.:) @(PublicKey s) o "y")
533 {-
534 unless (nat ffc_groupGen < ffc_fieldCharac) $
535 JSON.typeMismatch "FFC: groupGen is not lower than fieldCharac" (JSON.Object o)
536 -}
537 return $ ElectionCrypto_FFC ffc (G (F pubKey))
538
539
540 -- ** Type 'Hash'
541 newtype Hash = Hash Text
542 deriving (Eq,Ord,Show,Generic)
543 deriving anyclass (ToJSON,FromJSON)
544 deriving newtype NFData
545
546 hashJSON :: ToJSON a => a -> Hash
547 hashJSON = Hash . TL.toStrict . TL.decodeUtf8 . BSL64.encode . JSON.encode
548
549 hashElection :: Election c -> Election c
550 hashElection elec = elec{election_hash=hashJSON elec}
551
552 -- * Type 'Ballot'
553 data Ballot c = Ballot
554 { ballot_answers :: ![Answer c]
555 , ballot_signature :: !(Maybe (Signature c))
556 , ballot_election_uuid :: !UUID
557 , ballot_election_hash :: !Hash
558 } deriving (Generic,NFData)
559 deriving instance Reifies c FFC => ToJSON (Ballot c)
560 instance Reifies c FFC => FromJSON (Ballot c) where
561 parseJSON = JSON.withObject "Ballot" $ \o -> do
562 ballot_answers <- o .: "answers"
563 ballot_signature <- o .:? "signature"
564 ballot_election_uuid <- o .: "election_uuid"
565 ballot_election_hash <- o .: "election_hash"
566 return Ballot{..}
567
568 -- | @('encryptBallot' elec ('Just' ballotSecKey) opinionsByQuest)@
569 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
570 -- where 'opinionsByQuest' is a list of 'Opinion's
571 -- on each 'question_choices' of each 'election_questions'.
572 encryptBallot ::
573 Reifies c FFC =>
574 Monad m => RandomGen r =>
575 Election c ->
576 Maybe (SecretKey c) -> [[Bool]] ->
577 S.StateT r (ExceptT ErrorBallot m) (Ballot c)
578 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
579 | List.length election_questions /= List.length opinionsByQuest =
580 lift $ throwE $
581 ErrorBallot_WrongNumberOfAnswers
582 (fromIntegral $ List.length opinionsByQuest)
583 (fromIntegral $ List.length election_questions)
584 | otherwise = do
585 let (voterKeys, voterZKP) =
586 case ballotSecKeyMay of
587 Nothing -> (Nothing, ZKP "")
588 Just ballotSecKey ->
589 ( Just (ballotSecKey, ballotPubKey)
590 , ZKP (bytesNat ballotPubKey) )
591 where ballotPubKey = publicKey ballotSecKey
592 ballot_answers <-
593 S.mapStateT (withExceptT ErrorBallot_Answer) $
594 zipWithM (encryptAnswer (electionCrypto_FFC_PublicKey election_crypto) voterZKP)
595 election_questions opinionsByQuest
596 ballot_signature <- case voterKeys of
597 Nothing -> return Nothing
598 Just (ballotSecKey, signature_publicKey) -> do
599 signature_proof <-
600 prove ballotSecKey (Identity groupGen) $
601 \(Identity commitment) ->
602 hash
603 -- NOTE: the order is unusual, the commitments are first
604 -- then comes the statement. Best guess is that
605 -- this is easier to code due to their respective types.
606 (signatureCommitments voterZKP commitment)
607 (signatureStatement ballot_answers)
608 return $ Just Signature{..}
609 return Ballot
610 { ballot_answers
611 , ballot_election_hash = election_hash
612 , ballot_election_uuid = election_uuid
613 , ballot_signature
614 }
615
616 verifyBallot :: Reifies c FFC => Election c -> Ballot c -> Bool
617 verifyBallot Election{..} Ballot{..} =
618 let elecPubKey = electionCrypto_FFC_PublicKey election_crypto in
619 ballot_election_uuid == election_uuid &&
620 ballot_election_hash == election_hash &&
621 List.length election_questions == List.length ballot_answers &&
622 let (isValidSign, zkpSign) =
623 case ballot_signature of
624 Nothing -> (True, ZKP "")
625 Just Signature{..} ->
626 let zkp = ZKP (bytesNat signature_publicKey) in
627 (, zkp) $
628 proof_challenge signature_proof == hash
629 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
630 (signatureStatement ballot_answers)
631 in
632 and $ isValidSign :
633 List.zipWith (verifyAnswer elecPubKey zkpSign)
634 election_questions ballot_answers
635
636 -- ** Type 'Signature'
637 -- | Schnorr-like signature.
638 --
639 -- Used by each voter to sign his/her encrypted 'Ballot'
640 -- using his/her 'Credential',
641 -- in order to avoid ballot stuffing.
642 data Signature c = Signature
643 { signature_publicKey :: !(PublicKey c)
644 -- ^ Verification key.
645 , signature_proof :: !(Proof c)
646 } deriving (Generic,NFData)
647 deriving instance Reifies c FFC => ToJSON (Signature c)
648 instance Reifies c FFC => FromJSON (Signature c) where
649 parseJSON = JSON.withObject "Signature" $ \o -> do
650 signature_publicKey <- o .: "public_key"
651 proof_challenge <- o .: "challenge"
652 proof_response <- o .: "response"
653 let signature_proof = Proof{..}
654 return Signature{..}
655
656 -- *** Hashing
657
658 -- | @('signatureStatement' answers)@
659 -- returns the encrypted material to be signed:
660 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
661 signatureStatement :: Reifies c FFC => Foldable f => f (Answer c) -> [G c]
662 signatureStatement =
663 foldMap $ \Answer{..} ->
664 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
665 [encryption_nonce, encryption_vault]
666
667 -- | @('signatureCommitments' voterZKP commitment)@
668 signatureCommitments :: ZKP -> Commitment c -> BS.ByteString
669 signatureCommitments (ZKP voterZKP) commitment =
670 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
671 <> bytesNat commitment<>"|"
672
673 -- ** Type 'ErrorBallot'
674 -- | Error raised by 'encryptBallot'.
675 data ErrorBallot
676 = ErrorBallot_WrongNumberOfAnswers Natural Natural
677 -- ^ When the number of answers
678 -- is different than the number of questions.
679 | ErrorBallot_Answer ErrorAnswer
680 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
681 | ErrorBallot_Wrong
682 -- ^ TODO: to be more precise.
683 deriving (Eq,Show,Generic,NFData)