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