1 {-# LANGUAGE OverloadedStrings #-}
2 module Protocol.Election where
4 import Control.Applicative (Applicative(..))
5 import Control.Monad (Monad(..), join, mapM, 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, curry)
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, equal to @('pubKey' '^'r '*' 'groupGen' '^'clear)@
54 -- | Additive homomorphism.
55 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
56 instance SubGroup q => Additive (Encryption q) where
57 zero = Encryption one one
59 (encryption_nonce x * encryption_nonce y)
60 (encryption_vault x * encryption_vault y)
62 -- *** Type 'EncryptionNonce'
63 type EncryptionNonce = E
65 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
67 -- WARNING: the secret encryption nonce (@encNonce@)
68 -- is returned alongside the 'Encryption'
69 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
70 -- but this secret @encNonce@ MUST be forgotten after that,
71 -- as it may be used to decipher the 'Encryption'
72 -- without the 'SecretKey' associated with 'pubKey'.
74 Monad m => RandomGen r => SubGroup q =>
76 S.StateT r m (EncryptionNonce q, Encryption q)
77 encrypt pubKey clear = do
79 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
82 { encryption_nonce = groupGen^encNonce
83 , encryption_vault = pubKey ^encNonce * groupGen^clear
87 -- | 'Proof' of knowledge of a discrete logarithm:
88 -- @(secret == logBase base (base^secret))@.
90 { proof_challenge :: Challenge q
91 -- ^ 'Challenge' sent by the verifier to the prover
92 -- to ensure that the prover really has knowledge
93 -- of the secret and is not replaying.
94 -- Actually, 'proof_challenge' is not sent to the prover,
95 -- but derived from the prover's 'Commitment's and statements
96 -- with a collision resistant 'hash'.
97 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
98 , proof_response :: E q
99 -- ^ A discrete logarithm sent by the prover to the verifier,
100 -- as a response to 'proof_challenge'.
102 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@
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 -- @(commitment '==' base'^'nonce)@.
112 -- Therefore by expanding 'commitment':
113 -- @('proof_response' '==' logBase base (base'^'nonce) '-' logBase basePowSec (basePowSec '^' 'proof_challenge'))@,
114 -- which means that the prover must have known 'nonce' and 'sec'
115 -- to compute 'proof_response' efficiently with:
116 -- @('proof_response' '==' nonce '-' sec '*' 'proof_challenge')@,
118 -- The 'nonce' is introduced to ensure each 'prove' does not reveal
119 -- any information regarding the prover's secret 'sec',
120 -- by being randomly chosen by the prover.
124 -- | Zero-knowledge proof
126 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
127 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
129 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
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.
152 -- The 'Oracle' is given the 'commitBases'
153 -- raised to the power of the secret nonce of the 'Proof',
154 -- as those are the 'commitBases' that the verifier will obtain
155 -- when composing the 'proof_challenge' and 'proof_response' together
158 -- NOTE: 'sec' is @secKey@ in 'signature_proof' or @encNonce@ in 'proveEncryption'.
160 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
161 -- the statement must be included in the 'hash' (not only the commitments).
163 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
164 -- does not reveal any information regarding the secret 'sec'.
166 Monad m => RandomGen r => SubGroup q => Functor list =>
167 E q -> list (Commitment q) -> Oracle list q -> S.StateT r m (Proof q)
168 prove sec commitBases oracle = do
170 let proof_challenge = oracle $ (^ nonce) <$> commitBases
173 , proof_response = nonce - sec*proof_challenge
176 -- ** Type 'Commitment'
179 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
180 -- from the given 'Proof' with the knowledge of the verifier.
181 commit :: SubGroup q => Proof q -> G q -> G q -> Commitment q
182 commit Proof{..} base basePowSec =
183 base^proof_response *
184 basePowSec^proof_challenge
185 -- NOTE: Contrary to some textbook presentations,
186 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
187 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
188 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
189 {-# INLINE commit #-}
191 -- * Type 'Disjunction'
192 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
193 -- it's used in 'proveEncryption' to generate a 'Proof'
194 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
197 booleanDisjunctions :: SubGroup q => [Disjunction q]
198 booleanDisjunctions = List.take 2 groupGenInverses
200 intervalDisjunctions :: SubGroup q => Opinion q -> Opinion q -> [Disjunction q]
201 intervalDisjunctions mini maxi =
202 List.genericTake (fromMaybe 0 $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
203 List.genericDrop (nat mini) $
207 -- | Index of a 'Disjunction' within a list of them.
208 -- It is encrypted as an 'E'xponent by 'encrypt'.
211 -- ** Type 'DisjProof'
212 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
213 -- is indexing a 'Disjunction' within a list of them,
214 -- without revealing which 'Opinion' it is.
215 newtype DisjProof q = DisjProof [Proof q]
218 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
219 -- returns a 'DisjProof' that 'enc' 'encrypt's
220 -- the 'Disjunction's between 'prevDisjs' and 'nextDisjs'.
222 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
225 Monad m => RandomGen r => SubGroup q =>
226 PublicKey q -> ZKP ->
227 ([Disjunction q],[Disjunction q]) ->
228 (EncryptionNonce q, Encryption q) ->
229 S.StateT r m (DisjProof q)
230 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
231 -- Fake proofs for all values except the correct one.
232 prevFakes <- fakeProof `mapM` prevDisjs
233 nextFakes <- fakeProof `mapM` nextDisjs
234 let prevProofs = fst <$> prevFakes
235 let nextProofs = fst <$> nextFakes
237 sum (proof_challenge <$> prevProofs) +
238 sum (proof_challenge <$> nextProofs)
239 let statement = encryptionStatement voterZKP enc
240 correctProof <- prove encNonce [groupGen, elecPubKey] $
242 \correctCommitments ->
244 foldMap snd prevFakes <>
245 correctCommitments <>
246 foldMap snd nextFakes in
247 hash statement commitments - challengeSum
248 return $ DisjProof $ prevProofs <> (correctProof : nextProofs)
250 fakeProof :: Disjunction q -> S.StateT r m (Proof q, [Commitment q])
252 -- Returns 'Commitment's verifiables by the verifier,
253 -- but computed from random 'proof_challenge' and 'proof_response'
254 -- instead of correct ones.
255 proof_challenge <- random
256 proof_response <- random
257 let proof = Proof{..}
258 return (proof, encryptionCommitments elecPubKey enc disj proof)
263 PublicKey q -> ZKP ->
265 (Encryption q, DisjProof q) ->
266 ExceptT ErrorVerifyEncryption m Bool
267 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
268 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
270 throwE $ ErrorVerifyEncryption_InvalidProofLength
271 (fromIntegral $ List.length proofs)
272 (fromIntegral $ List.length disjs)
274 return $ challengeSum ==
275 hash (encryptionStatement voterZKP enc) (join commitments)
277 challengeSum = sum (proof_challenge <$> proofs)
280 encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
281 encryptionStatement (ZKP voterZKP) Encryption{..} =
282 "prove|"<>voterZKP<>"|"
283 <> bytesNat encryption_nonce<>","
284 <> bytesNat encryption_vault<>"|"
286 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
287 -- returns the 'Commitment's with only the knowledge of the verifier.
289 -- The 'Proof' comes from 'prove' of @fakeProof@ in 'proveEncryption'.
290 encryptionCommitments ::
292 PublicKey q -> Encryption q ->
293 Disjunction q -> Proof q -> [G q]
294 encryptionCommitments elecPubKey Encryption{..} disj proof =
295 [ commit proof groupGen encryption_nonce
296 -- == groupGen ^ nonce if 'Proof' comes from 'prove'
297 , commit proof elecPubKey (encryption_vault*disj)
298 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
299 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
302 -- ** Type 'ErrorVerifyEncryption'
303 -- | Error raised by 'verifyEncryption'.
304 data ErrorVerifyEncryption
305 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
306 -- ^ When the number of proofs is different than
307 -- the number of 'Disjunction's.
311 data Question q = Question
312 { question_text :: Text
313 , question_choices :: [Text]
314 , question_mini :: Opinion q
315 , question_maxi :: Opinion q
316 -- , question_blank :: Maybe Bool
317 } deriving (Eq, Show)
320 data Answer q = Answer
321 { answer_opinions :: [(Encryption q, DisjProof q)]
322 -- ^ Encrypted 'Opinion' for each 'question_choices'
323 -- with a 'DisjProof' that they belong to [0,1].
324 , answer_sumProof :: DisjProof q
325 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
326 -- is an element of @[mini..maxi]@.
327 -- , answer_blankProof ::
330 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
331 -- returns an 'Answer' validable by 'verifyAnswer',
332 -- unless an 'ErrorAnswer' is returned.
334 Monad m => RandomGen r => SubGroup q =>
335 PublicKey q -> ZKP ->
336 Question q -> [Bool] ->
337 S.StateT r (ExceptT ErrorAnswer m) (Answer q)
338 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
339 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
341 ErrorAnswer_WrongSumOfOpinions
345 | List.length opinions /= List.length question_choices =
347 ErrorAnswer_WrongNumberOfOpinions
348 (fromIntegral $ List.length opinions)
349 (fromIntegral $ List.length question_choices)
351 encryptions <- encrypt elecPubKey `mapM` opinions
352 individualProofs <- zipWithM
353 (\opinion -> proveEncryption elecPubKey zkp $
355 then ([booleanDisjunctions List.!!0],[])
356 else ([],[booleanDisjunctions List.!!1]))
357 opinionByChoice encryptions
358 sumProof <- proveEncryption elecPubKey zkp
359 (List.tail <$> List.genericSplitAt
360 (nat (opinionsSum - question_mini))
361 (intervalDisjunctions question_mini question_maxi))
362 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
363 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
366 { answer_opinions = List.zip
367 (snd <$> encryptions) -- NOTE: drop encNonce
369 , answer_sumProof = sumProof
372 opinionsSum = sum opinions
373 opinions = (\o -> if o then one else zero) <$> opinionByChoice
377 PublicKey q -> ZKP ->
378 Question q -> Answer q -> Bool
379 verifyAnswer elecPubKey zkp Question{..} Answer{..}
380 | List.length question_choices /= List.length answer_opinions = False
381 | otherwise = either (const False) id $ runExcept $ do
383 verifyEncryption elecPubKey zkp booleanDisjunctions
384 `traverse` answer_opinions
385 validSum <- verifyEncryption elecPubKey zkp
386 (intervalDisjunctions question_mini question_maxi)
387 ( sum (fst <$> answer_opinions)
389 return (and validOpinions && validSum)
391 -- ** Type 'ErrorAnswer'
392 -- | Error raised by 'encryptAnswer'.
394 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
395 -- ^ When the number of opinions is different than
396 -- the number of choices ('question_choices').
397 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
398 -- ^ When the sum of opinions is not within the bounds
399 -- of 'question_mini' and 'question_maxi'.
403 data Election q = Election
404 { election_name :: Text
405 , election_description :: Text
406 , election_publicKey :: PublicKey q
407 , election_questions :: [Question q]
408 , election_uuid :: UUID
409 , election_hash :: Hash -- TODO: serialize to JSON to calculate this
413 newtype Hash = Hash Text
414 deriving (Eq,Ord,Show)
417 data Ballot q = Ballot
418 { ballot_answers :: [Answer q]
419 , ballot_signature :: Maybe (Signature q)
420 , ballot_election_uuid :: UUID
421 , ballot_election_hash :: Hash
424 -- | @('encryptBallot' elec ('Just' secKey) opinionsByQuest)@
425 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
426 -- where 'opinionsByQuest' is a list of 'Opinion's
427 -- on each 'question_choices' of each 'election_questions'.
429 Monad m => RandomGen r => SubGroup q =>
430 Election q -> Maybe (SecretKey q) -> [[Bool]] ->
431 S.StateT r (ExceptT ErrorBallot m) (Ballot q)
432 encryptBallot Election{..} secKeyMay opinionsByQuest
433 | List.length election_questions /= List.length opinionsByQuest =
435 ErrorBallot_WrongNumberOfAnswers
436 (fromIntegral $ List.length opinionsByQuest)
437 (fromIntegral $ List.length election_questions)
439 let (voterKeys, voterZKP) =
441 Nothing -> (Nothing, ZKP "")
443 ( Just (secKey, pubKey)
444 , ZKP (bytesNat pubKey) )
445 where pubKey = publicKey secKey
447 S.mapStateT (withExceptT ErrorBallot_Answer) $
448 zipWithM (encryptAnswer election_publicKey voterZKP)
449 election_questions opinionsByQuest
450 ballot_signature <- case voterKeys of
451 Nothing -> return Nothing
452 Just (secKey, signature_publicKey) -> do
454 prove secKey (Identity groupGen) $
455 \(Identity commitment) ->
457 -- NOTE: the order is unusual, the commitments are first
458 -- then comes the statement. Best guess is that
459 -- this is easier to code due to their respective types.
460 (signatureCommitments voterZKP commitment)
461 (signatureStatement ballot_answers)
462 return $ Just Signature{..}
465 , ballot_election_hash = election_hash
466 , ballot_election_uuid = election_uuid
470 verifyBallot :: SubGroup q => Election q -> Ballot q -> Bool
471 verifyBallot Election{..} Ballot{..} =
472 ballot_election_uuid == election_uuid &&
473 ballot_election_hash == election_hash &&
474 List.length election_questions == List.length ballot_answers &&
475 let (isValidSign, zkpSign) =
476 case ballot_signature of
477 Nothing -> (True, ZKP "")
478 Just Signature{..} ->
479 let zkp = ZKP (bytesNat signature_publicKey) in
481 proof_challenge signature_proof == hash
482 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
483 (signatureStatement ballot_answers)
486 List.zipWith (verifyAnswer election_publicKey zkpSign)
487 election_questions ballot_answers
489 -- ** Type 'Signature'
490 -- | Schnorr-like signature.
492 -- Used by each voter to sign his/her encrypted 'Ballot'
493 -- using his/her 'Credential',
494 -- in order to avoid ballot stuffing.
495 data Signature q = Signature
496 { signature_publicKey :: PublicKey q
497 -- ^ Verification key.
498 , signature_proof :: Proof q
503 -- | @('signatureStatement' answers)@
504 -- returns the encrypted material to be signed:
505 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
506 signatureStatement :: Foldable f => SubGroup q => f (Answer q) -> [G q]
508 foldMap $ \Answer{..} ->
509 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
510 [encryption_nonce, encryption_vault]
512 -- | @('signatureCommitments' voterZKP commitment)@
513 signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
514 signatureCommitments (ZKP voterZKP) commitment =
515 "sig|"<>voterZKP<>"|"<>bytesNat commitment<>"|"
517 -- ** Type 'ErrorBallot'
518 -- | Error raised by 'encryptBallot'.
520 = ErrorBallot_WrongNumberOfAnswers Natural Natural
521 -- ^ When the number of answers
522 -- is different than the number of questions.
523 | ErrorBallot_Answer ErrorAnswer
524 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.