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
56 import Voting.Protocol.FFC (FFC)
58 -- * Type 'Encryption'
59 -- | ElGamal-like encryption.
60 -- Its security relies on the /Discrete Logarithm problem/.
62 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
63 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
64 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
65 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
66 -- to enable the additive homomorphism.
68 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
69 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
70 data Encryption crypto v c = Encryption
71 { encryption_nonce :: !(G crypto c)
72 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
73 -- equal to @('groupGen' '^'encNonce)@
74 , encryption_vault :: !(G crypto c)
75 -- ^ Encrypted 'clear' text,
76 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
78 deriving instance Eq (FieldElement crypto c) => Eq (Encryption crypto v c)
79 deriving instance (Show (FieldElement crypto c), Show (G crypto c)) => Show (Encryption crypto v c)
80 deriving instance NFData (FieldElement crypto c) => NFData (Encryption crypto v c)
84 , ToJSON (FieldElement crypto c)
85 ) => ToJSON (Encryption crypto v c) where
86 toJSON Encryption{..} =
88 [ "alpha" .= encryption_nonce
89 , "beta" .= encryption_vault
91 toEncoding Encryption{..} =
93 ( "alpha" .= encryption_nonce
94 <> "beta" .= encryption_vault
99 , FromJSON (G crypto c)
100 ) => FromJSON (Encryption crypto v c) where
101 parseJSON = JSON.withObject "Encryption" $ \o -> do
102 encryption_nonce <- o .: "alpha"
103 encryption_vault <- o .: "beta"
104 return Encryption{..}
106 -- | Additive homomorphism.
107 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
110 , Multiplicative (FieldElement crypto c)
111 ) => Additive (Encryption crypto v c) where
112 zero = Encryption one one
114 (encryption_nonce x * encryption_nonce y)
115 (encryption_vault x * encryption_vault y)
117 -- *** Type 'EncryptionNonce'
118 type EncryptionNonce = E
120 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
122 -- WARNING: the secret encryption nonce (@encNonce@)
123 -- is returned alongside the 'Encryption'
124 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
125 -- but this secret @encNonce@ MUST be forgotten after that,
126 -- as it may be used to decipher the 'Encryption'
127 -- without the 'SecretKey' associated with 'pubKey'.
132 Multiplicative (FieldElement crypto c) =>
133 Monad m => RandomGen r =>
134 PublicKey crypto c -> E crypto c ->
135 S.StateT r m (EncryptionNonce crypto c, Encryption crypto v c)
136 encrypt pubKey clear = do
138 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
141 { encryption_nonce = groupGen^encNonce
142 , encryption_vault = pubKey ^encNonce * groupGen^clear
146 -- | Non-Interactive Zero-Knowledge 'Proof'
147 -- of knowledge of a discrete logarithm:
148 -- @(secret == logBase base (base^secret))@.
149 data Proof crypto v c = Proof
150 { proof_challenge :: !(Challenge crypto c)
151 -- ^ 'Challenge' sent by the verifier to the prover
152 -- to ensure that the prover really has knowledge
153 -- of the secret and is not replaying.
154 -- Actually, 'proof_challenge' is not sent to the prover,
155 -- but derived from the prover's 'Commitment's and statements
156 -- with a collision resistant 'hash'.
157 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
158 , proof_response :: !(E crypto c)
159 -- ^ A discrete logarithm sent by the prover to the verifier,
160 -- as a response to 'proof_challenge'.
162 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
164 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
165 -- * @commitment '==' 'commit' proof base basePowSec '=='
166 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
167 -- * and @basePowSec '==' base'^'sec@,
169 -- then, with overwhelming probability (due to the 'hash' function),
170 -- the prover was not able to choose 'proof_challenge'
171 -- yet was able to compute a 'proof_response' such that
172 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
173 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
174 -- therefore the prover knows 'sec'.
176 -- The prover choses 'commitment' to be a random power of @base@,
177 -- to ensure that each 'prove' does not reveal any information
179 } deriving (Eq,Show,NFData,Generic)
180 instance Group crypto => ToJSON (Proof crypto v c) where
183 [ "challenge" .= proof_challenge
184 , "response" .= proof_response
186 toEncoding Proof{..} =
188 ( "challenge" .= proof_challenge
189 <> "response" .= proof_response
191 instance (Reifies c crypto, Group crypto) => FromJSON (Proof crypto v c) where
192 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
193 proof_challenge <- o .: "challenge"
194 proof_response <- o .: "response"
198 -- | Zero-knowledge proof.
200 -- A protocol is /zero-knowledge/ if the verifier
201 -- learns nothing from the protocol except that the prover
204 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
205 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
206 newtype ZKP = ZKP BS.ByteString
208 -- ** Type 'Challenge'
212 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
213 -- by 'hash'ing them (eventually with other 'Commitment's).
215 -- Used in 'prove' it enables a Fiat-Shamir transformation
216 -- of an /interactive zero-knowledge/ (IZK) proof
217 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
218 -- That is to say that the verifier does not have
219 -- to send a 'Challenge' to the prover.
220 -- Indeed, the prover now handles the 'Challenge'
221 -- which becomes a (collision resistant) 'hash'
222 -- of the prover's commitments (and statements to be a stronger proof).
223 type Oracle list crypto c = list (Commitment crypto c) -> Challenge crypto c
225 -- | @('prove' sec commitmentBases oracle)@
226 -- returns a 'Proof' that @sec@ is known
227 -- (by proving the knowledge of its discrete logarithm).
229 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
230 -- raised to the power of the secret nonce of the 'Proof',
231 -- as those are the 'Commitment's that the verifier will obtain
232 -- when composing the 'proof_challenge' and 'proof_response' together
235 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
236 -- the statement must be included in the 'hash' (along with the commitments).
238 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
239 -- does not reveal any information regarding the secret @sec@,
240 -- because two 'Proof's using the same 'Commitment'
241 -- can be used to deduce @sec@ (using the special-soundness).
243 forall crypto v c list m r.
247 Multiplicative (FieldElement crypto c) =>
248 Monad m => RandomGen r => Functor list =>
251 Oracle list crypto c ->
252 S.StateT r m (Proof crypto v c)
253 prove sec commitmentBases oracle = do
255 let commitments = (^ nonce) <$> commitmentBases
256 let proof_challenge = oracle commitments
259 , proof_response = nonce `op` (sec*proof_challenge)
262 -- | See comments in 'commit'.
264 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
268 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
269 -- when Helios-C specifications will be fixed.
274 Multiplicative (FieldElement crypto c) =>
275 Monad m => RandomGen r => Functor list =>
278 Oracle list crypto c ->
279 S.StateT r m (Proof crypto v c)
280 proveQuicker sec commitmentBases oracle = do
282 let commitments = (^ nonce) <$> commitmentBases
283 let proof_challenge = oracle commitments
286 , proof_response = nonce - sec*proof_challenge
289 -- | @('fakeProof')@ returns a 'Proof'
290 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
291 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
292 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
293 -- as a 'Proof' returned by 'prove'.
295 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
296 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
300 Monad m => RandomGen r =>
301 S.StateT r m (Proof crypto v c)
303 proof_challenge <- random
304 proof_response <- random
307 -- ** Type 'Commitment'
308 -- | A commitment from the prover to the verifier.
309 -- It's a power of 'groupGen' chosen randomly by the prover
310 -- when making a 'Proof' with 'prove'.
313 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
314 -- from the given 'Proof' with the knowledge of the verifier.
320 Multiplicative (FieldElement 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.
343 Multiplicative (FieldElement crypto c) =>
348 commitQuicker Proof{..} base basePowSec =
349 base^proof_response *
350 basePowSec^proof_challenge
352 -- * Type 'Disjunction'
353 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
354 -- it's used in 'proveEncryption' to generate a 'Proof'
355 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
358 booleanDisjunctions ::
362 Multiplicative (FieldElement crypto c) =>
363 [Disjunction crypto c]
364 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
366 intervalDisjunctions ::
370 Multiplicative (FieldElement crypto c) =>
371 Natural -> Natural -> [Disjunction crypto c]
372 intervalDisjunctions mini maxi =
373 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
374 List.genericDrop (nat mini) $
375 groupGenInverses @crypto
378 -- | Index of a 'Disjunction' within a list of them.
379 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
382 -- ** Type 'DisjProof'
383 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
384 -- is indexing a 'Disjunction' within a list of them,
385 -- without revealing which 'Opinion' it is.
386 newtype DisjProof crypto v c = DisjProof [Proof crypto v c]
387 deriving (Eq,Show,Generic)
388 deriving newtype (NFData,ToJSON,FromJSON)
390 deriving instance Eq (GroupExponent crypto c) => Eq (DisjProof crypto v c)
391 deriving instance Show (GroupExponent crypto c) => Show (DisjProof crypto v c)
392 deriving newtype instance NFData (GroupExponent crypto c) => NFData (DisjProof crypto v c)
393 deriving newtype instance
395 , ToJSON (GroupExponent crypto c)
396 ) => ToJSON (DisjProof crypto v c)
397 deriving newtype instance
399 , FromJSON (GroupExponent crypto c)
400 ) => FromJSON (DisjProof crypto v c)
403 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
404 -- returns a 'DisjProof' that 'enc' 'encrypt's
405 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
407 -- The prover proves that it knows an 'encNonce', such that:
408 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
410 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
412 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
417 ToNatural (FieldElement crypto c) =>
418 Multiplicative (FieldElement crypto c) =>
419 Monad m => RandomGen r =>
420 PublicKey crypto c -> ZKP ->
421 ([Disjunction crypto c],[Disjunction crypto c]) ->
422 (EncryptionNonce crypto c, Encryption crypto v c) ->
423 S.StateT r m (DisjProof crypto v c)
424 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
425 -- Fake proofs for all 'Disjunction's except the genuine one.
426 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
427 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
428 let fakeChallengeSum =
429 sum (proof_challenge <$> prevFakeProofs) +
430 sum (proof_challenge <$> nextFakeProofs)
431 let statement = encryptionStatement voterZKP enc
432 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
433 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
434 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
435 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
436 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
437 let challenge = hash statement commitments in
438 let genuineChallenge = challenge - fakeChallengeSum in
440 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
441 -- thus (sum (proof_challenge <$> proofs) == challenge)
442 -- as checked in 'verifyEncryption'.
443 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
444 return (DisjProof proofs)
450 ToNatural (FieldElement crypto c) =>
451 Multiplicative (FieldElement crypto c) =>
453 PublicKey crypto c -> ZKP ->
454 [Disjunction crypto c] -> (Encryption crypto v c, DisjProof crypto v c) ->
455 ExceptT ErrorVerifyEncryption m Bool
456 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
457 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
459 throwE $ ErrorVerifyEncryption_InvalidProofLength
460 (fromIntegral $ List.length proofs)
461 (fromIntegral $ List.length disjs)
463 return $ challengeSum ==
464 hash (encryptionStatement voterZKP enc) (join commitments)
466 challengeSum = sum (proof_challenge <$> proofs)
469 encryptionStatement ::
471 ToNatural (FieldElement crypto c) =>
472 ZKP -> Encryption crypto v c -> BS.ByteString
473 encryptionStatement (ZKP voterZKP) Encryption{..} =
474 "prove|"<>voterZKP<>"|"
475 <> bytesNat encryption_nonce<>","
476 <> bytesNat encryption_vault<>"|"
478 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
479 -- returns the 'Commitment's with only the knowledge of the verifier.
481 -- For the prover the 'Proof' comes from @fakeProof@,
482 -- and for the verifier the 'Proof' comes from the prover.
483 encryptionCommitments ::
487 Multiplicative (FieldElement crypto c) =>
488 PublicKey crypto c -> Encryption crypto v c ->
489 Disjunction crypto c -> Proof crypto v c -> [G crypto c]
490 encryptionCommitments elecPubKey Encryption{..} disj proof =
491 [ commit proof groupGen encryption_nonce
492 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
493 -- base==groupGen, basePowSec==groupGen^encNonce.
494 , commit proof elecPubKey (encryption_vault*disj)
495 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
496 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
497 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
500 -- ** Type 'ErrorVerifyEncryption'
501 -- | Error raised by 'verifyEncryption'.
502 data ErrorVerifyEncryption
503 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
504 -- ^ When the number of proofs is different than
505 -- the number of 'Disjunction's.
509 data Question v = Question
510 { question_text :: !Text
511 , question_choices :: ![Text]
512 , question_mini :: !Natural
513 , question_maxi :: !Natural
514 -- , question_blank :: Maybe Bool
515 } deriving (Eq,Show,Generic,NFData)
516 instance Reifies v Version => ToJSON (Question v) where
517 toJSON Question{..} =
519 [ "question" .= question_text
520 , "answers" .= question_choices
521 , "min" .= question_mini
522 , "max" .= question_maxi
524 toEncoding Question{..} =
526 ( "question" .= question_text
527 <> "answers" .= question_choices
528 <> "min" .= question_mini
529 <> "max" .= question_maxi
531 instance Reifies v Version => FromJSON (Question v) where
532 parseJSON = JSON.withObject "Question" $ \o -> do
533 question_text <- o .: "question"
534 question_choices <- o .: "answers"
535 question_mini <- o .: "min"
536 question_maxi <- o .: "max"
540 data Answer crypto v c = Answer
541 { answer_opinions :: ![(Encryption crypto v c, DisjProof crypto v c)]
542 -- ^ Encrypted 'Opinion' for each 'question_choices'
543 -- with a 'DisjProof' that they belong to [0,1].
544 , answer_sumProof :: !(DisjProof crypto v c)
545 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
546 -- is an element of @[mini..maxi]@.
547 -- , answer_blankProof ::
549 deriving instance Eq (FieldElement crypto c) => Eq (Answer crypto v c)
550 deriving instance (Show (FieldElement crypto c), Show (G crypto c)) => Show (Answer crypto v c)
551 deriving instance NFData (FieldElement crypto c) => NFData (Answer crypto v c)
555 , ToJSON (FieldElement crypto c)
557 ) => ToJSON (Answer crypto v c) where
559 let (answer_choices, answer_individual_proofs) =
560 List.unzip answer_opinions in
562 [ "choices" .= answer_choices
563 , "individual_proofs" .= answer_individual_proofs
564 , "overall_proof" .= answer_sumProof
566 toEncoding Answer{..} =
567 let (answer_choices, answer_individual_proofs) =
568 List.unzip answer_opinions in
570 ( "choices" .= answer_choices
571 <> "individual_proofs" .= answer_individual_proofs
572 <> "overall_proof" .= answer_sumProof
577 , FromJSON (G crypto c)
579 ) => FromJSON (Answer crypto v c) where
580 parseJSON = JSON.withObject "Answer" $ \o -> do
581 answer_choices <- o .: "choices"
582 answer_individual_proofs <- o .: "individual_proofs"
583 let answer_opinions = List.zip answer_choices answer_individual_proofs
584 answer_sumProof <- o .: "overall_proof"
587 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
588 -- returns an 'Answer' validable by 'verifyAnswer',
589 -- unless an 'ErrorAnswer' is returned.
594 Multiplicative (FieldElement crypto c) =>
595 ToNatural (FieldElement crypto c) =>
596 Monad m => RandomGen r =>
597 PublicKey crypto c -> ZKP ->
598 Question v -> [Bool] ->
599 S.StateT r (ExceptT ErrorAnswer m) (Answer crypto v c)
600 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
601 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
603 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
604 | List.length opinions /= List.length question_choices =
606 ErrorAnswer_WrongNumberOfOpinions
607 (fromIntegral $ List.length opinions)
608 (fromIntegral $ List.length question_choices)
610 encryptions <- encrypt elecPubKey `mapM` opinions
611 individualProofs <- zipWithM
612 (\opinion -> proveEncryption elecPubKey zkp $
614 then (List.init booleanDisjunctions,[])
615 else ([],List.tail booleanDisjunctions))
616 opinionByChoice encryptions
617 sumProof <- proveEncryption elecPubKey zkp
618 (List.tail <$> List.genericSplitAt
619 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
620 (intervalDisjunctions question_mini question_maxi))
621 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
622 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
625 { answer_opinions = List.zip
626 (snd <$> encryptions) -- NOTE: drop encNonce
628 , answer_sumProof = sumProof
631 opinionsSum = sum $ nat <$> opinions
632 opinions = (\o -> if o then one else zero) <$> opinionByChoice
638 Multiplicative (FieldElement crypto c) =>
639 ToNatural (FieldElement crypto c) =>
640 PublicKey crypto c -> ZKP ->
641 Question v -> Answer crypto v c -> Bool
642 verifyAnswer elecPubKey zkp Question{..} Answer{..}
643 | List.length question_choices /= List.length answer_opinions = False
644 | otherwise = either (const False) id $ runExcept $ do
646 verifyEncryption elecPubKey zkp booleanDisjunctions
647 `traverse` answer_opinions
648 validSum <- verifyEncryption elecPubKey zkp
649 (intervalDisjunctions question_mini question_maxi)
650 ( sum (fst <$> answer_opinions)
652 return (and validOpinions && validSum)
654 -- ** Type 'ErrorAnswer'
655 -- | Error raised by 'encryptAnswer'.
657 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
658 -- ^ When the number of opinions is different than
659 -- the number of choices ('question_choices').
660 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
661 -- ^ When the sum of opinions is not within the bounds
662 -- of 'question_mini' and 'question_maxi'.
663 deriving (Eq,Show,Generic,NFData)
666 data Election crypto v c = Election
667 { election_name :: !Text
668 , election_description :: !Text
669 , election_questions :: ![Question v]
670 , election_uuid :: !UUID
671 , election_hash :: Base64SHA256
672 , election_crypto :: !crypto
673 , election_version :: !(Maybe Version)
674 , election_public_key :: !(PublicKey crypto c)
676 deriving instance (Eq crypto, Eq (FieldElement crypto c)) => Eq (Election crypto v c)
677 deriving instance (Show crypto, Show (FieldElement crypto c)) => Show (Election crypto v c)
678 deriving instance (NFData crypto, NFData (FieldElement crypto c)) => NFData (Election crypto v c)
681 , ToJSON (FieldElement crypto c)
684 ) => ToJSON (Election crypto v c) where
685 toJSON Election{..} =
687 [ "name" .= election_name
688 , "description" .= election_description
689 , ("public_key", JSON.object
690 [ "group" .= election_crypto
691 , "y" .= election_public_key
693 , "questions" .= election_questions
694 , "uuid" .= election_uuid
696 maybe [] (\version -> [ "version" .= version ]) election_version
697 toEncoding Election{..} =
699 ( "name" .= election_name
700 <> "description" .= election_description
701 <> JSON.pair "public_key" (JSON.pairs $
702 "group" .= election_crypto
703 <> "y" .= election_public_key
705 <> "questions" .= election_questions
706 <> "uuid" .= election_uuid
708 maybe mempty ("version" .=) election_version
711 ReifyCrypto crypto =>
717 FieldElementConstraints crypto c =>
718 Election crypto v c -> r) ->
720 readElection filePath k = do
721 fileData <- lift $ BS.readFile filePath
723 jsonEitherFormatError $
724 JSON.eitherDecodeStrictWith JSON.jsonEOF
725 (JSON.iparse (parseElection fileData))
728 parseElection fileData = JSON.withObject "Election" $ \o -> do
729 election_version <- o .:? "version"
730 reify (fromMaybe stableVersion election_version) $ \(_v::Proxy v) -> do
731 (election_crypto, elecPubKey) <-
732 JSON.explicitParseField
733 (JSON.withObject "public_key" $ \obj -> do
734 crypto <- obj .: "group"
735 pubKey :: JSON.Value <- obj .: "y"
736 return (crypto, pubKey)
738 reifyCrypto election_crypto $ \(_c::Proxy c) -> do
739 election_name <- o .: "name"
740 election_description <- o .: "description"
741 election_questions <- o .: "questions" :: JSON.Parser [Question v]
742 election_uuid <- o .: "uuid"
743 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
744 return $ k $ Election
745 { election_questions = election_questions
746 , election_public_key = election_public_key
747 , election_hash = base64SHA256 fileData
755 ToJSON (FieldElement crypto c) =>
756 Election crypto v c -> Base64SHA256
757 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
759 -- ** Class 'ReifyCrypto'
760 -- | @('reifyCrypto' crypto k)@ is like @('reify' crypto k)@
761 -- but gives to @(k)@ more constraints than just @('Reifies' c crypto)@,
762 -- which is used when defining classes on @(crypto)@
763 -- where @(c)@ (the type variable guarantying the same
764 -- @crypto@graphic parameters are used throughout)
765 -- is not yet in scope and thus where one cannot
766 -- add those constraints requiring to have @(c)@ in scope.
767 -- See for instance the 'QuickcheckElection' class, in the tests.
769 -- For convenience, the 'ReifyCrypto' class also implies the pervasive
770 -- constraint 'Group'.
777 , JSON.FromJSON crypto
778 ) => ReifyCrypto crypto where
782 FieldElementConstraints crypto c =>
784 instance ReifyCrypto FFC where
787 -- ** Class 'FieldElementConstraints'
788 -- | List the 'Constraint's on the element of the field
789 -- when the @(crypto)@ has not been instantiated to a specific type yet.
790 -- It concerns only 'Constraint's whose method act on @(a)@,
791 -- not @(x c)@ (eg. 'Group').
792 type FieldElementConstraints crypto c =
793 ( Multiplicative (FieldElement crypto c)
794 , FromNatural (FieldElement crypto c)
795 , ToNatural (FieldElement crypto c)
796 , Eq (FieldElement crypto c)
797 , Ord (FieldElement crypto c)
798 , Show (FieldElement crypto c)
799 , NFData (FieldElement crypto c)
800 , FromJSON (FieldElement crypto c)
801 , ToJSON (FieldElement crypto c)
802 , FromJSON (G crypto c)
803 , ToJSON (G crypto c)
807 data Ballot crypto v c = Ballot
808 { ballot_answers :: ![Answer crypto v c]
809 , ballot_signature :: !(Maybe (Signature crypto v c))
810 , ballot_election_uuid :: !UUID
811 , ballot_election_hash :: !Base64SHA256
813 deriving instance (NFData (FieldElement crypto c), NFData crypto) => NFData (Ballot crypto v c)
818 , ToJSON (FieldElement crypto c)
819 ) => ToJSON (Ballot crypto v c) where
822 [ "answers" .= ballot_answers
823 , "election_uuid" .= ballot_election_uuid
824 , "election_hash" .= ballot_election_hash
826 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
827 toEncoding Ballot{..} =
829 ( "answers" .= ballot_answers
830 <> "election_uuid" .= ballot_election_uuid
831 <> "election_hash" .= ballot_election_hash
833 maybe mempty ("signature" .=) ballot_signature
838 , FromJSON (G crypto c)
839 ) => FromJSON (Ballot crypto v c) where
840 parseJSON = JSON.withObject "Ballot" $ \o -> do
841 ballot_answers <- o .: "answers"
842 ballot_signature <- o .:? "signature"
843 ballot_election_uuid <- o .: "election_uuid"
844 ballot_election_hash <- o .: "election_hash"
847 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
848 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
849 -- where 'opinionsByQuest' is a list of 'Opinion's
850 -- on each 'question_choices' of each 'election_questions'.
852 forall crypto m v c r.
857 Multiplicative (FieldElement crypto c) =>
858 ToNatural (FieldElement crypto c) =>
859 Monad m => RandomGen r =>
860 Election crypto v c ->
861 Maybe (SecretKey crypto c) -> [[Bool]] ->
862 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
863 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
864 | List.length election_questions /= List.length opinionsByQuest =
866 ErrorBallot_WrongNumberOfAnswers
867 (fromIntegral $ List.length opinionsByQuest)
868 (fromIntegral $ List.length election_questions)
870 let (voterKeys, voterZKP) =
871 case ballotSecKeyMay of
872 Nothing -> (Nothing, ZKP "")
874 ( Just (ballotSecKey, ballotPubKey)
875 , ZKP (bytesNat ballotPubKey) )
876 where ballotPubKey = publicKey ballotSecKey
878 S.mapStateT (withExceptT ErrorBallot_Answer) $
879 zipWithM (encryptAnswer election_public_key voterZKP)
880 election_questions opinionsByQuest
881 ballot_signature <- case voterKeys of
882 Nothing -> return Nothing
883 Just (ballotSecKey, signature_publicKey) -> do
885 proveQuicker ballotSecKey (Identity groupGen) $
886 \(Identity commitment) ->
888 -- NOTE: the order is unusual, the commitments are first
889 -- then comes the statement. Best guess is that
890 -- this is easier to code due to their respective types.
891 (signatureCommitments @_ @crypto voterZKP commitment)
892 (signatureStatement @_ @crypto ballot_answers)
893 return $ Just Signature{..}
896 , ballot_election_hash = election_hash
897 , ballot_election_uuid = election_uuid
906 Multiplicative (FieldElement crypto c) =>
907 ToNatural (FieldElement crypto c) =>
908 ToNatural (PublicKey crypto c) =>
909 Election crypto v c ->
910 Ballot crypto v c -> Bool
911 verifyBallot Election{..} Ballot{..} =
912 ballot_election_uuid == election_uuid &&
913 ballot_election_hash == election_hash &&
914 List.length election_questions == List.length ballot_answers &&
915 let (isValidSign, zkpSign) =
916 case ballot_signature of
917 Nothing -> (True, ZKP "")
918 Just Signature{..} ->
919 let zkp = ZKP (bytesNat signature_publicKey) in
921 proof_challenge signature_proof == hash
922 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
923 (signatureStatement @_ @crypto ballot_answers)
926 List.zipWith (verifyAnswer election_public_key zkpSign)
927 election_questions ballot_answers
929 -- ** Type 'Signature'
930 -- | Schnorr-like signature.
932 -- Used by each voter to sign his/her encrypted 'Ballot'
933 -- using his/her 'Credential',
934 -- in order to avoid ballot stuffing.
935 data Signature crypto v c = Signature
936 { signature_publicKey :: !(PublicKey crypto c)
937 -- ^ Verification key.
938 , signature_proof :: !(Proof crypto v c)
942 , NFData (FieldElement crypto c)
943 ) => NFData (Signature crypto v c)
947 , ToJSON (FieldElement crypto c)
948 ) => ToJSON (Signature crypto v c) where
949 toJSON (Signature pubKey Proof{..}) =
951 [ "public_key" .= pubKey
952 , "challenge" .= proof_challenge
953 , "response" .= proof_response
955 toEncoding (Signature pubKey Proof{..}) =
957 ( "public_key" .= pubKey
958 <> "challenge" .= proof_challenge
959 <> "response" .= proof_response
965 , FromJSON (PublicKey crypto c)
966 ) => FromJSON (Signature crypto v c) where
967 parseJSON = JSON.withObject "Signature" $ \o -> do
968 signature_publicKey <- o .: "public_key"
969 proof_challenge <- o .: "challenge"
970 proof_response <- o .: "response"
971 let signature_proof = Proof{..}
976 -- | @('signatureStatement' answers)@
977 -- returns the encrypted material to be signed:
978 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
979 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
981 foldMap $ \Answer{..} ->
982 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
983 [encryption_nonce, encryption_vault]
985 -- | @('signatureCommitments' voterZKP commitment)@
986 signatureCommitments ::
988 ToNatural (FieldElement crypto c) =>
989 ZKP -> Commitment crypto c -> BS.ByteString
990 signatureCommitments (ZKP voterZKP) commitment =
991 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
992 <> bytesNat commitment<>"|"
994 -- ** Type 'ErrorBallot'
995 -- | Error raised by 'encryptBallot'.
997 = ErrorBallot_WrongNumberOfAnswers Natural Natural
998 -- ^ When the number of answers
999 -- is different than the number of questions.
1000 | ErrorBallot_Answer ErrorAnswer
1001 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
1003 -- ^ TODO: to be more precise.
1004 deriving (Eq,Show,Generic,NFData)
1007 -- | Version of the Helios-C protocol.
1008 data Version = Version
1009 { version_branch :: [Natural]
1010 , version_tags :: [(Text, Natural)]
1011 } deriving (Eq,Ord,Generic,NFData)
1012 instance IsString Version where
1013 fromString = fromJust . readVersion
1014 instance Show Version where
1015 showsPrec _p Version{..} =
1017 (List.intersperse (showChar '.') $
1018 showsPrec 0 <$> version_branch) .
1020 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
1021 if n > 0 then showsPrec 0 n else id)
1023 instance ToJSON Version where
1024 toJSON = toJSON . show
1025 toEncoding = toEncoding . show
1026 instance FromJSON Version where
1027 parseJSON (JSON.String s)
1028 | Just v <- readVersion (Text.unpack s)
1030 parseJSON json = JSON.typeMismatch "Version" json
1032 hasVersionTag :: Version -> Text -> Bool
1033 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
1035 experimentalVersion :: Version
1036 experimentalVersion = stableVersion
1037 {version_tags = [(versionTagQuicker,0)]}
1039 stableVersion :: Version
1040 stableVersion = "1.6"
1042 versionTagQuicker :: Text
1043 versionTagQuicker = "quicker"
1045 readVersion :: String -> Maybe Version
1046 readVersion = parseReadP $ do
1047 version_branch <- Read.sepBy1
1048 (Read.read <$> Read.munch1 Char.isDigit)
1050 version_tags <- Read.many $ (,)
1051 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
1052 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
1055 parseReadP :: Read.ReadP a -> String -> Maybe a
1057 let p' = Read.readP_to_S p in