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 (G crypto c) => Eq (Encryption crypto v c)
79 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Encryption crypto v c)
80 deriving instance NFData (G crypto c) => NFData (Encryption crypto v 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 (G 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 (G 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 (G 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 (G 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 (G crypto c) =>
321 Invertible (G crypto c) =>
326 commit Proof{..} base basePowSec =
327 (base^proof_response) `op`
328 (basePowSec^proof_challenge)
331 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
334 -- TODO: contrary to some textbook presentations,
335 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
336 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
337 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
338 {-# INLINE commit #-}
340 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
341 -- 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 Multiplicative (G crypto c) =>
364 Invertible (G crypto c) =>
365 [Disjunction crypto c]
366 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
368 intervalDisjunctions ::
372 Multiplicative (G crypto c) =>
373 Invertible (G crypto c) =>
374 Natural -> Natural -> [Disjunction crypto c]
375 intervalDisjunctions mini maxi =
376 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
377 List.genericDrop (nat mini) $
378 groupGenInverses @crypto
381 -- | Index of a 'Disjunction' within a list of them.
382 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
385 -- ** Type 'DisjProof'
386 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
387 -- is indexing a 'Disjunction' within a list of them,
388 -- without revealing which 'Opinion' it is.
389 newtype DisjProof crypto v c = DisjProof [Proof crypto v c]
390 deriving (Eq,Show,Generic)
391 deriving newtype (NFData,ToJSON,FromJSON)
393 deriving instance Eq (GroupExponent crypto c) => Eq (DisjProof crypto v c)
394 deriving instance Show (GroupExponent crypto c) => Show (DisjProof crypto v c)
395 deriving newtype instance NFData (GroupExponent crypto c) => NFData (DisjProof crypto v c)
396 deriving newtype instance
398 , ToJSON (GroupExponent crypto c)
399 ) => ToJSON (DisjProof crypto v c)
400 deriving newtype instance
402 , FromJSON (GroupExponent crypto c)
403 ) => FromJSON (DisjProof crypto v c)
406 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
407 -- returns a 'DisjProof' that 'enc' 'encrypt's
408 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
410 -- The prover proves that it knows an 'encNonce', such that:
411 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
413 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
415 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
420 ToNatural (G crypto c) =>
421 Multiplicative (G crypto c) =>
422 Invertible (G crypto c) =>
423 Monad m => RandomGen r =>
424 PublicKey crypto c -> ZKP ->
425 ([Disjunction crypto c],[Disjunction crypto c]) ->
426 (EncryptionNonce crypto c, Encryption crypto v c) ->
427 S.StateT r m (DisjProof crypto v c)
428 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
429 -- Fake proofs for all 'Disjunction's except the genuine one.
430 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
431 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
432 let fakeChallengeSum =
433 sum (proof_challenge <$> prevFakeProofs) +
434 sum (proof_challenge <$> nextFakeProofs)
435 let statement = encryptionStatement voterZKP enc
436 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
437 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
438 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
439 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
440 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
441 let challenge = hash statement commitments in
442 let genuineChallenge = challenge - fakeChallengeSum in
444 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
445 -- thus (sum (proof_challenge <$> proofs) == challenge)
446 -- as checked in 'verifyEncryption'.
447 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
448 return (DisjProof proofs)
454 ToNatural (G crypto c) =>
455 Multiplicative (G crypto c) =>
456 Invertible (G crypto c) =>
458 PublicKey crypto c -> ZKP ->
459 [Disjunction crypto c] -> (Encryption crypto v c, DisjProof crypto v c) ->
460 ExceptT ErrorVerifyEncryption m Bool
461 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
462 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
464 throwE $ ErrorVerifyEncryption_InvalidProofLength
465 (fromIntegral $ List.length proofs)
466 (fromIntegral $ List.length disjs)
468 return $ challengeSum ==
469 hash (encryptionStatement voterZKP enc) (join commitments)
471 challengeSum = sum (proof_challenge <$> proofs)
474 encryptionStatement ::
476 ToNatural (G crypto c) =>
477 ZKP -> Encryption crypto v c -> BS.ByteString
478 encryptionStatement (ZKP voterZKP) Encryption{..} =
479 "prove|"<>voterZKP<>"|"
480 <> bytesNat encryption_nonce<>","
481 <> bytesNat encryption_vault<>"|"
483 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
484 -- returns the 'Commitment's with only the knowledge of the verifier.
486 -- For the prover the 'Proof' comes from @fakeProof@,
487 -- and for the verifier the 'Proof' comes from the prover.
488 encryptionCommitments ::
492 Invertible (G crypto c) =>
493 PublicKey crypto c -> Encryption crypto v c ->
494 Disjunction crypto c -> Proof crypto v c -> [G crypto c]
495 encryptionCommitments elecPubKey Encryption{..} disj proof =
496 [ commit proof groupGen encryption_nonce
497 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
498 -- base==groupGen, basePowSec==groupGen^encNonce.
499 , commit proof elecPubKey (encryption_vault*disj)
500 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
501 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
502 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
505 -- ** Type 'ErrorVerifyEncryption'
506 -- | Error raised by 'verifyEncryption'.
507 data ErrorVerifyEncryption
508 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
509 -- ^ When the number of proofs is different than
510 -- the number of 'Disjunction's.
514 data Question v = Question
515 { question_text :: !Text
516 , question_choices :: ![Text]
517 , question_mini :: !Natural
518 , question_maxi :: !Natural
519 -- , question_blank :: Maybe Bool
520 } deriving (Eq,Show,Generic,NFData)
521 instance Reifies v Version => ToJSON (Question v) where
522 toJSON Question{..} =
524 [ "question" .= question_text
525 , "answers" .= question_choices
526 , "min" .= question_mini
527 , "max" .= question_maxi
529 toEncoding Question{..} =
531 ( "question" .= question_text
532 <> "answers" .= question_choices
533 <> "min" .= question_mini
534 <> "max" .= question_maxi
536 instance Reifies v Version => FromJSON (Question v) where
537 parseJSON = JSON.withObject "Question" $ \o -> do
538 question_text <- o .: "question"
539 question_choices <- o .: "answers"
540 question_mini <- o .: "min"
541 question_maxi <- o .: "max"
545 data Answer crypto v c = Answer
546 { answer_opinions :: ![(Encryption crypto v c, DisjProof crypto v c)]
547 -- ^ Encrypted 'Opinion' for each 'question_choices'
548 -- with a 'DisjProof' that they belong to [0,1].
549 , answer_sumProof :: !(DisjProof crypto v c)
550 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
551 -- is an element of @[mini..maxi]@.
552 -- , answer_blankProof ::
554 deriving instance Eq (G crypto c) => Eq (Answer crypto v c)
555 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Answer crypto v c)
556 deriving instance NFData (G crypto c) => NFData (Answer crypto v c)
560 , ToJSON (G crypto c)
562 ) => ToJSON (Answer crypto v c) where
564 let (answer_choices, answer_individual_proofs) =
565 List.unzip answer_opinions in
567 [ "choices" .= answer_choices
568 , "individual_proofs" .= answer_individual_proofs
569 , "overall_proof" .= answer_sumProof
571 toEncoding Answer{..} =
572 let (answer_choices, answer_individual_proofs) =
573 List.unzip answer_opinions in
575 ( "choices" .= answer_choices
576 <> "individual_proofs" .= answer_individual_proofs
577 <> "overall_proof" .= answer_sumProof
582 , FromJSON (G crypto c)
584 ) => FromJSON (Answer crypto v c) where
585 parseJSON = JSON.withObject "Answer" $ \o -> do
586 answer_choices <- o .: "choices"
587 answer_individual_proofs <- o .: "individual_proofs"
588 let answer_opinions = List.zip answer_choices answer_individual_proofs
589 answer_sumProof <- o .: "overall_proof"
592 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
593 -- returns an 'Answer' validable by 'verifyAnswer',
594 -- unless an 'ErrorAnswer' is returned.
599 Multiplicative (G crypto c) =>
600 Invertible (G crypto c) =>
601 ToNatural (G crypto c) =>
602 Monad m => RandomGen r =>
603 PublicKey crypto c -> ZKP ->
604 Question v -> [Bool] ->
605 S.StateT r (ExceptT ErrorAnswer m) (Answer crypto v c)
606 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
607 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
609 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
610 | List.length opinions /= List.length question_choices =
612 ErrorAnswer_WrongNumberOfOpinions
613 (fromIntegral $ List.length opinions)
614 (fromIntegral $ List.length question_choices)
616 encryptions <- encrypt elecPubKey `mapM` opinions
617 individualProofs <- zipWithM
618 (\opinion -> proveEncryption elecPubKey zkp $
620 then (List.init booleanDisjunctions,[])
621 else ([],List.tail booleanDisjunctions))
622 opinionByChoice encryptions
623 sumProof <- proveEncryption elecPubKey zkp
624 (List.tail <$> List.genericSplitAt
625 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
626 (intervalDisjunctions question_mini question_maxi))
627 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
628 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
631 { answer_opinions = List.zip
632 (snd <$> encryptions) -- NOTE: drop encNonce
634 , answer_sumProof = sumProof
637 opinionsSum = sum $ nat <$> opinions
638 opinions = (\o -> if o then one else zero) <$> opinionByChoice
644 Multiplicative (G crypto c) =>
645 Invertible (G crypto c) =>
646 ToNatural (G crypto c) =>
647 PublicKey crypto c -> ZKP ->
648 Question v -> Answer crypto v c -> Bool
649 verifyAnswer elecPubKey zkp Question{..} Answer{..}
650 | List.length question_choices /= List.length answer_opinions = False
651 | otherwise = either (const False) id $ runExcept $ do
653 verifyEncryption elecPubKey zkp booleanDisjunctions
654 `traverse` answer_opinions
655 validSum <- verifyEncryption elecPubKey zkp
656 (intervalDisjunctions question_mini question_maxi)
657 ( sum (fst <$> answer_opinions)
659 return (and validOpinions && validSum)
661 -- ** Type 'ErrorAnswer'
662 -- | Error raised by 'encryptAnswer'.
664 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
665 -- ^ When the number of opinions is different than
666 -- the number of choices ('question_choices').
667 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
668 -- ^ When the sum of opinions is not within the bounds
669 -- of 'question_mini' and 'question_maxi'.
670 deriving (Eq,Show,Generic,NFData)
673 data Election crypto v c = Election
674 { election_name :: !Text
675 , election_description :: !Text
676 , election_questions :: ![Question v]
677 , election_uuid :: !UUID
678 , election_hash :: Base64SHA256
679 , election_crypto :: !crypto
680 , election_version :: !(Maybe Version)
681 , election_public_key :: !(PublicKey crypto c)
683 deriving instance (Eq crypto, Eq (G crypto c)) => Eq (Election crypto v c)
684 deriving instance (Show crypto, Show (G crypto c)) => Show (Election crypto v c)
685 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Election crypto v c)
688 , ToJSON (G crypto c)
691 ) => ToJSON (Election crypto v c) where
692 toJSON Election{..} =
694 [ "name" .= election_name
695 , "description" .= election_description
696 , ("public_key", JSON.object
697 [ "group" .= election_crypto
698 , "y" .= election_public_key
700 , "questions" .= election_questions
701 , "uuid" .= election_uuid
703 maybe [] (\version -> [ "version" .= version ]) election_version
704 toEncoding Election{..} =
706 ( "name" .= election_name
707 <> "description" .= election_description
708 <> JSON.pair "public_key" (JSON.pairs $
709 "group" .= election_crypto
710 <> "y" .= election_public_key
712 <> "questions" .= election_questions
713 <> "uuid" .= election_uuid
715 maybe mempty ("version" .=) election_version
718 ReifyCrypto crypto =>
724 GConstraints crypto c =>
725 Election crypto v c -> r) ->
727 readElection filePath k = do
728 fileData <- lift $ BS.readFile filePath
730 jsonEitherFormatError $
731 JSON.eitherDecodeStrictWith JSON.jsonEOF
732 (JSON.iparse (parseElection fileData))
735 parseElection fileData = JSON.withObject "Election" $ \o -> do
736 election_version <- o .:? "version"
737 reify (fromMaybe stableVersion election_version) $ \(_v::Proxy v) -> do
738 (election_crypto, elecPubKey) <-
739 JSON.explicitParseField
740 (JSON.withObject "public_key" $ \obj -> do
741 crypto <- obj .: "group"
742 pubKey :: JSON.Value <- obj .: "y"
743 return (crypto, pubKey)
745 reifyCrypto election_crypto $ \(_c::Proxy c) -> do
746 election_name <- o .: "name"
747 election_description <- o .: "description"
748 election_questions <- o .: "questions" :: JSON.Parser [Question v]
749 election_uuid <- o .: "uuid"
750 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
751 return $ k $ Election
752 { election_questions = election_questions
753 , election_public_key = election_public_key
754 , election_hash = base64SHA256 fileData
762 ToJSON (G crypto c) =>
763 Election crypto v c -> Base64SHA256
764 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
766 -- ** Class 'ReifyCrypto'
767 -- | @('reifyCrypto' crypto k)@ is like @('reify' crypto k)@
768 -- but gives to @(k)@ more constraints than just @('Reifies' c crypto)@,
769 -- which is used when defining classes on @(crypto)@
770 -- where @(c)@ (the type variable guarantying the same
771 -- @crypto@graphic parameters are used throughout)
772 -- is not yet in scope and thus where one cannot
773 -- add those constraints requiring to have @(c)@ in scope.
774 -- See for instance the 'QuickcheckElection' class, in the tests.
776 -- For convenience, the 'ReifyCrypto' class also implies the pervasive
777 -- constraint 'Group'.
784 , JSON.FromJSON crypto
785 ) => ReifyCrypto crypto where
789 GConstraints crypto c =>
791 instance ReifyCrypto FFC where
794 -- ** Class 'GConstraints'
795 -- | List the 'Constraint's on the element of the field
796 -- when the @(crypto)@ has not been instantiated to a specific type yet.
797 -- It concerns only 'Constraint's whose method act on @(a)@,
798 -- not @(x c)@ (eg. 'Group').
799 type GConstraints crypto c =
800 ( Multiplicative (G crypto c)
801 , Invertible (G crypto c)
802 , FromNatural (G crypto c)
803 , ToNatural (G crypto c)
807 , NFData (G crypto c)
808 , FromJSON (G crypto c)
809 , ToJSON (G crypto c)
810 , FromJSON (G crypto c)
811 , ToJSON (G crypto c)
815 data Ballot crypto v c = Ballot
816 { ballot_answers :: ![Answer crypto v c]
817 , ballot_signature :: !(Maybe (Signature crypto v c))
818 , ballot_election_uuid :: !UUID
819 , ballot_election_hash :: !Base64SHA256
821 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
826 , ToJSON (G crypto c)
827 ) => ToJSON (Ballot crypto v c) where
830 [ "answers" .= ballot_answers
831 , "election_uuid" .= ballot_election_uuid
832 , "election_hash" .= ballot_election_hash
834 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
835 toEncoding Ballot{..} =
837 ( "answers" .= ballot_answers
838 <> "election_uuid" .= ballot_election_uuid
839 <> "election_hash" .= ballot_election_hash
841 maybe mempty ("signature" .=) ballot_signature
846 , FromJSON (G crypto c)
847 ) => FromJSON (Ballot crypto v c) where
848 parseJSON = JSON.withObject "Ballot" $ \o -> do
849 ballot_answers <- o .: "answers"
850 ballot_signature <- o .:? "signature"
851 ballot_election_uuid <- o .: "election_uuid"
852 ballot_election_hash <- o .: "election_hash"
855 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
856 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
857 -- where 'opinionsByQuest' is a list of 'Opinion's
858 -- on each 'question_choices' of each 'election_questions'.
860 forall crypto m v c r.
865 Multiplicative (G crypto c) =>
866 Invertible (G crypto c) =>
867 ToNatural (G crypto c) =>
868 Monad m => RandomGen r =>
869 Election crypto v c ->
870 Maybe (SecretKey crypto c) -> [[Bool]] ->
871 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
872 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
873 | List.length election_questions /= List.length opinionsByQuest =
875 ErrorBallot_WrongNumberOfAnswers
876 (fromIntegral $ List.length opinionsByQuest)
877 (fromIntegral $ List.length election_questions)
879 let (voterKeys, voterZKP) =
880 case ballotSecKeyMay of
881 Nothing -> (Nothing, ZKP "")
883 ( Just (ballotSecKey, ballotPubKey)
884 , ZKP (bytesNat ballotPubKey) )
885 where ballotPubKey = publicKey ballotSecKey
887 S.mapStateT (withExceptT ErrorBallot_Answer) $
888 zipWithM (encryptAnswer election_public_key voterZKP)
889 election_questions opinionsByQuest
890 ballot_signature <- case voterKeys of
891 Nothing -> return Nothing
892 Just (ballotSecKey, signature_publicKey) -> do
894 proveQuicker ballotSecKey (Identity groupGen) $
895 \(Identity commitment) ->
897 -- NOTE: the order is unusual, the commitments are first
898 -- then comes the statement. Best guess is that
899 -- this is easier to code due to their respective types.
900 (signatureCommitments @_ @crypto voterZKP commitment)
901 (signatureStatement @_ @crypto ballot_answers)
902 return $ Just Signature{..}
905 , ballot_election_hash = election_hash
906 , ballot_election_uuid = election_uuid
915 Multiplicative (G crypto c) =>
916 Invertible (G crypto c) =>
917 ToNatural (G crypto c) =>
918 Election crypto v c ->
919 Ballot crypto v c -> Bool
920 verifyBallot Election{..} Ballot{..} =
921 ballot_election_uuid == election_uuid &&
922 ballot_election_hash == election_hash &&
923 List.length election_questions == List.length ballot_answers &&
924 let (isValidSign, zkpSign) =
925 case ballot_signature of
926 Nothing -> (True, ZKP "")
927 Just Signature{..} ->
928 let zkp = ZKP (bytesNat signature_publicKey) in
930 proof_challenge signature_proof == hash
931 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
932 (signatureStatement @_ @crypto ballot_answers)
935 List.zipWith (verifyAnswer election_public_key zkpSign)
936 election_questions ballot_answers
938 -- ** Type 'Signature'
939 -- | Schnorr-like signature.
941 -- Used by each voter to sign his/her encrypted 'Ballot'
942 -- using his/her 'Credential',
943 -- in order to avoid ballot stuffing.
944 data Signature crypto v c = Signature
945 { signature_publicKey :: !(PublicKey crypto c)
946 -- ^ Verification key.
947 , signature_proof :: !(Proof crypto v c)
951 , NFData (G crypto c)
952 ) => NFData (Signature crypto v c)
956 , ToJSON (G crypto c)
957 ) => ToJSON (Signature crypto v c) where
958 toJSON (Signature pubKey Proof{..}) =
960 [ "public_key" .= pubKey
961 , "challenge" .= proof_challenge
962 , "response" .= proof_response
964 toEncoding (Signature pubKey Proof{..}) =
966 ( "public_key" .= pubKey
967 <> "challenge" .= proof_challenge
968 <> "response" .= proof_response
974 , FromJSON (PublicKey crypto c)
975 ) => FromJSON (Signature crypto v c) where
976 parseJSON = JSON.withObject "Signature" $ \o -> do
977 signature_publicKey <- o .: "public_key"
978 proof_challenge <- o .: "challenge"
979 proof_response <- o .: "response"
980 let signature_proof = Proof{..}
985 -- | @('signatureStatement' answers)@
986 -- returns the encrypted material to be signed:
987 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
988 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
990 foldMap $ \Answer{..} ->
991 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
992 [encryption_nonce, encryption_vault]
994 -- | @('signatureCommitments' voterZKP commitment)@
995 signatureCommitments ::
997 ToNatural (G crypto c) =>
998 ZKP -> Commitment crypto c -> BS.ByteString
999 signatureCommitments (ZKP voterZKP) commitment =
1000 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
1001 <> bytesNat commitment<>"|"
1003 -- ** Type 'ErrorBallot'
1004 -- | Error raised by 'encryptBallot'.
1006 = ErrorBallot_WrongNumberOfAnswers Natural Natural
1007 -- ^ When the number of answers
1008 -- is different than the number of questions.
1009 | ErrorBallot_Answer ErrorAnswer
1010 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
1012 -- ^ TODO: to be more precise.
1013 deriving (Eq,Show,Generic,NFData)
1016 -- | Version of the Helios-C protocol.
1017 data Version = Version
1018 { version_branch :: [Natural]
1019 , version_tags :: [(Text, Natural)]
1020 } deriving (Eq,Ord,Generic,NFData)
1021 instance IsString Version where
1022 fromString = fromJust . readVersion
1023 instance Show Version where
1024 showsPrec _p Version{..} =
1026 (List.intersperse (showChar '.') $
1027 showsPrec 0 <$> version_branch) .
1029 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
1030 if n > 0 then showsPrec 0 n else id)
1032 instance ToJSON Version where
1033 toJSON = toJSON . show
1034 toEncoding = toEncoding . show
1035 instance FromJSON Version where
1036 parseJSON (JSON.String s)
1037 | Just v <- readVersion (Text.unpack s)
1039 parseJSON json = JSON.typeMismatch "Version" json
1041 hasVersionTag :: Version -> Text -> Bool
1042 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
1044 experimentalVersion :: Version
1045 experimentalVersion = stableVersion
1046 {version_tags = [(versionTagQuicker,0)]}
1048 stableVersion :: Version
1049 stableVersion = "1.6"
1051 versionTagQuicker :: Text
1052 versionTagQuicker = "quicker"
1054 readVersion :: String -> Maybe Version
1055 readVersion = parseReadP $ do
1056 version_branch <- Read.sepBy1
1057 (Read.read <$> Read.munch1 Char.isDigit)
1059 version_tags <- Read.many $ (,)
1060 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
1061 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
1064 parseReadP :: Read.ReadP a -> String -> Maybe a
1066 let p' = Read.readP_to_S p in