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 = groupReify (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 = groupReify (Proxy @c) $ do
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 $ \(c::Proxy c) -> groupReify c $ do
724 election_name <- o .: "name"
725 election_description <- o .: "description"
726 election_questions <- o .: "questions" :: JSON.Parser [Question v]
727 election_uuid <- o .: "uuid"
728 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
729 return $ k $ Election
730 { election_questions = election_questions
731 , election_public_key = election_public_key
732 , election_hash = base64SHA256 fileData
740 ToJSON (G crypto c) =>
741 Election crypto v c -> Base64SHA256
742 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
745 data Ballot crypto v c = Ballot
746 { ballot_answers :: ![Answer crypto v c]
747 , ballot_signature :: !(Maybe (Signature crypto v c))
748 , ballot_election_uuid :: !UUID
749 , ballot_election_hash :: !Base64SHA256
751 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
756 , ToJSON (G crypto c)
757 ) => ToJSON (Ballot crypto v c) where
760 [ "answers" .= ballot_answers
761 , "election_uuid" .= ballot_election_uuid
762 , "election_hash" .= ballot_election_hash
764 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
765 toEncoding Ballot{..} =
767 ( "answers" .= ballot_answers
768 <> "election_uuid" .= ballot_election_uuid
769 <> "election_hash" .= ballot_election_hash
771 maybe mempty ("signature" .=) ballot_signature
776 , FromJSON (G crypto c)
777 ) => FromJSON (Ballot crypto v c) where
778 parseJSON = JSON.withObject "Ballot" $ \o -> do
779 ballot_answers <- o .: "answers"
780 ballot_signature <- o .:? "signature"
781 ballot_election_uuid <- o .: "election_uuid"
782 ballot_election_hash <- o .: "election_hash"
785 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
786 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
787 -- where 'opinionsByQuest' is a list of 'Opinion's
788 -- on each 'question_choices' of each 'election_questions'.
791 Reifies c crypto => Group crypto => Key crypto =>
792 Monad m => RandomGen r =>
793 Election crypto v c ->
794 Maybe (SecretKey crypto c) -> [[Bool]] ->
795 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
796 encryptBallot (Election{..}::Election crypto v c) ballotSecKeyMay opinionsByQuest
797 | List.length election_questions /= List.length opinionsByQuest =
799 ErrorBallot_WrongNumberOfAnswers
800 (fromIntegral $ List.length opinionsByQuest)
801 (fromIntegral $ List.length election_questions)
802 | otherwise = groupReify (Proxy @c) $ do
803 let (voterKeys, voterZKP) =
804 case ballotSecKeyMay of
805 Nothing -> (Nothing, ZKP "")
807 ( Just (ballotSecKey, ballotPubKey)
808 , ZKP (bytesNat ballotPubKey) )
809 where ballotPubKey = publicKey ballotSecKey
811 S.mapStateT (withExceptT ErrorBallot_Answer) $
812 zipWithM (encryptAnswer election_public_key voterZKP)
813 election_questions opinionsByQuest
814 ballot_signature <- case voterKeys of
815 Nothing -> return Nothing
816 Just (ballotSecKey, signature_publicKey) -> do
818 proveQuicker ballotSecKey (Identity groupGen) $
819 \(Identity commitment) ->
821 -- NOTE: the order is unusual, the commitments are first
822 -- then comes the statement. Best guess is that
823 -- this is easier to code due to their respective types.
824 (signatureCommitments @_ @crypto voterZKP commitment)
825 (signatureStatement @_ @crypto ballot_answers)
826 return $ Just Signature{..}
829 , ballot_election_hash = election_hash
830 , ballot_election_uuid = election_uuid
836 Reifies c crypto => Group crypto =>
837 Election crypto v c ->
838 Ballot crypto v c -> Bool
839 verifyBallot (Election{..}::Election crypto v c) Ballot{..} = groupReify (Proxy @c) $
840 ballot_election_uuid == election_uuid &&
841 ballot_election_hash == election_hash &&
842 List.length election_questions == List.length ballot_answers &&
843 let (isValidSign, zkpSign) =
844 case ballot_signature of
845 Nothing -> (True, ZKP "")
846 Just Signature{..} ->
847 let zkp = ZKP (bytesNat signature_publicKey) in
849 proof_challenge signature_proof == hash
850 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
851 (signatureStatement @_ @crypto ballot_answers)
854 List.zipWith (verifyAnswer election_public_key zkpSign)
855 election_questions ballot_answers
857 -- ** Type 'Signature'
858 -- | Schnorr-like signature.
860 -- Used by each voter to sign his/her encrypted 'Ballot'
861 -- using his/her 'Credential',
862 -- in order to avoid ballot stuffing.
863 data Signature crypto v c = Signature
864 { signature_publicKey :: !(PublicKey crypto c)
865 -- ^ Verification key.
866 , signature_proof :: !(Proof crypto v c)
868 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Signature crypto v c)
872 , ToJSON (G crypto c)
873 ) => ToJSON (Signature crypto v c) where
874 toJSON (Signature pubKey Proof{..}) =
876 [ "public_key" .= pubKey
877 , "challenge" .= proof_challenge
878 , "response" .= proof_response
880 toEncoding (Signature pubKey Proof{..}) =
882 ( "public_key" .= pubKey
883 <> "challenge" .= proof_challenge
884 <> "response" .= proof_response
890 , FromJSON (PublicKey crypto c)
891 ) => FromJSON (Signature crypto v c) where
892 parseJSON = JSON.withObject "Signature" $ \o -> do
893 signature_publicKey <- o .: "public_key"
894 proof_challenge <- o .: "challenge"
895 proof_response <- o .: "response"
896 let signature_proof = Proof{..}
901 -- | @('signatureStatement' answers)@
902 -- returns the encrypted material to be signed:
903 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
904 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
906 foldMap $ \Answer{..} ->
907 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
908 [encryption_nonce, encryption_vault]
910 -- | @('signatureCommitments' voterZKP commitment)@
911 signatureCommitments ::
913 ToNatural (G crypto c) =>
914 ZKP -> Commitment crypto c -> BS.ByteString
915 signatureCommitments (ZKP voterZKP) commitment =
916 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
917 <> bytesNat commitment<>"|"
919 -- ** Type 'ErrorBallot'
920 -- | Error raised by 'encryptBallot'.
922 = ErrorBallot_WrongNumberOfAnswers Natural Natural
923 -- ^ When the number of answers
924 -- is different than the number of questions.
925 | ErrorBallot_Answer ErrorAnswer
926 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
928 -- ^ TODO: to be more precise.
929 deriving (Eq,Show,Generic,NFData)
932 -- | Version of the Helios-C protocol.
933 data Version = Version
934 { version_branch :: [Natural]
935 , version_tags :: [(Text, Natural)]
936 } deriving (Eq,Ord,Generic,NFData)
937 instance IsString Version where
938 fromString = fromJust . readVersion
939 instance Show Version where
940 showsPrec _p Version{..} =
942 (List.intersperse (showChar '.') $
943 showsPrec 0 <$> version_branch) .
945 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
946 if n > 0 then showsPrec 0 n else id)
948 instance ToJSON Version where
949 toJSON = toJSON . show
950 toEncoding = toEncoding . show
951 instance FromJSON Version where
952 parseJSON (JSON.String s)
953 | Just v <- readVersion (Text.unpack s)
955 parseJSON json = JSON.typeMismatch "Version" json
957 hasVersionTag :: Version -> Text -> Bool
958 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
960 experimentalVersion :: Version
961 experimentalVersion = stableVersion
962 {version_tags = [(versionTagQuicker,0)]}
964 stableVersion :: Version
965 stableVersion = "1.6"
967 versionTagQuicker :: Text
968 versionTagQuicker = "quicker"
970 readVersion :: String -> Maybe Version
971 readVersion = parseReadP $ do
972 version_branch <- Read.sepBy1
973 (Read.read <$> Read.munch1 Char.isDigit)
975 version_tags <- Read.many $ (,)
976 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
977 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
980 parseReadP :: Read.ReadP a -> String -> Maybe a
982 let p' = Read.readP_to_S p in