1 {-# LANGUAGE OverloadedStrings #-}
2 module Protocol.Election where
4 import Control.Applicative (Applicative(..))
5 import Control.Monad (Monad(..), join, mapM, replicateM, unless, zipWithM)
6 import Control.Monad.Trans.Class (MonadTrans(..))
7 import Control.Monad.Trans.Except (ExceptT, runExcept, throwE, withExceptT)
9 import Data.Either (either)
10 import Data.Eq (Eq(..))
11 import Data.Foldable (Foldable, foldMap, and, sequenceA_)
12 import Data.Function (($), (.), id, const)
13 import Data.Functor (Functor, (<$>))
14 import Data.Functor.Identity (Identity(..))
15 import Data.Functor.Compose (Compose(..))
16 import Data.Maybe (Maybe(..), fromMaybe)
17 import Data.Ord (Ord(..))
18 import Data.Semigroup (Semigroup(..))
19 import Data.Text (Text)
20 import Data.Traversable (Traversable(..))
21 import Data.Tuple (fst, snd, uncurry)
22 import GHC.Natural (minusNaturalMaybe)
23 import Numeric.Natural (Natural)
24 import Prelude (fromIntegral)
25 import Text.Show (Show(..))
26 import qualified Control.Monad.Trans.State.Strict as S
27 import qualified Data.ByteString as BS
28 import qualified Data.List as List
31 import Protocol.Arithmetic
32 import Protocol.Credential
34 -- * Type 'Encryption'
35 -- | ElGamal-like encryption.
36 -- Its security relies on the /Discrete Logarithm problem/.
38 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
39 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
40 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
41 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
42 -- to enable the additive homomorphism.
44 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
45 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
46 data Encryption q = Encryption
47 { encryption_nonce :: G q
48 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
49 -- equal to @('groupGen' '^'encNonce)@
50 , encryption_vault :: G q
51 -- ^ Encrypted 'clear' text,
52 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
55 -- | Additive homomorphism.
56 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
57 instance SubGroup q => Additive (Encryption q) where
58 zero = Encryption one one
60 (encryption_nonce x * encryption_nonce y)
61 (encryption_vault x * encryption_vault y)
63 -- *** Type 'EncryptionNonce'
64 type EncryptionNonce = E
66 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
68 -- WARNING: the secret encryption nonce (@encNonce@)
69 -- is returned alongside the 'Encryption'
70 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
71 -- but this secret @encNonce@ MUST be forgotten after that,
72 -- as it may be used to decipher the 'Encryption'
73 -- without the 'SecretKey' associated with 'pubKey'.
75 Monad m => RandomGen r => SubGroup q =>
77 S.StateT r m (EncryptionNonce q, Encryption q)
78 encrypt pubKey clear = do
80 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
83 { encryption_nonce = groupGen^encNonce
84 , encryption_vault = pubKey ^encNonce * groupGen^clear
88 -- | 'Proof' of knowledge of a discrete logarithm:
89 -- @(secret == logBase base (base^secret))@.
91 { proof_challenge :: Challenge q
92 -- ^ 'Challenge' sent by the verifier to the prover
93 -- to ensure that the prover really has knowledge
94 -- of the secret and is not replaying.
95 -- Actually, 'proof_challenge' is not sent to the prover,
96 -- but derived from the prover's 'Commitment's and statements
97 -- with a collision resistant 'hash'.
98 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
99 , proof_response :: E q
100 -- ^ A discrete logarithm sent by the prover to the verifier,
101 -- as a response to 'proof_challenge'.
103 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
105 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
106 -- * @commitment '==' 'commit' proof base basePowSec '=='
107 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
108 -- * and @basePowSec '==' base'^'sec@,
110 -- then, with overwhelming probability (due to the 'hash' function),
111 -- the prover was not able to choose 'proof_challenge'
112 -- yet was able to compute a 'proof_response' such that
113 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
114 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
115 -- therefore the prover knows 'sec'.
117 -- The prover choses 'commitment' to be a random power of @base@,
118 -- to ensure that each 'prove' does not reveal any information about its secret.
122 -- | Zero-knowledge proof.
124 -- A protocol is /zero-knowledge/ if the verifier
125 -- learns nothing from the protocol except that the prover
128 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
129 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
130 newtype ZKP = ZKP BS.ByteString
132 -- ** Type 'Challenge'
136 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
137 -- by 'hash'ing them (eventually with other 'Commitment's).
139 -- Used in 'prove' it enables a Fiat-Shamir transformation
140 -- of an /interactive zero-knowledge/ (IZK) proof
141 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
142 -- That is to say that the verifier does not have
143 -- to send a 'Challenge' to the prover.
144 -- Indeed, the prover now handles the 'Challenge'
145 -- which becomes a (collision resistant) 'hash'
146 -- of the prover's commitments (and statements to be a stronger proof).
147 type Oracle list q = list (Commitment q) -> Challenge q
149 -- | @('prove' sec commitBases oracle)@
150 -- returns a 'Proof' that @sec@ is known
151 -- (by proving the knowledge of its discrete logarithm).
153 -- The 'Oracle' is given 'Commitment's equal to the 'commitBases'
154 -- raised to the power of the secret nonce of the 'Proof',
155 -- as those are the 'Commitment's that the verifier will obtain
156 -- when composing the 'proof_challenge' and 'proof_response' together
159 -- NOTE: @sec@ is @secKey@ in 'signature_proof' or @encNonce@ in 'proveEncryption'.
161 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
162 -- the statement must be included in the 'hash' (not only the commitments).
164 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
165 -- does not reveal any information regarding the secret @sec@,
166 -- because two 'Proof's using the same 'Commitment'
167 -- can be used to deduce @sec@ (using the special-soundness).
169 Monad m => RandomGen r => SubGroup q => Functor list =>
170 E q -> list (G q) -> Oracle list q -> S.StateT r m (Proof q)
171 prove sec commitBases oracle = do
173 let commitments = (^ nonce) <$> commitBases
174 let proof_challenge = oracle commitments
177 , proof_response = nonce - sec*proof_challenge
180 -- | @('fakeProof')@ returns a 'Proof'
181 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
182 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
183 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
184 -- as a 'Proof' returned by 'prove'.
186 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
187 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
188 fakeProof :: Monad m => RandomGen r => SubGroup q => S.StateT r m (Proof q)
190 proof_challenge <- random
191 proof_response <- random
194 -- ** Type 'Commitment'
195 -- | A commitment from the prover to the verifier.
196 -- It's a power of 'groupGen' chosen randomly by the prover
197 -- when making a 'Proof' with 'prove'.
200 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
201 -- from the given 'Proof' with the knowledge of the verifier.
202 commit :: SubGroup q => Proof q -> G q -> G q -> Commitment q
203 commit Proof{..} base basePowSec =
204 base^proof_response *
205 basePowSec^proof_challenge
206 -- NOTE: Contrary to some textbook presentations,
207 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
208 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
209 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
210 {-# INLINE commit #-}
212 -- * Type 'Disjunction'
213 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
214 -- it's used in 'proveEncryption' to generate a 'Proof'
215 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
218 booleanDisjunctions :: SubGroup q => [Disjunction q]
219 booleanDisjunctions = List.take 2 groupGenInverses
221 intervalDisjunctions :: SubGroup q => Opinion q -> Opinion q -> [Disjunction q]
222 intervalDisjunctions mini maxi =
223 List.genericTake (fromMaybe 0 $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
224 List.genericDrop (nat mini) $
228 -- | Index of a 'Disjunction' within a list of them.
229 -- It is encrypted as an 'E'xponent by 'encrypt'.
232 -- ** Type 'DisjProof'
233 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
234 -- is indexing a 'Disjunction' within a list of them,
235 -- without revealing which 'Opinion' it is.
236 newtype DisjProof q = DisjProof [Proof q]
239 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
240 -- returns a 'DisjProof' that 'enc' 'encrypt's
241 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
243 -- The prover proves that it knows an 'encNonce', such that:
244 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
246 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
248 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
250 Monad m => RandomGen r => SubGroup q =>
251 PublicKey q -> ZKP ->
252 ([Disjunction q],[Disjunction q]) ->
253 (EncryptionNonce q, Encryption q) ->
254 S.StateT r m (DisjProof q)
255 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
256 -- Fake proofs for all 'Disjunction's except the genuine one.
257 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
258 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
259 let fakeChallengeSum =
260 sum (proof_challenge <$> prevFakeProofs) +
261 sum (proof_challenge <$> nextFakeProofs)
262 let statement = encryptionStatement voterZKP enc
263 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
264 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
265 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
266 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
267 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
268 let challenge = hash statement commitments in
269 let genuineChallenge = challenge - fakeChallengeSum in
271 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
272 -- thus (sum (proof_challenge <$> proofs) == challenge)
273 -- as checked in 'verifyEncryption'.
274 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
275 return (DisjProof proofs)
278 Monad m => SubGroup q =>
279 PublicKey q -> ZKP ->
280 [Disjunction q] -> (Encryption q, DisjProof q) ->
281 ExceptT ErrorVerifyEncryption m Bool
282 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
283 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
285 throwE $ ErrorVerifyEncryption_InvalidProofLength
286 (fromIntegral $ List.length proofs)
287 (fromIntegral $ List.length disjs)
289 return $ challengeSum ==
290 hash (encryptionStatement voterZKP enc) (join commitments)
292 challengeSum = sum (proof_challenge <$> proofs)
295 encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
296 encryptionStatement (ZKP voterZKP) Encryption{..} =
297 "prove|"<>voterZKP<>"|"
298 <> bytesNat encryption_nonce<>","
299 <> bytesNat encryption_vault<>"|"
301 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
302 -- returns the 'Commitment's with only the knowledge of the verifier.
304 -- For the prover the 'Proof' comes from @fakeProof@,
305 -- and for the verifier the 'Proof' comes from the prover.
306 encryptionCommitments ::
308 PublicKey q -> Encryption q ->
309 Disjunction q -> Proof q -> [G q]
310 encryptionCommitments elecPubKey Encryption{..} disj proof =
311 [ commit proof groupGen encryption_nonce
312 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
313 -- base==groupGen, basePowSec==groupGen^encNonce.
314 , commit proof elecPubKey (encryption_vault*disj)
315 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
316 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
317 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
320 -- ** Type 'ErrorVerifyEncryption'
321 -- | Error raised by 'verifyEncryption'.
322 data ErrorVerifyEncryption
323 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
324 -- ^ When the number of proofs is different than
325 -- the number of 'Disjunction's.
329 data Question q = Question
330 { question_text :: Text
331 , question_choices :: [Text]
332 , question_mini :: Opinion q
333 , question_maxi :: Opinion q
334 -- , question_blank :: Maybe Bool
335 } deriving (Eq, Show)
338 data Answer q = Answer
339 { answer_opinions :: [(Encryption q, DisjProof q)]
340 -- ^ Encrypted 'Opinion' for each 'question_choices'
341 -- with a 'DisjProof' that they belong to [0,1].
342 , answer_sumProof :: DisjProof q
343 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
344 -- is an element of @[mini..maxi]@.
345 -- , answer_blankProof ::
348 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
349 -- returns an 'Answer' validable by 'verifyAnswer',
350 -- unless an 'ErrorAnswer' is returned.
352 Monad m => RandomGen r => SubGroup q =>
353 PublicKey q -> ZKP ->
354 Question q -> [Bool] ->
355 S.StateT r (ExceptT ErrorAnswer m) (Answer q)
356 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
357 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
359 ErrorAnswer_WrongSumOfOpinions
363 | List.length opinions /= List.length question_choices =
365 ErrorAnswer_WrongNumberOfOpinions
366 (fromIntegral $ List.length opinions)
367 (fromIntegral $ List.length question_choices)
369 encryptions <- encrypt elecPubKey `mapM` opinions
370 individualProofs <- zipWithM
371 (\opinion -> proveEncryption elecPubKey zkp $
373 then ([booleanDisjunctions List.!!0],[])
374 else ([],[booleanDisjunctions List.!!1]))
375 opinionByChoice encryptions
376 sumProof <- proveEncryption elecPubKey zkp
377 (List.tail <$> List.genericSplitAt
378 (nat (opinionsSum - question_mini))
379 (intervalDisjunctions question_mini question_maxi))
380 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
381 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
384 { answer_opinions = List.zip
385 (snd <$> encryptions) -- NOTE: drop encNonce
387 , answer_sumProof = sumProof
390 opinionsSum = sum opinions
391 opinions = (\o -> if o then one else zero) <$> opinionByChoice
395 PublicKey q -> ZKP ->
396 Question q -> Answer q -> Bool
397 verifyAnswer elecPubKey zkp Question{..} Answer{..}
398 | List.length question_choices /= List.length answer_opinions = False
399 | otherwise = either (const False) id $ runExcept $ do
401 verifyEncryption elecPubKey zkp booleanDisjunctions
402 `traverse` answer_opinions
403 validSum <- verifyEncryption elecPubKey zkp
404 (intervalDisjunctions question_mini question_maxi)
405 ( sum (fst <$> answer_opinions)
407 return (and validOpinions && validSum)
409 -- ** Type 'ErrorAnswer'
410 -- | Error raised by 'encryptAnswer'.
412 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
413 -- ^ When the number of opinions is different than
414 -- the number of choices ('question_choices').
415 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
416 -- ^ When the sum of opinions is not within the bounds
417 -- of 'question_mini' and 'question_maxi'.
421 data Election q = Election
422 { election_name :: Text
423 , election_description :: Text
424 , election_publicKey :: PublicKey q
425 , election_questions :: [Question q]
426 , election_uuid :: UUID
427 , election_hash :: Hash -- TODO: serialize to JSON to calculate this
431 newtype Hash = Hash Text
432 deriving (Eq,Ord,Show)
435 data Ballot q = Ballot
436 { ballot_answers :: [Answer q]
437 , ballot_signature :: Maybe (Signature q)
438 , ballot_election_uuid :: UUID
439 , ballot_election_hash :: Hash
442 -- | @('encryptBallot' elec ('Just' secKey) opinionsByQuest)@
443 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
444 -- where 'opinionsByQuest' is a list of 'Opinion's
445 -- on each 'question_choices' of each 'election_questions'.
447 Monad m => RandomGen r => SubGroup q =>
448 Election q -> Maybe (SecretKey q) -> [[Bool]] ->
449 S.StateT r (ExceptT ErrorBallot m) (Ballot q)
450 encryptBallot Election{..} secKeyMay opinionsByQuest
451 | List.length election_questions /= List.length opinionsByQuest =
453 ErrorBallot_WrongNumberOfAnswers
454 (fromIntegral $ List.length opinionsByQuest)
455 (fromIntegral $ List.length election_questions)
457 let (voterKeys, voterZKP) =
459 Nothing -> (Nothing, ZKP "")
461 ( Just (secKey, pubKey)
462 , ZKP (bytesNat pubKey) )
463 where pubKey = publicKey secKey
465 S.mapStateT (withExceptT ErrorBallot_Answer) $
466 zipWithM (encryptAnswer election_publicKey voterZKP)
467 election_questions opinionsByQuest
468 ballot_signature <- case voterKeys of
469 Nothing -> return Nothing
470 Just (secKey, signature_publicKey) -> do
472 prove secKey (Identity groupGen) $
473 \(Identity commitment) ->
475 -- NOTE: the order is unusual, the commitments are first
476 -- then comes the statement. Best guess is that
477 -- this is easier to code due to their respective types.
478 (signatureCommitments voterZKP commitment)
479 (signatureStatement ballot_answers)
480 return $ Just Signature{..}
483 , ballot_election_hash = election_hash
484 , ballot_election_uuid = election_uuid
488 verifyBallot :: SubGroup q => Election q -> Ballot q -> Bool
489 verifyBallot Election{..} Ballot{..} =
490 ballot_election_uuid == election_uuid &&
491 ballot_election_hash == election_hash &&
492 List.length election_questions == List.length ballot_answers &&
493 let (isValidSign, zkpSign) =
494 case ballot_signature of
495 Nothing -> (True, ZKP "")
496 Just Signature{..} ->
497 let zkp = ZKP (bytesNat signature_publicKey) in
499 proof_challenge signature_proof == hash
500 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
501 (signatureStatement ballot_answers)
504 List.zipWith (verifyAnswer election_publicKey zkpSign)
505 election_questions ballot_answers
507 -- ** Type 'Signature'
508 -- | Schnorr-like signature.
510 -- Used by each voter to sign his/her encrypted 'Ballot'
511 -- using his/her 'Credential',
512 -- in order to avoid ballot stuffing.
513 data Signature q = Signature
514 { signature_publicKey :: PublicKey q
515 -- ^ Verification key.
516 , signature_proof :: Proof q
521 -- | @('signatureStatement' answers)@
522 -- returns the encrypted material to be signed:
523 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
524 signatureStatement :: Foldable f => SubGroup q => f (Answer q) -> [G q]
526 foldMap $ \Answer{..} ->
527 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
528 [encryption_nonce, encryption_vault]
530 -- | @('signatureCommitments' voterZKP commitment)@
531 signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
532 signatureCommitments (ZKP voterZKP) commitment =
533 "sig|"<>voterZKP<>"|"<>bytesNat commitment<>"|"
535 -- ** Type 'ErrorBallot'
536 -- | Error raised by 'encryptBallot'.
538 = ErrorBallot_WrongNumberOfAnswers Natural Natural
539 -- ^ When the number of answers
540 -- is different than the number of questions.
541 | ErrorBallot_Answer ErrorAnswer
542 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.