2 {-# LANGUAGE DataKinds #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE ViewPatterns #-}
5 module Protocol.Election where
7 import Control.Monad (Monad(..), mapM, sequence)
9 import Data.Eq (Eq(..))
10 import Data.Foldable (Foldable, foldMap, and)
11 import Data.Function (($), (.))
12 import Data.Functor ((<$>))
13 import Data.Maybe (Maybe(..), fromJust)
14 import Data.Ord (Ord(..))
15 import Data.Proxy (Proxy(..))
16 import Data.Semigroup (Semigroup(..))
17 import Data.String (IsString(..))
18 import Data.Text (Text)
19 import Data.Tuple (fst, snd, uncurry)
21 import Text.Show (Show(..))
22 import qualified Control.Monad.Trans.State.Strict as S
23 import qualified Data.ByteString as BS
24 import qualified Data.List as List
26 import Protocol.Arithmetic
27 import Utils.MeasuredList as ML
28 import qualified Utils.Natural as Nat
29 import qualified Utils.Constraint as Constraint
31 -- * Type 'Encryption'
32 -- | ElGamal-like encryption.
33 data Encryption q = Encryption
34 { encryption_nonce :: G q
35 -- ^ Public part of the random 'secNonce': @('groupGen' '^'r)@
36 , encryption_vault :: G q
37 -- ^ Encrypted opinion: @('pubKey' '^'r '*' 'groupGen' '^'opinion)@
40 -- | Additive homomorphism.
41 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
42 instance SubGroup q => Additive (Encryption q) where
43 zero = Encryption one one
45 (encryption_nonce x * encryption_nonce y)
46 (encryption_vault x * encryption_vault y)
48 -- ** Type 'PublicKey'
50 -- ** Type 'SecretKey'
52 -- *** Type 'SecretNonce'
55 -- | Zero-knowledge proof
56 type ZKP = BS.ByteString
59 -- | Index of a 'Disjunction' within a 'ML.MeasuredList' of them.
60 -- It is encoded as an 'E'xponent in 'encrypt'.
61 type Opinion = Nat.Index
63 -- | @('encrypt' pubKey opinion)@ returns an ElGamal-like 'Encryption'.
65 -- WARNING: the secret nonce is returned alongside the 'Encryption'
66 -- in order to prove the validity of the encrypted content in 'nizkProof',
67 -- but this secret nonce MUST be forgotten after that,
68 -- as it may be used to decipher the 'Encryption'
69 -- without the secret key associated with 'pubKey'.
71 Monad m => RandomGen r => SubGroup q =>
72 PublicKey q -> Opinion ds ->
73 S.StateT r m (SecretNonce q, Encryption q)
74 encrypt pubKey (Nat.Index o) = do
75 let opinion = inE (natVal o)
77 -- NOTE: preserve the 'secNonce' for 'nizkProof'.
80 { encryption_nonce = groupGen^secNonce
81 , encryption_vault = pubKey ^secNonce * groupGen^opinion
82 -- NOTE: pubKey == groupGen ^ secKey
83 -- NOTE: 'opinion' is put as exponent in order
84 -- to make an additive homomorphism
85 -- instead of a multiplicative homomorphism.
90 { proof_challenge :: Challenge q
91 , proof_response :: E q
94 -- ** Type 'Challenge'
97 type Oracle q = [Commitment q] -> Challenge q
99 -- | Fiat-Shamir transformation
100 -- of an interactive zero-knowledge (IZK) proof
101 -- into a non-interactive zero-knowledge (NIZK) proof.
103 Monad m => RandomGen r => SubGroup q =>
104 SecretNonce q -> [Commitment q] -> Oracle q -> S.StateT r m (Proof q)
105 nizkProof secNonce commits oracle = do
107 let commitments = (^ nonce) <$> commits
108 let proof_challenge = oracle commitments
111 , proof_response = nonce + secNonce*proof_challenge
114 -- ** Type 'Commitment'
117 -- ** Type 'Disjunction'
118 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
119 -- it's used in 'proveEncryption' to generate a 'Proof'
120 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
123 booleanDisjunctions :: SubGroup q => ML.MeasuredList 2 (Disjunction q)
124 booleanDisjunctions = fromJust $ ML.fromList $ List.take 2 groupGenInverses
126 intervalDisjunctions ::
130 ML.MeasuredList (maxi-mini) (Disjunction q)
131 intervalDisjunctions Bounds{}
132 | Constraint.Proof <- (Nat.<=) @mini @maxi =
135 List.genericTake (Nat.nat @(maxi-mini)) $
136 List.genericDrop (Nat.nat @mini) $
139 -- ** Type 'ValidityProof'
140 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
141 -- is indexing a 'Disjunction' within a list of them,
142 -- without knowing which 'Opinion' it is.
143 newtype ValidityProof disjs q = ValidityProof (ML.MeasuredList disjs (Proof q))
146 encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
147 encryptionStatement zkp Encryption{..} =
149 fromString (show (natG encryption_nonce))<>","<>
150 fromString (show (natG encryption_vault))<>"|"
155 Monad m => RandomGen r => SubGroup q =>
156 PublicKey q -> ZKP ->
157 ML.MeasuredList disjs (Disjunction q) -> Opinion disjs ->
158 (SecretNonce q, Encryption q) ->
159 S.StateT r m (ValidityProof disjs q)
160 proveEncryption pubKey zkp disjs
161 (Nat.Index (o::Proxy o))
162 (secNonce, enc@Encryption{..})
163 -- NOTE: the 'Constraint.Proof's below are needed to prove
164 -- that the returned 'ValidityProof' has the same length
165 -- than the given list of 'Disjunction's.
166 | Constraint.Proof <- (Nat.+<=) @o @1 @disjs -- prove that o+1<=disjs implies 1<=disjs-o and o<=disjs
167 , Constraint.Proof <- (Nat.<=) @o @disjs -- prove that o<=disjs implies disjs-o is a Natural and o+(disjs-o) ~ disjs
168 , Constraint.Proof <- (Nat.<=) @1 @(disjs-o) -- prove that ((disjs-o)-1)+1 ~ disjs-o
170 let (prevDisjs, ML.uncons -> (_indexedDisj,nextDisjs)) = ML.splitAt o disjs
171 prevFakes <- fakeProof `mapM` prevDisjs
172 nextFakes <- fakeProof `mapM` nextDisjs
175 sum (proof_challenge . fst <$> prevFakes) +
176 sum (proof_challenge . fst <$> nextFakes)
177 genuineProof <- nizkProof secNonce [groupGen, pubKey] $
181 foldMap snd prevFakes <>
183 foldMap snd nextFakes in
184 -- NOTE: this is a so-called strong Fiat-Shamir transformation (not a weak):
185 -- because the statement is included in the hash (not only the commitments).
186 hash (encryptionStatement zkp enc) commitments + challengeSum
191 (ML.cons genuineProof (fst <$> nextFakes))
193 fakeProof :: Disjunction q -> S.StateT r m (Proof q, [Commitment q])
195 proof_challenge <- random
196 proof_response <- random
198 [ groupGen^proof_response / encryption_nonce ^proof_challenge
199 , pubKey ^proof_response / (encryption_vault*disj)^proof_challenge
201 return (Proof{..}, commitments)
203 validateEncryption ::
205 PublicKey q -> ZKP ->
206 ML.MeasuredList n (Disjunction q) ->
207 (Encryption q, ValidityProof n q) -> Bool
208 validateEncryption pubKey zkp disjs (enc@Encryption{..}, ValidityProof proofs) =
209 hash (encryptionStatement zkp enc) commitments == challengeSum
211 challengeSum = sum (proof_challenge <$> proofs)
212 commitments = foldMap commitment (ML.zip disjs proofs)
213 where commitment (disj, Proof{..}) =
216 -- y1 = encryption_nonce
217 -- y2 = (encryption_vault * disj)
218 -- com1 = g^res / y1 ^ ch
219 -- com2 = h^res / y2 ^ ch
220 [ groupGen^proof_response / encryption_nonce ^proof_challenge
221 , pubKey ^proof_response / (encryption_vault*disj)^proof_challenge
225 data Question choices (mini::Nat) (maxi::Nat) q =
227 { question_text :: Text
228 , question_answers :: ML.MeasuredList choices Text
229 , question_bounds :: Bounds mini maxi
230 -- , question_blank :: Maybe Bool
231 } deriving (Eq, Show)
234 data Bounds mini maxi =
235 ((mini<=maxi), Nat.Known mini, Nat.Known maxi) =>
236 Bounds (Proxy mini) (Proxy maxi)
237 instance Show (Bounds mini maxi) where
238 showsPrec p Bounds{} = showsPrec p (Nat.nat @mini, Nat.nat @maxi)
239 instance Eq (Bounds mini maxi) where
243 data Answer choices mini maxi q = Answer
244 { answer_opinions :: ML.MeasuredList choices (Encryption q, ValidityProof 2 q)
245 -- ^ Encrypted 'Opinion' for each 'question_answers'
246 -- with a 'ValidityProof' that they belong to [0,1].
247 , answer_sumProof :: ValidityProof (maxi-mini) q
248 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
249 -- is an element of ['mini'..'maxi'].
250 -- , answer_blankProof ::
253 -- | @('answer' pubKey zkp quest opinions)@
254 -- returns a validable 'Answer',
255 -- unless the given 'opinions' do not respect 'question_bounds'.
257 forall m r q mini maxi choices.
258 Monad m => RandomGen r => SubGroup q =>
259 PublicKey q -> ZKP ->
260 Question choices mini maxi q ->
261 ML.MeasuredList choices (Opinion 2) ->
262 S.StateT r m (Maybe (Answer choices mini maxi q))
263 answer pubKey zkp Question{..} opinions
264 | Bounds{} <- question_bounds
265 , SomeNat (_opinionsSum::Proxy opinionsSum) <-
266 someNatVal $ sum $ (\(Nat.Index o) -> natVal o) <$> opinions
267 -- prove that opinionsSum-mini is a Natural
268 , Just Constraint.Proof <- (Nat.<=?) @mini @opinionsSum
269 -- prove that (opinionsSum-mini)+1 is a Natural
270 , Constraint.Proof <- (Nat.+) @(opinionsSum-mini) @1
271 -- prove that maxi-mini is a Natural
272 , Constraint.Proof <- (Nat.<=) @mini @maxi
273 -- prove that (opinionsSum-mini)+1 <= maxi-mini
274 , Just Constraint.Proof <- (Nat.<=?) @((opinionsSum-mini)+1) @(maxi-mini)
276 encryptions <- encrypt pubKey `mapM` opinions
278 sequence $ ML.zipWith
279 (proveEncryption pubKey zkp booleanDisjunctions)
282 proveEncryption pubKey zkp
283 (intervalDisjunctions question_bounds)
284 (Nat.Index $ Proxy @(opinionsSum-mini))
285 ( sum (fst <$> encryptions)
286 , sum (snd <$> encryptions) )
288 { answer_opinions = ML.zip
289 (snd <$> encryptions) -- NOTE: drop secNonce
291 , answer_sumProof = sumProof
293 | otherwise = return Nothing
297 PublicKey q -> ZKP ->
298 Question choices mini maxi q ->
299 Answer choices mini maxi q -> Bool
300 validateAnswer pubKey zkp Question{..} Answer{..} =
301 and (validateEncryption pubKey zkp booleanDisjunctions <$> answer_opinions) &&
302 validateEncryption pubKey zkp
303 (intervalDisjunctions question_bounds)
304 ( sum (fst <$> answer_opinions)
308 data Election quests choices mini maxi q = Election
309 { election_name :: Text
310 , election_description :: Text
311 , election_publicKey :: PublicKey q
312 , election_questions :: ML.MeasuredList quests (Question choices mini maxi q)
313 , election_uuid :: UUID
314 , election_hash :: Hash
318 newtype UUID = UUID Text
319 deriving (Eq,Ord,Show)
322 newtype Hash = Hash Text
323 deriving (Eq,Ord,Show)
326 data Ballot quests choices mini maxi q = Ballot
327 { ballot_answers :: ML.MeasuredList quests (Answer choices mini maxi q)
328 , ballot_signature :: Maybe (Signature q)
329 , ballot_election_uuid :: UUID
330 , ballot_election_hash :: Hash
337 Election quests choices mini maxi q ->
338 Maybe (SecretKey q) ->
339 ML.MeasuredList quests (ML.MeasuredList choices (Opinion 2)) ->
340 S.StateT r m (Maybe (Ballot quests choices mini maxi q))
341 ballot Election{..} secKeyMay opinionsByQuest = do
344 Nothing -> (Nothing, "")
346 ( Just (secKey, pubKey)
347 , fromString (show (natG pubKey)) )
348 where pubKey = groupGen ^ secKey
351 uncurry (answer election_publicKey zkp) `mapM`
352 ML.zip election_questions opinionsByQuest
353 case answersByQuestMay of
354 Nothing -> return Nothing
355 Just answersByQuest -> do
356 ballot_signature <- case keysMay of
357 Nothing -> return Nothing
358 Just (secKey, pubKey) -> do
360 let commitment = groupGen ^ w
361 let proof_challenge = hash
362 (signatureCommitments zkp commitment)
363 (signatureStatement answersByQuest)
364 return $ Just Signature
365 { signature_publicKey = pubKey
366 , signature_proof = Proof
368 , proof_response = w - secKey*proof_challenge
372 { ballot_answers = answersByQuest
373 , ballot_election_hash = election_hash
374 , ballot_election_uuid = election_uuid
380 Election quests choices mini maxi q ->
381 Ballot quests choices mini maxi q ->
383 validateBallot Election{..} Ballot{..} =
384 ballot_election_uuid == election_uuid &&
385 ballot_election_hash == election_hash &&
386 let (validSign, zkp) =
387 case ballot_signature of
388 Nothing -> (True, "")
389 Just (Signature pubKey Proof{..}) ->
390 let zkp = fromString (show (natG pubKey)) in
392 let commitment = groupGen ^ proof_response * pubKey ^ proof_challenge in
393 let prefix = signatureCommitments zkp commitment in
394 let contents = signatureStatement ballot_answers in
395 hash prefix contents == proof_challenge
399 and (ML.zipWith (validateAnswer election_publicKey zkp)
400 election_questions ballot_answers)
402 -- ** Type 'Signature'
403 -- | Schnorr-like signature.
405 -- Used to avoid 'Ballot' stuffing.
406 data Signature q = Signature
407 { signature_publicKey :: PublicKey q
408 , signature_proof :: Proof q
411 signatureStatement ::
412 Foldable f => SubGroup q =>
413 f (Answer choices mini maxi q) -> [G q]
415 foldMap $ \Answer{..} ->
416 (`foldMap` answer_opinions) $ \(Encryption{..}, _vp) ->
417 [encryption_nonce, encryption_vault]
419 signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
420 signatureCommitments zkp commitment =
421 "sig|"<>zkp<>"|"<>fromString (show (natG commitment))<>"|"