]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: replace F by G
[majurity.git] / hjugement-protocol / src / Voting / Protocol / Election.hs
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
8
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(..),(.:),(.:?),(.=))
15 import Data.Bool
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
52
53 import Voting.Protocol.Utils
54 import Voting.Protocol.Arith
55 import Voting.Protocol.Credential
56 import Voting.Protocol.FFC (FFC)
57
58 -- * Type 'Encryption'
59 -- | ElGamal-like encryption.
60 -- Its security relies on the /Discrete Logarithm problem/.
61 --
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.
67 --
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)@
77 } deriving (Generic)
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)
81 instance
82 ( Reifies v Version
83 , Reifies c crypto
84 , ToJSON (G crypto c)
85 ) => ToJSON (Encryption crypto v c) where
86 toJSON Encryption{..} =
87 JSON.object
88 [ "alpha" .= encryption_nonce
89 , "beta" .= encryption_vault
90 ]
91 toEncoding Encryption{..} =
92 JSON.pairs
93 ( "alpha" .= encryption_nonce
94 <> "beta" .= encryption_vault
95 )
96 instance
97 ( Reifies v Version
98 , Reifies c crypto
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{..}
105
106 -- | Additive homomorphism.
107 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
108 instance
109 ( Reifies c crypto
110 , Multiplicative (G crypto c)
111 ) => Additive (Encryption crypto v c) where
112 zero = Encryption one one
113 x+y = Encryption
114 (encryption_nonce x * encryption_nonce y)
115 (encryption_vault x * encryption_vault y)
116
117 -- *** Type 'EncryptionNonce'
118 type EncryptionNonce = E
119
120 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
121 --
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'.
128 encrypt ::
129 Reifies v Version =>
130 Reifies c crypto =>
131 Group crypto =>
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
137 encNonce <- random
138 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
139 return $ (encNonce,)
140 Encryption
141 { encryption_nonce = groupGen^encNonce
142 , encryption_vault = pubKey ^encNonce * groupGen^clear
143 }
144
145 -- * Type 'Proof'
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'.
161 --
162 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
163 --
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@,
168 --
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'.
175 --
176 -- The prover choses 'commitment' to be a random power of @base@,
177 -- to ensure that each 'prove' does not reveal any information
178 -- about its secret.
179 } deriving (Eq,Show,NFData,Generic)
180 instance Group crypto => ToJSON (Proof crypto v c) where
181 toJSON Proof{..} =
182 JSON.object
183 [ "challenge" .= proof_challenge
184 , "response" .= proof_response
185 ]
186 toEncoding Proof{..} =
187 JSON.pairs
188 ( "challenge" .= proof_challenge
189 <> "response" .= proof_response
190 )
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"
195 return Proof{..}
196
197 -- ** Type 'ZKP'
198 -- | Zero-knowledge proof.
199 --
200 -- A protocol is /zero-knowledge/ if the verifier
201 -- learns nothing from the protocol except that the prover
202 -- knows the secret.
203 --
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
207
208 -- ** Type 'Challenge'
209 type Challenge = E
210
211 -- ** Type 'Oracle'
212 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
213 -- by 'hash'ing them (eventually with other 'Commitment's).
214 --
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
224
225 -- | @('prove' sec commitmentBases oracle)@
226 -- returns a 'Proof' that @sec@ is known
227 -- (by proving the knowledge of its discrete logarithm).
228 --
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
233 -- (with 'commit').
234 --
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).
237 --
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).
242 prove ::
243 forall crypto v c list m r.
244 Reifies c crypto =>
245 Reifies v Version =>
246 Group crypto =>
247 Multiplicative (G crypto c) =>
248 Monad m => RandomGen r => Functor list =>
249 E crypto c ->
250 list (G crypto c) ->
251 Oracle list crypto c ->
252 S.StateT r m (Proof crypto v c)
253 prove sec commitmentBases oracle = do
254 nonce <- random
255 let commitments = (^ nonce) <$> commitmentBases
256 let proof_challenge = oracle commitments
257 return Proof
258 { proof_challenge
259 , proof_response = nonce `op` (sec*proof_challenge)
260 }
261 where
262 -- | See comments in 'commit'.
263 op =
264 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
265 then (-)
266 else (+)
267
268 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
269 -- when Helios-C specifications will be fixed.
270 proveQuicker ::
271 Reifies c crypto =>
272 Reifies v Version =>
273 Group crypto =>
274 Multiplicative (G crypto c) =>
275 Monad m => RandomGen r => Functor list =>
276 E crypto c ->
277 list (G crypto c) ->
278 Oracle list crypto c ->
279 S.StateT r m (Proof crypto v c)
280 proveQuicker sec commitmentBases oracle = do
281 nonce <- random
282 let commitments = (^ nonce) <$> commitmentBases
283 let proof_challenge = oracle commitments
284 return Proof
285 { proof_challenge
286 , proof_response = nonce - sec*proof_challenge
287 }
288
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'.
294 --
295 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
296 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
297 fakeProof ::
298 Reifies c crypto =>
299 Group crypto =>
300 Monad m => RandomGen r =>
301 S.StateT r m (Proof crypto v c)
302 fakeProof = do
303 proof_challenge <- random
304 proof_response <- random
305 return Proof{..}
306
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'.
311 type Commitment = G
312
313 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
314 -- from the given 'Proof' with the knowledge of the verifier.
315 commit ::
316 forall crypto v c.
317 Reifies v Version =>
318 Reifies c crypto =>
319 Group crypto =>
320 Multiplicative (G crypto c) =>
321 Invertible (G crypto c) =>
322 Proof crypto v c ->
323 G crypto c ->
324 G crypto c ->
325 Commitment crypto c
326 commit Proof{..} base basePowSec =
327 (base^proof_response) `op`
328 (basePowSec^proof_challenge)
329 where
330 op =
331 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
332 then (*)
333 else (/)
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 #-}
339
340 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
341 -- when Helios-C specifications will be fixed.
342 commitQuicker ::
343 Reifies c crypto =>
344 Multiplicative (G crypto c) =>
345 Proof crypto v c ->
346 G crypto c ->
347 G crypto c ->
348 Commitment crypto c
349 commitQuicker Proof{..} base basePowSec =
350 base^proof_response *
351 basePowSec^proof_challenge
352
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)@,
357 type Disjunction = G
358
359 booleanDisjunctions ::
360 forall crypto c.
361 Reifies c crypto =>
362 Group crypto =>
363 Multiplicative (G crypto c) =>
364 Invertible (G crypto c) =>
365 [Disjunction crypto c]
366 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
367
368 intervalDisjunctions ::
369 forall crypto c.
370 Reifies c crypto =>
371 Group crypto =>
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
379
380 -- ** Type 'Opinion'
381 -- | Index of a 'Disjunction' within a list of them.
382 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
383 type Opinion = E
384
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)
392 {-
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
397 ( Reifies c crypto
398 , ToJSON (GroupExponent crypto c)
399 ) => ToJSON (DisjProof crypto v c)
400 deriving newtype instance
401 ( Reifies c crypto
402 , FromJSON (GroupExponent crypto c)
403 ) => FromJSON (DisjProof crypto v c)
404 -}
405
406 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
407 -- returns a 'DisjProof' that 'enc' 'encrypt's
408 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
409 --
410 -- The prover proves that it knows an 'encNonce', such that:
411 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
412 --
413 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
414 --
415 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
416 proveEncryption ::
417 Reifies v Version =>
418 Reifies c crypto =>
419 Group crypto =>
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
443 genuineChallenge
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)
449
450 verifyEncryption ::
451 Reifies v Version =>
452 Reifies c crypto =>
453 Group crypto =>
454 ToNatural (G crypto c) =>
455 Multiplicative (G crypto c) =>
456 Invertible (G crypto c) =>
457 Monad m =>
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
463 Nothing ->
464 throwE $ ErrorVerifyEncryption_InvalidProofLength
465 (fromIntegral $ List.length proofs)
466 (fromIntegral $ List.length disjs)
467 Just commitments ->
468 return $ challengeSum ==
469 hash (encryptionStatement voterZKP enc) (join commitments)
470 where
471 challengeSum = sum (proof_challenge <$> proofs)
472
473 -- ** Hashing
474 encryptionStatement ::
475 Reifies c crypto =>
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<>"|"
482
483 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
484 -- returns the 'Commitment's with only the knowledge of the verifier.
485 --
486 -- For the prover the 'Proof' comes from @fakeProof@,
487 -- and for the verifier the 'Proof' comes from the prover.
488 encryptionCommitments ::
489 Reifies v Version =>
490 Reifies c crypto =>
491 Group crypto =>
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.
503 ]
504
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.
511 deriving (Eq,Show)
512
513 -- * Type 'Question'
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{..} =
523 JSON.object
524 [ "question" .= question_text
525 , "answers" .= question_choices
526 , "min" .= question_mini
527 , "max" .= question_maxi
528 ]
529 toEncoding Question{..} =
530 JSON.pairs
531 ( "question" .= question_text
532 <> "answers" .= question_choices
533 <> "min" .= question_mini
534 <> "max" .= question_maxi
535 )
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"
542 return Question{..}
543
544 -- * Type 'Answer'
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 ::
553 } deriving (Generic)
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)
557 instance
558 ( Reifies v Version
559 , Reifies c crypto
560 , ToJSON (G crypto c)
561 , Group crypto
562 ) => ToJSON (Answer crypto v c) where
563 toJSON Answer{..} =
564 let (answer_choices, answer_individual_proofs) =
565 List.unzip answer_opinions in
566 JSON.object
567 [ "choices" .= answer_choices
568 , "individual_proofs" .= answer_individual_proofs
569 , "overall_proof" .= answer_sumProof
570 ]
571 toEncoding Answer{..} =
572 let (answer_choices, answer_individual_proofs) =
573 List.unzip answer_opinions in
574 JSON.pairs
575 ( "choices" .= answer_choices
576 <> "individual_proofs" .= answer_individual_proofs
577 <> "overall_proof" .= answer_sumProof
578 )
579 instance
580 ( Reifies v Version
581 , Reifies c crypto
582 , FromJSON (G crypto c)
583 , Group crypto
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"
590 return Answer{..}
591
592 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
593 -- returns an 'Answer' validable by 'verifyAnswer',
594 -- unless an 'ErrorAnswer' is returned.
595 encryptAnswer ::
596 Reifies v Version =>
597 Reifies c crypto =>
598 Group crypto =>
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) =
608 lift $ throwE $
609 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
610 | List.length opinions /= List.length question_choices =
611 lift $ throwE $
612 ErrorAnswer_WrongNumberOfOpinions
613 (fromIntegral $ List.length opinions)
614 (fromIntegral $ List.length question_choices)
615 | otherwise = do
616 encryptions <- encrypt elecPubKey `mapM` opinions
617 individualProofs <- zipWithM
618 (\opinion -> proveEncryption elecPubKey zkp $
619 if opinion
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
629 )
630 return $ Answer
631 { answer_opinions = List.zip
632 (snd <$> encryptions) -- NOTE: drop encNonce
633 individualProofs
634 , answer_sumProof = sumProof
635 }
636 where
637 opinionsSum = sum $ nat <$> opinions
638 opinions = (\o -> if o then one else zero) <$> opinionByChoice
639
640 verifyAnswer ::
641 Reifies v Version =>
642 Reifies c crypto =>
643 Group crypto =>
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
652 validOpinions <-
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)
658 , answer_sumProof )
659 return (and validOpinions && validSum)
660
661 -- ** Type 'ErrorAnswer'
662 -- | Error raised by 'encryptAnswer'.
663 data ErrorAnswer
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)
671
672 -- * Type 'Election'
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)
682 } deriving (Generic)
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)
686 instance
687 ( ToJSON crypto
688 , ToJSON (G crypto c)
689 , Reifies v Version
690 , Reifies c crypto
691 ) => ToJSON (Election crypto v c) where
692 toJSON Election{..} =
693 JSON.object $
694 [ "name" .= election_name
695 , "description" .= election_description
696 , ("public_key", JSON.object
697 [ "group" .= election_crypto
698 , "y" .= election_public_key
699 ])
700 , "questions" .= election_questions
701 , "uuid" .= election_uuid
702 ] <>
703 maybe [] (\version -> [ "version" .= version ]) election_version
704 toEncoding Election{..} =
705 JSON.pairs $
706 ( "name" .= election_name
707 <> "description" .= election_description
708 <> JSON.pair "public_key" (JSON.pairs $
709 "group" .= election_crypto
710 <> "y" .= election_public_key
711 )
712 <> "questions" .= election_questions
713 <> "uuid" .= election_uuid
714 ) <>
715 maybe mempty ("version" .=) election_version
716
717 readElection ::
718 ReifyCrypto crypto =>
719 FromJSON crypto =>
720 FilePath ->
721 (forall v c.
722 Reifies v Version =>
723 Reifies c crypto =>
724 GConstraints crypto c =>
725 Election crypto v c -> r) ->
726 ExceptT String IO r
727 readElection filePath k = do
728 fileData <- lift $ BS.readFile filePath
729 ExceptT $ return $
730 jsonEitherFormatError $
731 JSON.eitherDecodeStrictWith JSON.jsonEOF
732 (JSON.iparse (parseElection fileData))
733 fileData
734 where
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)
744 ) o "public_key"
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
755 , ..
756 }
757
758 hashElection ::
759 ToJSON crypto =>
760 Reifies c crypto =>
761 Reifies v Version =>
762 ToJSON (G crypto c) =>
763 Election crypto v c -> Base64SHA256
764 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
765
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.
775 --
776 -- For convenience, the 'ReifyCrypto' class also implies the pervasive
777 -- constraint 'Group'.
778 class
779 ( Group crypto
780 , Key crypto
781 , Show crypto
782 , NFData crypto
783 , JSON.ToJSON crypto
784 , JSON.FromJSON crypto
785 ) => ReifyCrypto crypto where
786 reifyCrypto ::
787 crypto -> (forall c.
788 Reifies c crypto =>
789 GConstraints crypto c =>
790 Proxy c -> r) -> r
791 instance ReifyCrypto FFC where
792 reifyCrypto = reify
793
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)
804 , Eq (G crypto c)
805 , Ord (G crypto c)
806 , Show (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)
812 )
813
814 -- * Type 'Ballot'
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
820 } deriving (Generic)
821 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
822 instance
823 ( Reifies v Version
824 , Reifies c crypto
825 , Group crypto
826 , ToJSON (G crypto c)
827 ) => ToJSON (Ballot crypto v c) where
828 toJSON Ballot{..} =
829 JSON.object $
830 [ "answers" .= ballot_answers
831 , "election_uuid" .= ballot_election_uuid
832 , "election_hash" .= ballot_election_hash
833 ] <>
834 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
835 toEncoding Ballot{..} =
836 JSON.pairs $
837 ( "answers" .= ballot_answers
838 <> "election_uuid" .= ballot_election_uuid
839 <> "election_hash" .= ballot_election_hash
840 ) <>
841 maybe mempty ("signature" .=) ballot_signature
842 instance
843 ( Reifies v Version
844 , Reifies c crypto
845 , Group crypto
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"
853 return Ballot{..}
854
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'.
859 encryptBallot ::
860 forall crypto m v c r.
861 Reifies c crypto =>
862 Reifies v Version =>
863 Group crypto =>
864 Key crypto =>
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 =
874 lift $ throwE $
875 ErrorBallot_WrongNumberOfAnswers
876 (fromIntegral $ List.length opinionsByQuest)
877 (fromIntegral $ List.length election_questions)
878 | otherwise = do
879 let (voterKeys, voterZKP) =
880 case ballotSecKeyMay of
881 Nothing -> (Nothing, ZKP "")
882 Just ballotSecKey ->
883 ( Just (ballotSecKey, ballotPubKey)
884 , ZKP (bytesNat ballotPubKey) )
885 where ballotPubKey = publicKey ballotSecKey
886 ballot_answers <-
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
893 signature_proof <-
894 proveQuicker ballotSecKey (Identity groupGen) $
895 \(Identity commitment) ->
896 hash @_ @crypto
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{..}
903 return Ballot
904 { ballot_answers
905 , ballot_election_hash = election_hash
906 , ballot_election_uuid = election_uuid
907 , ballot_signature
908 }
909
910 verifyBallot ::
911 forall crypto v c.
912 Reifies v Version =>
913 Reifies c crypto =>
914 Group crypto =>
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
929 (, zkp) $
930 proof_challenge signature_proof == hash
931 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
932 (signatureStatement @_ @crypto ballot_answers)
933 in
934 and $ isValidSign :
935 List.zipWith (verifyAnswer election_public_key zkpSign)
936 election_questions ballot_answers
937
938 -- ** Type 'Signature'
939 -- | Schnorr-like signature.
940 --
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)
948 } deriving (Generic)
949 deriving instance
950 ( NFData crypto
951 , NFData (G crypto c)
952 ) => NFData (Signature crypto v c)
953 instance
954 ( Reifies c crypto
955 , Reifies v Version
956 , ToJSON (G crypto c)
957 ) => ToJSON (Signature crypto v c) where
958 toJSON (Signature pubKey Proof{..}) =
959 JSON.object
960 [ "public_key" .= pubKey
961 , "challenge" .= proof_challenge
962 , "response" .= proof_response
963 ]
964 toEncoding (Signature pubKey Proof{..}) =
965 JSON.pairs
966 ( "public_key" .= pubKey
967 <> "challenge" .= proof_challenge
968 <> "response" .= proof_response
969 )
970 instance
971 ( Reifies c crypto
972 , Reifies v Version
973 , Group crypto
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{..}
981 return Signature{..}
982
983 -- *** Hashing
984
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]
989 signatureStatement =
990 foldMap $ \Answer{..} ->
991 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
992 [encryption_nonce, encryption_vault]
993
994 -- | @('signatureCommitments' voterZKP commitment)@
995 signatureCommitments ::
996 Reifies c crypto =>
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<>"|"
1002
1003 -- ** Type 'ErrorBallot'
1004 -- | Error raised by 'encryptBallot'.
1005 data ErrorBallot
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'.
1011 | ErrorBallot_Wrong
1012 -- ^ TODO: to be more precise.
1013 deriving (Eq,Show,Generic,NFData)
1014
1015 -- * Type 'Version'
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{..} =
1025 List.foldr (.) id
1026 (List.intersperse (showChar '.') $
1027 showsPrec 0 <$> version_branch) .
1028 List.foldr (.) id
1029 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
1030 if n > 0 then showsPrec 0 n else id)
1031 <$> version_tags)
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)
1038 = return v
1039 parseJSON json = JSON.typeMismatch "Version" json
1040
1041 hasVersionTag :: Version -> Text -> Bool
1042 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
1043
1044 experimentalVersion :: Version
1045 experimentalVersion = stableVersion
1046 {version_tags = [(versionTagQuicker,0)]}
1047
1048 stableVersion :: Version
1049 stableVersion = "1.6"
1050
1051 versionTagQuicker :: Text
1052 versionTagQuicker = "quicker"
1053
1054 readVersion :: String -> Maybe Version
1055 readVersion = parseReadP $ do
1056 version_branch <- Read.sepBy1
1057 (Read.read <$> Read.munch1 Char.isDigit)
1058 (Read.char '.')
1059 version_tags <- Read.many $ (,)
1060 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
1061 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
1062 return Version{..}
1063
1064 parseReadP :: Read.ReadP a -> String -> Maybe a
1065 parseReadP p s =
1066 let p' = Read.readP_to_S p in
1067 listToMaybe $ do
1068 (x, "") <- p' s
1069 return x