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)
20 import Numeric.Natural (Natural)
22 import Text.Show (Show(..))
23 import qualified Control.Monad.Trans.State.Strict as S
24 import qualified Data.ByteString as BS
25 import qualified Data.List as List
27 import Protocol.Arithmetic
28 import Protocol.Credential
29 import Utils.MeasuredList as ML
30 import qualified Utils.Natural as Nat
31 import qualified Utils.Constraint as Constraint
33 -- * Type 'Encryption'
34 -- | ElGamal-like encryption.
35 data Encryption q = Encryption
36 { encryption_nonce :: G q
37 -- ^ Public part of the random 'secNonce': @('groupGen' '^'r)@
38 , encryption_vault :: G q
39 -- ^ Encrypted opinion: @('pubKey' '^'r '*' 'groupGen' '^'opinion)@
42 -- | Additive homomorphism.
43 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
44 instance SubGroup q => Additive (Encryption q) where
45 zero = Encryption one one
47 (encryption_nonce x * encryption_nonce y)
48 (encryption_vault x * encryption_vault y)
50 -- *** Type 'SecretNonce'
53 -- | Zero-knowledge proof
54 type ZKP = BS.ByteString
57 -- | Index of a 'Disjunction' within a 'ML.MeasuredList' of them.
58 -- It is encoded as an 'E'xponent in 'encrypt'.
59 type Opinion = Nat.Index
61 -- | @('encrypt' pubKey opinion)@ returns an ElGamal-like 'Encryption'.
63 -- WARNING: the secret nonce is returned alongside the 'Encryption'
64 -- in order to prove the validity of the encrypted content in 'nizkProof',
65 -- but this secret nonce MUST be forgotten after that,
66 -- as it may be used to decipher the 'Encryption'
67 -- without the secret key associated with 'pubKey'.
69 Monad m => RandomGen r => SubGroup q =>
70 PublicKey q -> Opinion ds ->
71 S.StateT r m (SecretNonce q, Encryption q)
72 encrypt pubKey (Nat.Index o) = do
73 let opinion = inE (natVal o)
75 -- NOTE: preserve the 'secNonce' for 'nizkProof'.
78 { encryption_nonce = groupGen^secNonce
79 , encryption_vault = pubKey ^secNonce * groupGen^opinion
80 -- NOTE: pubKey == groupGen ^ secKey
81 -- NOTE: 'opinion' is put as exponent in order
82 -- to make an additive homomorphism
83 -- instead of a multiplicative homomorphism.
88 { proof_challenge :: Challenge q
89 , proof_response :: E q
92 -- ** Type 'Challenge'
95 type Oracle q = [Commitment q] -> Challenge q
97 -- | Fiat-Shamir transformation
98 -- of an interactive zero-knowledge (IZK) proof
99 -- into a non-interactive zero-knowledge (NIZK) proof.
101 Monad m => RandomGen r => SubGroup q =>
102 SecretNonce q -> [Commitment q] -> Oracle q -> S.StateT r m (Proof q)
103 nizkProof secNonce commits oracle = do
105 let commitments = (^ nonce) <$> commits
106 let proof_challenge = oracle commitments
109 , proof_response = nonce + secNonce*proof_challenge
112 -- ** Type 'Commitment'
115 -- ** Type 'Disjunction'
116 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
117 -- it's used in 'proveEncryption' to generate a 'Proof'
118 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
121 booleanDisjunctions :: SubGroup q => ML.MeasuredList 2 (Disjunction q)
122 booleanDisjunctions = fromJust $ ML.fromList $ List.take 2 groupGenInverses
124 intervalDisjunctions ::
128 ML.MeasuredList (maxi-mini) (Disjunction q)
129 intervalDisjunctions Bounds{}
130 | Constraint.Proof <- (Nat.<=) @mini @maxi =
133 List.genericTake (Nat.nat @(maxi-mini)) $
134 List.genericDrop (Nat.nat @mini) $
137 -- ** Type 'ValidityProof'
138 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
139 -- is indexing a 'Disjunction' within a list of them,
140 -- without knowing which 'Opinion' it is.
141 newtype ValidityProof disjs q = ValidityProof (ML.MeasuredList disjs (Proof q))
144 encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
145 encryptionStatement zkp Encryption{..} =
147 fromString (show (natG encryption_nonce))<>","<>
148 fromString (show (natG encryption_vault))<>"|"
153 Monad m => RandomGen r => SubGroup q =>
154 PublicKey q -> ZKP ->
155 ML.MeasuredList disjs (Disjunction q) -> Opinion disjs ->
156 (SecretNonce q, Encryption q) ->
157 S.StateT r m (ValidityProof disjs q)
158 proveEncryption pubKey zkp disjs
159 (Nat.Index (o::Proxy o))
160 (secNonce, enc@Encryption{..})
161 -- NOTE: the 'Constraint.Proof's below are needed to prove
162 -- that the returned 'ValidityProof' has the same length
163 -- than the given list of 'Disjunction's.
164 | Constraint.Proof <- (Nat.+<=) @o @1 @disjs -- prove that o+1<=disjs implies 1<=disjs-o and o<=disjs
165 , Constraint.Proof <- (Nat.<=) @o @disjs -- prove that o<=disjs implies disjs-o is a Natural and o+(disjs-o) ~ disjs
166 , Constraint.Proof <- (Nat.<=) @1 @(disjs-o) -- prove that ((disjs-o)-1)+1 ~ disjs-o
168 let (prevDisjs, ML.uncons -> (_indexedDisj,nextDisjs)) = ML.splitAt o disjs
169 prevFakes <- fakeProof `mapM` prevDisjs
170 nextFakes <- fakeProof `mapM` nextDisjs
173 sum (proof_challenge . fst <$> prevFakes) +
174 sum (proof_challenge . fst <$> nextFakes)
175 genuineProof <- nizkProof secNonce [groupGen, pubKey] $
179 foldMap snd prevFakes <>
181 foldMap snd nextFakes in
182 -- NOTE: this is a so-called strong Fiat-Shamir transformation (not a weak):
183 -- because the statement is included in the hash (not only the commitments).
184 hash (encryptionStatement zkp enc) commitments + challengeSum
189 (ML.cons genuineProof (fst <$> nextFakes))
191 fakeProof :: Disjunction q -> S.StateT r m (Proof q, [Commitment q])
193 proof_challenge <- random
194 proof_response <- random
196 [ groupGen^proof_response / encryption_nonce ^proof_challenge
197 , pubKey ^proof_response / (encryption_vault*disj)^proof_challenge
199 return (Proof{..}, commitments)
201 validateEncryption ::
203 PublicKey q -> ZKP ->
204 ML.MeasuredList n (Disjunction q) ->
205 (Encryption q, ValidityProof n q) -> Bool
206 validateEncryption pubKey zkp disjs (enc@Encryption{..}, ValidityProof proofs) =
207 hash (encryptionStatement zkp enc) commitments == challengeSum
209 challengeSum = sum (proof_challenge <$> proofs)
210 commitments = foldMap commitment (ML.zip disjs proofs)
211 where commitment (disj, Proof{..}) =
214 -- y1 = encryption_nonce
215 -- y2 = (encryption_vault * disj)
216 -- com1 = g^res / y1 ^ ch
217 -- com2 = h^res / y2 ^ ch
218 [ groupGen^proof_response / encryption_nonce ^proof_challenge
219 , pubKey ^proof_response / (encryption_vault*disj)^proof_challenge
223 data Question choices (mini::Nat) (maxi::Nat) q =
225 { question_text :: Text
226 , question_answers :: ML.MeasuredList choices Text
227 , question_bounds :: Bounds mini maxi
228 -- , question_blank :: Maybe Bool
229 } deriving (Eq, Show)
232 data Bounds mini maxi =
233 ((mini<=maxi), Nat.Known mini, Nat.Known maxi) =>
234 Bounds (Proxy mini) (Proxy maxi)
235 instance Show (Bounds mini maxi) where
236 showsPrec p Bounds{} = showsPrec p (Nat.nat @mini, Nat.nat @maxi)
237 instance Eq (Bounds mini maxi) where
241 data Answer choices mini maxi q = Answer
242 { answer_opinions :: ML.MeasuredList choices (Encryption q, ValidityProof 2 q)
243 -- ^ Encrypted 'Opinion' for each 'question_answers'
244 -- with a 'ValidityProof' that they belong to [0,1].
245 , answer_sumProof :: ValidityProof (maxi-mini) q
246 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
247 -- is an element of ['mini'..'maxi'].
248 -- , answer_blankProof ::
251 -- | @('answer' pubKey zkp quest opinions)@
252 -- returns a validable 'Answer',
253 -- unless the given 'opinions' do not respect 'question_bounds'.
255 forall m r q mini maxi choices.
256 Monad m => RandomGen r => SubGroup q =>
257 PublicKey q -> ZKP ->
258 Question choices mini maxi q ->
259 ML.MeasuredList choices (Opinion 2) ->
260 S.StateT r m (Maybe (Answer choices mini maxi q))
261 answer pubKey zkp Question{..} opinions
262 | Bounds{} <- question_bounds
263 , SomeNat (_opinionsSum::Proxy opinionsSum) <-
264 someNatVal $ sum $ (\(Nat.Index o) -> natVal o) <$> opinions
265 -- prove that opinionsSum-mini is a Natural
266 , Just Constraint.Proof <- (Nat.<=?) @mini @opinionsSum
267 -- prove that (opinionsSum-mini)+1 is a Natural
268 , Constraint.Proof <- (Nat.+) @(opinionsSum-mini) @1
269 -- prove that maxi-mini is a Natural
270 , Constraint.Proof <- (Nat.<=) @mini @maxi
271 -- prove that (opinionsSum-mini)+1 <= maxi-mini
272 , Just Constraint.Proof <- (Nat.<=?) @((opinionsSum-mini)+1) @(maxi-mini)
274 encryptions <- encrypt pubKey `mapM` opinions
276 sequence $ ML.zipWith
277 (proveEncryption pubKey zkp booleanDisjunctions)
280 proveEncryption pubKey zkp
281 (intervalDisjunctions question_bounds)
282 (Nat.Index $ Proxy @(opinionsSum-mini))
283 ( sum (fst <$> encryptions)
284 , sum (snd <$> encryptions) )
286 { answer_opinions = ML.zip
287 (snd <$> encryptions) -- NOTE: drop secNonce
289 , answer_sumProof = sumProof
291 | otherwise = return Nothing
295 PublicKey q -> ZKP ->
296 Question choices mini maxi q ->
297 Answer choices mini maxi q -> Bool
298 validateAnswer pubKey zkp Question{..} Answer{..} =
299 and (validateEncryption pubKey zkp booleanDisjunctions <$> answer_opinions) &&
300 validateEncryption pubKey zkp
301 (intervalDisjunctions question_bounds)
302 ( sum (fst <$> answer_opinions)
306 data Election quests choices mini maxi q = Election
307 { election_name :: Text
308 , election_description :: Text
309 , election_publicKey :: PublicKey q
310 , election_questions :: ML.MeasuredList quests (Question choices mini maxi q)
311 , election_uuid :: UUID
312 , election_hash :: Hash
316 newtype Hash = Hash Text
317 deriving (Eq,Ord,Show)
320 data Ballot quests choices mini maxi q = Ballot
321 { ballot_answers :: ML.MeasuredList quests (Answer choices mini maxi q)
322 , ballot_signature :: Maybe (Signature q)
323 , ballot_election_uuid :: UUID
324 , ballot_election_hash :: Hash
331 Election quests choices mini maxi q ->
332 Maybe (SecretKey q) ->
333 ML.MeasuredList quests (ML.MeasuredList choices (Opinion 2)) ->
334 S.StateT r m (Maybe (Ballot quests choices mini maxi q))
335 ballot Election{..} secKeyMay opinionsByQuest = do
338 Nothing -> (Nothing, "")
340 ( Just (secKey, pubKey)
341 , fromString (show (natG pubKey)) )
342 where pubKey = groupGen ^ secKey
345 uncurry (answer election_publicKey zkp) `mapM`
346 ML.zip election_questions opinionsByQuest
347 case answersByQuestMay of
348 Nothing -> return Nothing
349 Just answersByQuest -> do
350 ballot_signature <- case keysMay of
351 Nothing -> return Nothing
352 Just (secKey, pubKey) -> do
354 let commitment = groupGen ^ w
355 let proof_challenge = hash
356 (signatureCommitments zkp commitment)
357 (signatureStatement answersByQuest)
358 return $ Just Signature
359 { signature_publicKey = pubKey
360 , signature_proof = Proof
362 , proof_response = w - secKey*proof_challenge
366 { ballot_answers = answersByQuest
367 , ballot_election_hash = election_hash
368 , ballot_election_uuid = election_uuid
374 Election quests choices mini maxi q ->
375 Ballot quests choices mini maxi q ->
377 validateBallot Election{..} Ballot{..} =
378 ballot_election_uuid == election_uuid &&
379 ballot_election_hash == election_hash &&
380 let (validSign, zkp) =
381 case ballot_signature of
382 Nothing -> (True, "")
383 Just (Signature pubKey Proof{..}) ->
384 let zkp = fromString (show (natG pubKey)) in
386 let commitment = groupGen ^ proof_response * pubKey ^ proof_challenge in
387 let prefix = signatureCommitments zkp commitment in
388 let contents = signatureStatement ballot_answers in
389 hash prefix contents == proof_challenge
393 and (ML.zipWith (validateAnswer election_publicKey zkp)
394 election_questions ballot_answers)
396 -- ** Type 'Signature'
397 -- | Schnorr-like signature.
399 -- Used to avoid 'Ballot' stuffing.
400 data Signature q = Signature
401 { signature_publicKey :: PublicKey q
402 , signature_proof :: Proof q
405 signatureStatement ::
406 Foldable f => SubGroup q =>
407 f (Answer choices mini maxi q) -> [G q]
409 foldMap $ \Answer{..} ->
410 (`foldMap` answer_opinions) $ \(Encryption{..}, _vp) ->
411 [encryption_nonce, encryption_vault]
413 signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
414 signatureCommitments zkp commitment =
415 "sig|"<>zkp<>"|"<>fromString (show (natG commitment))<>"|"