1 {-# LANGUAGE DeriveAnyClass #-}
2 {-# LANGUAGE DeriveGeneric #-}
3 {-# LANGUAGE DerivingStrategies #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE Rank2Types #-} -- for readElection
6 {-# LANGUAGE UndecidableInstances #-} -- for Reifies instances
7 module Voting.Protocol.Election where
9 import Control.Applicative (Applicative(..), Alternative(..))
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(..),(.:),(.:?),(.=))
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, fromMaybe, listToMaybe)
23 import Data.Monoid (Monoid(..))
24 import Data.Ord (Ord(..))
25 import Data.Proxy (Proxy(..))
26 import Data.Reflection (Reifies(..), reify)
27 import Data.Semigroup (Semigroup(..))
28 import Data.String (String, IsString(..))
29 import Data.Text (Text)
30 import Data.Traversable (Traversable(..))
31 import Data.Tuple (fst, snd)
32 import GHC.Generics (Generic)
33 import GHC.Natural (minusNaturalMaybe)
34 import Numeric.Natural (Natural)
35 import Prelude (fromIntegral)
36 import System.IO (IO, FilePath)
37 import System.Random (RandomGen)
38 import Text.Show (Show(..), showChar, showString)
39 import qualified Control.Monad.Trans.State.Strict as S
40 import qualified Data.Aeson as JSON
41 import qualified Data.Aeson.Encoding as JSON
42 import qualified Data.Aeson.Internal as JSON
43 import qualified Data.Aeson.Parser.Internal as JSON
44 import qualified Data.Aeson.Types as JSON
45 import qualified Data.ByteString as BS
46 import qualified Data.ByteString.Lazy as BSL
47 import qualified Data.Char as Char
48 import qualified Data.List as List
49 import qualified Data.Text as Text
50 import qualified Text.ParserCombinators.ReadP as Read
51 import qualified Text.Read as Read
53 import Voting.Protocol.Utils
54 import Voting.Protocol.Arith
55 import Voting.Protocol.Credential
57 -- * Type 'Encryption'
58 -- | ElGamal-like encryption.
59 -- Its security relies on the /Discrete Logarithm problem/.
61 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
62 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
63 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
64 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
65 -- to enable the additive homomorphism.
67 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
68 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
69 data Encryption crypto v c = Encryption
70 { encryption_nonce :: !(G crypto c)
71 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
72 -- equal to @('groupGen' '^'encNonce)@
73 , encryption_vault :: !(G crypto c)
74 -- ^ Encrypted 'clear' text,
75 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
77 deriving instance Eq (G crypto c) => Eq (Encryption crypto v c)
78 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Encryption crypto v c)
79 deriving instance NFData (G crypto c) => NFData (Encryption crypto v c)
84 ) => ToJSON (Encryption crypto v c) where
85 toJSON Encryption{..} =
87 [ "alpha" .= encryption_nonce
88 , "beta" .= encryption_vault
90 toEncoding Encryption{..} =
92 ( "alpha" .= encryption_nonce
93 <> "beta" .= encryption_vault
98 , FromJSON (G crypto c)
99 ) => FromJSON (Encryption crypto v c) where
100 parseJSON = JSON.withObject "Encryption" $ \o -> do
101 encryption_nonce <- o .: "alpha"
102 encryption_vault <- o .: "beta"
103 return Encryption{..}
105 -- | Additive homomorphism.
106 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
109 , Multiplicative (G crypto c)
110 ) => Additive (Encryption crypto v c) where
111 zero = Encryption one one
113 (encryption_nonce x * encryption_nonce y)
114 (encryption_vault x * encryption_vault y)
116 -- *** Type 'EncryptionNonce'
117 type EncryptionNonce = E
119 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
121 -- WARNING: the secret encryption nonce (@encNonce@)
122 -- is returned alongside the 'Encryption'
123 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
124 -- but this secret @encNonce@ MUST be forgotten after that,
125 -- as it may be used to decipher the 'Encryption'
126 -- without the 'SecretKey' associated with 'pubKey'.
131 Multiplicative (G crypto c) =>
132 Monad m => RandomGen r =>
133 PublicKey crypto c -> E crypto c ->
134 S.StateT r m (EncryptionNonce crypto c, Encryption crypto v c)
135 encrypt pubKey clear = do
137 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
140 { encryption_nonce = groupGen^encNonce
141 , encryption_vault = pubKey ^encNonce * groupGen^clear
145 -- | Non-Interactive Zero-Knowledge 'Proof'
146 -- of knowledge of a discrete logarithm:
147 -- @(secret == logBase base (base^secret))@.
148 data Proof crypto v c = Proof
149 { proof_challenge :: !(Challenge crypto c)
150 -- ^ 'Challenge' sent by the verifier to the prover
151 -- to ensure that the prover really has knowledge
152 -- of the secret and is not replaying.
153 -- Actually, 'proof_challenge' is not sent to the prover,
154 -- but derived from the prover's 'Commitment's and statements
155 -- with a collision resistant 'hash'.
156 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
157 , proof_response :: !(E crypto c)
158 -- ^ A discrete logarithm sent by the prover to the verifier,
159 -- as a response to 'proof_challenge'.
161 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
163 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
164 -- * @commitment '==' 'commit' proof base basePowSec '=='
165 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
166 -- * and @basePowSec '==' base'^'sec@,
168 -- then, with overwhelming probability (due to the 'hash' function),
169 -- the prover was not able to choose 'proof_challenge'
170 -- yet was able to compute a 'proof_response' such that
171 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
172 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
173 -- therefore the prover knows 'sec'.
175 -- The prover choses 'commitment' to be a random power of @base@,
176 -- to ensure that each 'prove' does not reveal any information
178 } deriving (Eq,Show,NFData,Generic)
179 instance Group crypto => ToJSON (Proof crypto v c) where
182 [ "challenge" .= proof_challenge
183 , "response" .= proof_response
185 toEncoding Proof{..} =
187 ( "challenge" .= proof_challenge
188 <> "response" .= proof_response
190 instance (Reifies c crypto, Group crypto) => FromJSON (Proof crypto v c) where
191 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
192 proof_challenge <- o .: "challenge"
193 proof_response <- o .: "response"
197 -- | Zero-knowledge proof.
199 -- A protocol is /zero-knowledge/ if the verifier
200 -- learns nothing from the protocol except that the prover
203 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
204 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
205 newtype ZKP = ZKP BS.ByteString
207 -- ** Type 'Challenge'
211 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
212 -- by 'hash'ing them (eventually with other 'Commitment's).
214 -- Used in 'prove' it enables a Fiat-Shamir transformation
215 -- of an /interactive zero-knowledge/ (IZK) proof
216 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
217 -- That is to say that the verifier does not have
218 -- to send a 'Challenge' to the prover.
219 -- Indeed, the prover now handles the 'Challenge'
220 -- which becomes a (collision resistant) 'hash'
221 -- of the prover's commitments (and statements to be a stronger proof).
222 type Oracle list crypto c = list (Commitment crypto c) -> Challenge crypto c
224 -- | @('prove' sec commitmentBases oracle)@
225 -- returns a 'Proof' that @sec@ is known
226 -- (by proving the knowledge of its discrete logarithm).
228 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
229 -- raised to the power of the secret nonce of the 'Proof',
230 -- as those are the 'Commitment's that the verifier will obtain
231 -- when composing the 'proof_challenge' and 'proof_response' together
234 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
235 -- the statement must be included in the 'hash' (along with the commitments).
237 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
238 -- does not reveal any information regarding the secret @sec@,
239 -- because two 'Proof's using the same 'Commitment'
240 -- can be used to deduce @sec@ (using the special-soundness).
242 forall crypto v c list m r.
246 Multiplicative (G crypto c) =>
247 Monad m => RandomGen r => Functor list =>
250 Oracle list crypto c ->
251 S.StateT r m (Proof crypto v c)
252 prove sec commitmentBases oracle = do
254 let commitments = (^ nonce) <$> commitmentBases
255 let proof_challenge = oracle commitments
258 , proof_response = nonce `op` (sec*proof_challenge)
261 -- | See comments in 'commit'.
263 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
267 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
268 -- when Helios-C specifications will be fixed.
273 Multiplicative (G crypto c) =>
274 Monad m => RandomGen r => Functor list =>
277 Oracle list crypto c ->
278 S.StateT r m (Proof crypto v c)
279 proveQuicker sec commitmentBases oracle = do
281 let commitments = (^ nonce) <$> commitmentBases
282 let proof_challenge = oracle commitments
285 , proof_response = nonce - sec*proof_challenge
288 -- | @('fakeProof')@ returns a 'Proof'
289 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
290 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
291 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
292 -- as a 'Proof' returned by 'prove'.
294 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
295 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
299 Monad m => RandomGen r =>
300 S.StateT r m (Proof crypto v c)
302 proof_challenge <- random
303 proof_response <- random
306 -- ** Type 'Commitment'
307 -- | A commitment from the prover to the verifier.
308 -- It's a power of 'groupGen' chosen randomly by the prover
309 -- when making a 'Proof' with 'prove'.
312 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
313 -- from the given 'Proof' with the knowledge of the verifier.
319 Multiplicative (G crypto c) =>
320 Invertible (G crypto c) =>
325 commit Proof{..} base basePowSec =
326 (base^proof_response) `op`
327 (basePowSec^proof_challenge)
330 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
333 -- TODO: contrary to some textbook presentations,
334 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
335 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
336 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
337 {-# INLINE commit #-}
339 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
340 -- when Helios-C specifications will be fixed.
344 Multiplicative (G crypto c) =>
349 commitQuicker Proof{..} base basePowSec =
350 base^proof_response *
351 basePowSec^proof_challenge
353 -- * Type 'Disjunction'
354 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
355 -- it's used in 'proveEncryption' to generate a 'Proof'
356 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
359 booleanDisjunctions ::
363 Invertible (G crypto c) =>
364 [Disjunction crypto c]
365 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
367 intervalDisjunctions ::
371 Invertible (G crypto c) =>
372 Natural -> Natural -> [Disjunction crypto c]
373 intervalDisjunctions mini maxi =
374 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
375 List.genericDrop (nat mini) $
376 groupGenInverses @crypto
379 -- | Index of a 'Disjunction' within a list of them.
380 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
383 -- ** Type 'DisjProof'
384 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
385 -- is indexing a 'Disjunction' within a list of them,
386 -- without revealing which 'Opinion' it is.
387 newtype DisjProof crypto v c = DisjProof [Proof crypto v c]
388 deriving (Eq,Show,Generic)
389 deriving newtype (NFData,ToJSON,FromJSON)
391 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
392 -- returns a 'DisjProof' that 'enc' 'encrypt's
393 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
395 -- The prover proves that it knows an 'encNonce', such that:
396 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
398 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
400 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
405 ToNatural (G crypto c) =>
406 Multiplicative (G crypto c) =>
407 Invertible (G crypto c) =>
408 Monad m => RandomGen r =>
409 PublicKey crypto c -> ZKP ->
410 ([Disjunction crypto c],[Disjunction crypto c]) ->
411 (EncryptionNonce crypto c, Encryption crypto v c) ->
412 S.StateT r m (DisjProof crypto v c)
413 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
414 -- Fake proofs for all 'Disjunction's except the genuine one.
415 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
416 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
417 let fakeChallengeSum =
418 sum (proof_challenge <$> prevFakeProofs) +
419 sum (proof_challenge <$> nextFakeProofs)
420 let statement = encryptionStatement voterZKP enc
421 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
422 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
423 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
424 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
425 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
426 let challenge = hash statement commitments in
427 let genuineChallenge = challenge - fakeChallengeSum in
429 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
430 -- thus (sum (proof_challenge <$> proofs) == challenge)
431 -- as checked in 'verifyEncryption'.
432 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
433 return (DisjProof proofs)
439 ToNatural (G crypto c) =>
440 Multiplicative (G crypto c) =>
441 Invertible (G crypto c) =>
443 PublicKey crypto c -> ZKP ->
444 [Disjunction crypto c] -> (Encryption crypto v c, DisjProof crypto v c) ->
445 ExceptT ErrorVerifyEncryption m Bool
446 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
447 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
449 throwE $ ErrorVerifyEncryption_InvalidProofLength
450 (fromIntegral $ List.length proofs)
451 (fromIntegral $ List.length disjs)
453 return $ challengeSum ==
454 hash (encryptionStatement voterZKP enc) (join commitments)
456 challengeSum = sum (proof_challenge <$> proofs)
459 encryptionStatement ::
461 ToNatural (G crypto c) =>
462 ZKP -> Encryption crypto v c -> BS.ByteString
463 encryptionStatement (ZKP voterZKP) Encryption{..} =
464 "prove|"<>voterZKP<>"|"
465 <> bytesNat encryption_nonce<>","
466 <> bytesNat encryption_vault<>"|"
468 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
469 -- returns the 'Commitment's with only the knowledge of the verifier.
471 -- For the prover the 'Proof' comes from @fakeProof@,
472 -- and for the verifier the 'Proof' comes from the prover.
473 encryptionCommitments ::
477 Invertible (G crypto c) =>
478 PublicKey crypto c -> Encryption crypto v c ->
479 Disjunction crypto c -> Proof crypto v c -> [G crypto c]
480 encryptionCommitments elecPubKey Encryption{..} disj proof =
481 [ commit proof groupGen encryption_nonce
482 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
483 -- base==groupGen, basePowSec==groupGen^encNonce.
484 , commit proof elecPubKey (encryption_vault*disj)
485 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
486 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
487 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
490 -- ** Type 'ErrorVerifyEncryption'
491 -- | Error raised by 'verifyEncryption'.
492 data ErrorVerifyEncryption
493 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
494 -- ^ When the number of proofs is different than
495 -- the number of 'Disjunction's.
499 data Question v = Question
500 { question_text :: !Text
501 , question_choices :: ![Text]
502 , question_mini :: !Natural
503 , question_maxi :: !Natural
504 -- , question_blank :: Maybe Bool
505 } deriving (Eq,Show,Generic,NFData)
506 instance Reifies v Version => ToJSON (Question v) where
507 toJSON Question{..} =
509 [ "question" .= question_text
510 , "answers" .= question_choices
511 , "min" .= question_mini
512 , "max" .= question_maxi
514 toEncoding Question{..} =
516 ( "question" .= question_text
517 <> "answers" .= question_choices
518 <> "min" .= question_mini
519 <> "max" .= question_maxi
521 instance Reifies v Version => FromJSON (Question v) where
522 parseJSON = JSON.withObject "Question" $ \o -> do
523 question_text <- o .: "question"
524 question_choices <- o .: "answers"
525 question_mini <- o .: "min"
526 question_maxi <- o .: "max"
530 data Answer crypto v c = Answer
531 { answer_opinions :: ![(Encryption crypto v c, DisjProof crypto v c)]
532 -- ^ Encrypted 'Opinion' for each 'question_choices'
533 -- with a 'DisjProof' that they belong to [0,1].
534 , answer_sumProof :: !(DisjProof crypto v c)
535 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
536 -- is an element of @[mini..maxi]@.
537 -- , answer_blankProof ::
539 deriving instance Eq (G crypto c) => Eq (Answer crypto v c)
540 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Answer crypto v c)
541 deriving instance NFData (G crypto c) => NFData (Answer crypto v c)
545 , ToJSON (G crypto c)
547 ) => ToJSON (Answer crypto v c) where
549 let (answer_choices, answer_individual_proofs) =
550 List.unzip answer_opinions in
552 [ "choices" .= answer_choices
553 , "individual_proofs" .= answer_individual_proofs
554 , "overall_proof" .= answer_sumProof
556 toEncoding Answer{..} =
557 let (answer_choices, answer_individual_proofs) =
558 List.unzip answer_opinions in
560 ( "choices" .= answer_choices
561 <> "individual_proofs" .= answer_individual_proofs
562 <> "overall_proof" .= answer_sumProof
567 , FromJSON (G crypto c)
569 ) => FromJSON (Answer crypto v c) where
570 parseJSON = JSON.withObject "Answer" $ \o -> do
571 answer_choices <- o .: "choices"
572 answer_individual_proofs <- o .: "individual_proofs"
573 let answer_opinions = List.zip answer_choices answer_individual_proofs
574 answer_sumProof <- o .: "overall_proof"
577 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
578 -- returns an 'Answer' validable by 'verifyAnswer',
579 -- unless an 'ErrorAnswer' is returned.
582 Reifies c crypto => Group crypto =>
583 Monad m => RandomGen r =>
584 PublicKey crypto c -> ZKP ->
585 Question v -> [Bool] ->
586 S.StateT r (ExceptT ErrorAnswer m) (Answer crypto v c)
587 encryptAnswer (elecPubKey::PublicKey crypto c) zkp Question{..} opinionByChoice
588 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
590 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
591 | List.length opinions /= List.length question_choices =
593 ErrorAnswer_WrongNumberOfOpinions
594 (fromIntegral $ List.length opinions)
595 (fromIntegral $ List.length question_choices)
596 | otherwise, Dict <- groupDict (Proxy @c) = do
597 encryptions <- encrypt elecPubKey `mapM` opinions
598 individualProofs <- zipWithM
599 (\opinion -> proveEncryption elecPubKey zkp $
601 then (List.init booleanDisjunctions,[])
602 else ([],List.tail booleanDisjunctions))
603 opinionByChoice encryptions
604 sumProof <- proveEncryption elecPubKey zkp
605 (List.tail <$> List.genericSplitAt
606 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
607 (intervalDisjunctions question_mini question_maxi))
608 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
609 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
612 { answer_opinions = List.zip
613 (snd <$> encryptions) -- NOTE: drop encNonce
615 , answer_sumProof = sumProof
618 opinionsSum = sum $ nat <$> opinions
619 opinions = (\o -> if o then one else zero) <$> opinionByChoice
623 Reifies c crypto => Group crypto =>
624 PublicKey crypto c -> ZKP ->
625 Question v -> Answer crypto v c -> Bool
626 verifyAnswer (elecPubKey::PublicKey crypto c) zkp Question{..} Answer{..}
627 | List.length question_choices /= List.length answer_opinions = False
628 | otherwise, Dict <- groupDict (Proxy @c) =
629 either (const False) id $ runExcept $ do
631 verifyEncryption elecPubKey zkp booleanDisjunctions
632 `traverse` answer_opinions
633 validSum <- verifyEncryption elecPubKey zkp
634 (intervalDisjunctions question_mini question_maxi)
635 ( sum (fst <$> answer_opinions)
637 return (and validOpinions && validSum)
639 -- ** Type 'ErrorAnswer'
640 -- | Error raised by 'encryptAnswer'.
642 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
643 -- ^ When the number of opinions is different than
644 -- the number of choices ('question_choices').
645 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
646 -- ^ When the sum of opinions is not within the bounds
647 -- of 'question_mini' and 'question_maxi'.
648 deriving (Eq,Show,Generic,NFData)
651 data Election crypto v c = Election
652 { election_name :: !Text
653 , election_description :: !Text
654 , election_questions :: ![Question v]
655 , election_uuid :: !UUID
656 , election_hash :: Base64SHA256
657 , election_crypto :: !crypto
658 , election_version :: !(Maybe Version)
659 , election_public_key :: !(PublicKey crypto c)
661 deriving instance (Eq crypto, Eq (G crypto c)) => Eq (Election crypto v c)
662 deriving instance (Show crypto, Show (G crypto c)) => Show (Election crypto v c)
663 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Election crypto v c)
666 , ToJSON (G crypto c)
669 ) => ToJSON (Election crypto v c) where
670 toJSON Election{..} =
672 [ "name" .= election_name
673 , "description" .= election_description
674 , ("public_key", JSON.object
675 [ "group" .= election_crypto
676 , "y" .= election_public_key
678 , "questions" .= election_questions
679 , "uuid" .= election_uuid
681 maybe [] (\version -> [ "version" .= version ]) election_version
682 toEncoding Election{..} =
684 ( "name" .= election_name
685 <> "description" .= election_description
686 <> JSON.pair "public_key" (JSON.pairs $
687 "group" .= election_crypto
688 <> "y" .= election_public_key
690 <> "questions" .= election_questions
691 <> "uuid" .= election_uuid
693 maybe mempty ("version" .=) election_version
702 GroupDict crypto c =>
703 Election crypto v c -> r) ->
705 readElection filePath k = do
706 fileData <- lift $ BS.readFile filePath
708 jsonEitherFormatError $
709 JSON.eitherDecodeStrictWith JSON.jsonEOF
710 (JSON.iparse (parseElection fileData))
713 parseElection fileData = JSON.withObject "Election" $ \o -> do
714 election_version <- o .:? "version"
715 reify (fromMaybe stableVersion election_version) $ \(_v::Proxy v) -> do
716 (election_crypto, elecPubKey) <-
717 JSON.explicitParseField
718 (JSON.withObject "public_key" $ \obj -> do
719 crypto <- obj .: "group"
720 pubKey :: JSON.Value <- obj .: "y"
721 return (crypto, pubKey)
723 reify election_crypto $ \case
724 (c::Proxy c) | Dict <- groupDict c -> do
725 election_name <- o .: "name"
726 election_description <- o .: "description"
727 election_questions <- o .: "questions" :: JSON.Parser [Question v]
728 election_uuid <- o .: "uuid"
729 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
730 return $ k $ Election
731 { election_questions = election_questions
732 , election_public_key = election_public_key
733 , election_hash = base64SHA256 fileData
741 ToJSON (G crypto c) =>
742 Election crypto v c -> Base64SHA256
743 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
746 data Ballot crypto v c = Ballot
747 { ballot_answers :: ![Answer crypto v c]
748 , ballot_signature :: !(Maybe (Signature crypto v c))
749 , ballot_election_uuid :: !UUID
750 , ballot_election_hash :: !Base64SHA256
752 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
757 , ToJSON (G crypto c)
758 ) => ToJSON (Ballot crypto v c) where
761 [ "answers" .= ballot_answers
762 , "election_uuid" .= ballot_election_uuid
763 , "election_hash" .= ballot_election_hash
765 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
766 toEncoding Ballot{..} =
768 ( "answers" .= ballot_answers
769 <> "election_uuid" .= ballot_election_uuid
770 <> "election_hash" .= ballot_election_hash
772 maybe mempty ("signature" .=) ballot_signature
777 , FromJSON (G crypto c)
778 ) => FromJSON (Ballot crypto v c) where
779 parseJSON = JSON.withObject "Ballot" $ \o -> do
780 ballot_answers <- o .: "answers"
781 ballot_signature <- o .:? "signature"
782 ballot_election_uuid <- o .: "election_uuid"
783 ballot_election_hash <- o .: "election_hash"
786 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
787 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
788 -- where 'opinionsByQuest' is a list of 'Opinion's
789 -- on each 'question_choices' of each 'election_questions'.
792 Reifies c crypto => Group crypto => Key crypto =>
793 Monad m => RandomGen r =>
794 Election crypto v c ->
795 Maybe (SecretKey crypto c) -> [[Bool]] ->
796 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
797 encryptBallot (Election{..}::Election crypto v c) ballotSecKeyMay opinionsByQuest
798 | List.length election_questions /= List.length opinionsByQuest =
800 ErrorBallot_WrongNumberOfAnswers
801 (fromIntegral $ List.length opinionsByQuest)
802 (fromIntegral $ List.length election_questions)
803 | otherwise, Dict <- groupDict (Proxy @c) = do
804 let (voterKeys, voterZKP) =
805 case ballotSecKeyMay of
806 Nothing -> (Nothing, ZKP "")
808 ( Just (ballotSecKey, ballotPubKey)
809 , ZKP (bytesNat ballotPubKey) )
810 where ballotPubKey = publicKey ballotSecKey
812 S.mapStateT (withExceptT ErrorBallot_Answer) $
813 zipWithM (encryptAnswer election_public_key voterZKP)
814 election_questions opinionsByQuest
815 ballot_signature <- case voterKeys of
816 Nothing -> return Nothing
817 Just (ballotSecKey, signature_publicKey) -> do
819 proveQuicker ballotSecKey (Identity groupGen) $
820 \(Identity commitment) ->
822 -- NOTE: the order is unusual, the commitments are first
823 -- then comes the statement. Best guess is that
824 -- this is easier to code due to their respective types.
825 (signatureCommitments @_ @crypto voterZKP commitment)
826 (signatureStatement @_ @crypto ballot_answers)
827 return $ Just Signature{..}
830 , ballot_election_hash = election_hash
831 , ballot_election_uuid = election_uuid
837 Reifies c crypto => Group crypto =>
838 Election crypto v c ->
839 Ballot crypto v c -> Bool
840 verifyBallot (Election{..}::Election crypto v c) Ballot{..}
841 | Dict <- groupDict (Proxy @c) =
842 ballot_election_uuid == election_uuid &&
843 ballot_election_hash == election_hash &&
844 List.length election_questions == List.length ballot_answers &&
845 let (isValidSign, zkpSign) =
846 case ballot_signature of
847 Nothing -> (True, ZKP "")
848 Just Signature{..} ->
849 let zkp = ZKP (bytesNat signature_publicKey) in
851 proof_challenge signature_proof == hash
852 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
853 (signatureStatement @_ @crypto ballot_answers)
856 List.zipWith (verifyAnswer election_public_key zkpSign)
857 election_questions ballot_answers
859 -- ** Type 'Signature'
860 -- | Schnorr-like signature.
862 -- Used by each voter to sign his/her encrypted 'Ballot'
863 -- using his/her 'Credential',
864 -- in order to avoid ballot stuffing.
865 data Signature crypto v c = Signature
866 { signature_publicKey :: !(PublicKey crypto c)
867 -- ^ Verification key.
868 , signature_proof :: !(Proof crypto v c)
870 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Signature crypto v c)
874 , ToJSON (G crypto c)
875 ) => ToJSON (Signature crypto v c) where
876 toJSON (Signature pubKey Proof{..}) =
878 [ "public_key" .= pubKey
879 , "challenge" .= proof_challenge
880 , "response" .= proof_response
882 toEncoding (Signature pubKey Proof{..}) =
884 ( "public_key" .= pubKey
885 <> "challenge" .= proof_challenge
886 <> "response" .= proof_response
892 , FromJSON (PublicKey crypto c)
893 ) => FromJSON (Signature crypto v c) where
894 parseJSON = JSON.withObject "Signature" $ \o -> do
895 signature_publicKey <- o .: "public_key"
896 proof_challenge <- o .: "challenge"
897 proof_response <- o .: "response"
898 let signature_proof = Proof{..}
903 -- | @('signatureStatement' answers)@
904 -- returns the encrypted material to be signed:
905 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
906 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
908 foldMap $ \Answer{..} ->
909 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
910 [encryption_nonce, encryption_vault]
912 -- | @('signatureCommitments' voterZKP commitment)@
913 signatureCommitments ::
915 ToNatural (G crypto c) =>
916 ZKP -> Commitment crypto c -> BS.ByteString
917 signatureCommitments (ZKP voterZKP) commitment =
918 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
919 <> bytesNat commitment<>"|"
921 -- ** Type 'ErrorBallot'
922 -- | Error raised by 'encryptBallot'.
924 = ErrorBallot_WrongNumberOfAnswers Natural Natural
925 -- ^ When the number of answers
926 -- is different than the number of questions.
927 | ErrorBallot_Answer ErrorAnswer
928 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
930 -- ^ TODO: to be more precise.
931 deriving (Eq,Show,Generic,NFData)
934 -- | Version of the Helios-C protocol.
935 data Version = Version
936 { version_branch :: [Natural]
937 , version_tags :: [(Text, Natural)]
938 } deriving (Eq,Ord,Generic,NFData)
939 instance IsString Version where
940 fromString = fromJust . readVersion
941 instance Show Version where
942 showsPrec _p Version{..} =
944 (List.intersperse (showChar '.') $
945 showsPrec 0 <$> version_branch) .
947 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
948 if n > 0 then showsPrec 0 n else id)
950 instance ToJSON Version where
951 toJSON = toJSON . show
952 toEncoding = toEncoding . show
953 instance FromJSON Version where
954 parseJSON (JSON.String s)
955 | Just v <- readVersion (Text.unpack s)
957 parseJSON json = JSON.typeMismatch "Version" json
959 hasVersionTag :: Version -> Text -> Bool
960 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
962 experimentalVersion :: Version
963 experimentalVersion = stableVersion
964 {version_tags = [(versionTagQuicker,0)]}
966 stableVersion :: Version
967 stableVersion = "1.6"
969 versionTagQuicker :: Text
970 versionTagQuicker = "quicker"
972 readVersion :: String -> Maybe Version
973 readVersion = parseReadP $ do
974 version_branch <- Read.sepBy1
975 (Read.read <$> Read.munch1 Char.isDigit)
977 version_tags <- Read.many $ (,)
978 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
979 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
982 parseReadP :: Read.ReadP a -> String -> Maybe a
984 let p' = Read.readP_to_S p in