]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: add Version and abstract over FFC
[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 (FieldElement crypto c) => Eq (Encryption crypto v c)
79 deriving instance (Show (FieldElement crypto c), Show (G crypto c)) => Show (Encryption crypto v c)
80 deriving instance NFData (FieldElement crypto c) => NFData (Encryption crypto v c)
81 instance
82 ( Reifies v Version
83 , Reifies c crypto
84 , ToJSON (FieldElement 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 (FieldElement 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 (FieldElement 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 (FieldElement 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 (FieldElement 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 (FieldElement crypto c) =>
321 Proof crypto v c ->
322 G crypto c ->
323 G crypto c ->
324 Commitment crypto c
325 commit Proof{..} base basePowSec =
326 (base^proof_response) `op`
327 (basePowSec^proof_challenge)
328 where
329 op =
330 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
331 then (*)
332 else (/)
333 -- TODO: contrary to some textbook presentations,
334 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
335 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
336 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
337 {-# INLINE commit #-}
338
339 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
340 -- when Helios-C specifications will be fixed.
341 commitQuicker ::
342 Reifies c crypto =>
343 Multiplicative (FieldElement crypto c) =>
344 Proof crypto v c ->
345 G crypto c ->
346 G crypto c ->
347 Commitment crypto c
348 commitQuicker Proof{..} base basePowSec =
349 base^proof_response *
350 basePowSec^proof_challenge
351
352 -- * Type 'Disjunction'
353 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
354 -- it's used in 'proveEncryption' to generate a 'Proof'
355 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
356 type Disjunction = G
357
358 booleanDisjunctions ::
359 forall crypto c.
360 Reifies c crypto =>
361 Group crypto =>
362 Multiplicative (FieldElement crypto c) =>
363 [Disjunction crypto c]
364 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
365
366 intervalDisjunctions ::
367 forall crypto c.
368 Reifies c crypto =>
369 Group crypto =>
370 Multiplicative (FieldElement crypto c) =>
371 Natural -> Natural -> [Disjunction crypto c]
372 intervalDisjunctions mini maxi =
373 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
374 List.genericDrop (nat mini) $
375 groupGenInverses @crypto
376
377 -- ** Type 'Opinion'
378 -- | Index of a 'Disjunction' within a list of them.
379 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
380 type Opinion = E
381
382 -- ** Type 'DisjProof'
383 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
384 -- is indexing a 'Disjunction' within a list of them,
385 -- without revealing which 'Opinion' it is.
386 newtype DisjProof crypto v c = DisjProof [Proof crypto v c]
387 deriving (Eq,Show,Generic)
388 deriving newtype (NFData,ToJSON,FromJSON)
389 {-
390 deriving instance Eq (GroupExponent crypto c) => Eq (DisjProof crypto v c)
391 deriving instance Show (GroupExponent crypto c) => Show (DisjProof crypto v c)
392 deriving newtype instance NFData (GroupExponent crypto c) => NFData (DisjProof crypto v c)
393 deriving newtype instance
394 ( Reifies c crypto
395 , ToJSON (GroupExponent crypto c)
396 ) => ToJSON (DisjProof crypto v c)
397 deriving newtype instance
398 ( Reifies c crypto
399 , FromJSON (GroupExponent crypto c)
400 ) => FromJSON (DisjProof crypto v c)
401 -}
402
403 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
404 -- returns a 'DisjProof' that 'enc' 'encrypt's
405 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
406 --
407 -- The prover proves that it knows an 'encNonce', such that:
408 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
409 --
410 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
411 --
412 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
413 proveEncryption ::
414 Reifies v Version =>
415 Reifies c crypto =>
416 Group crypto =>
417 ToNatural (FieldElement crypto c) =>
418 Multiplicative (FieldElement crypto c) =>
419 Monad m => RandomGen r =>
420 PublicKey crypto c -> ZKP ->
421 ([Disjunction crypto c],[Disjunction crypto c]) ->
422 (EncryptionNonce crypto c, Encryption crypto v c) ->
423 S.StateT r m (DisjProof crypto v c)
424 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
425 -- Fake proofs for all 'Disjunction's except the genuine one.
426 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
427 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
428 let fakeChallengeSum =
429 sum (proof_challenge <$> prevFakeProofs) +
430 sum (proof_challenge <$> nextFakeProofs)
431 let statement = encryptionStatement voterZKP enc
432 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
433 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
434 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
435 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
436 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
437 let challenge = hash statement commitments in
438 let genuineChallenge = challenge - fakeChallengeSum in
439 genuineChallenge
440 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
441 -- thus (sum (proof_challenge <$> proofs) == challenge)
442 -- as checked in 'verifyEncryption'.
443 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
444 return (DisjProof proofs)
445
446 verifyEncryption ::
447 Reifies v Version =>
448 Reifies c crypto =>
449 Group crypto =>
450 ToNatural (FieldElement crypto c) =>
451 Multiplicative (FieldElement crypto c) =>
452 Monad m =>
453 PublicKey crypto c -> ZKP ->
454 [Disjunction crypto c] -> (Encryption crypto v c, DisjProof crypto v c) ->
455 ExceptT ErrorVerifyEncryption m Bool
456 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
457 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
458 Nothing ->
459 throwE $ ErrorVerifyEncryption_InvalidProofLength
460 (fromIntegral $ List.length proofs)
461 (fromIntegral $ List.length disjs)
462 Just commitments ->
463 return $ challengeSum ==
464 hash (encryptionStatement voterZKP enc) (join commitments)
465 where
466 challengeSum = sum (proof_challenge <$> proofs)
467
468 -- ** Hashing
469 encryptionStatement ::
470 Reifies c crypto =>
471 ToNatural (FieldElement crypto c) =>
472 ZKP -> Encryption crypto v c -> BS.ByteString
473 encryptionStatement (ZKP voterZKP) Encryption{..} =
474 "prove|"<>voterZKP<>"|"
475 <> bytesNat encryption_nonce<>","
476 <> bytesNat encryption_vault<>"|"
477
478 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
479 -- returns the 'Commitment's with only the knowledge of the verifier.
480 --
481 -- For the prover the 'Proof' comes from @fakeProof@,
482 -- and for the verifier the 'Proof' comes from the prover.
483 encryptionCommitments ::
484 Reifies v Version =>
485 Reifies c crypto =>
486 Group crypto =>
487 Multiplicative (FieldElement crypto c) =>
488 PublicKey crypto c -> Encryption crypto v c ->
489 Disjunction crypto c -> Proof crypto v c -> [G crypto c]
490 encryptionCommitments elecPubKey Encryption{..} disj proof =
491 [ commit proof groupGen encryption_nonce
492 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
493 -- base==groupGen, basePowSec==groupGen^encNonce.
494 , commit proof elecPubKey (encryption_vault*disj)
495 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
496 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
497 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
498 ]
499
500 -- ** Type 'ErrorVerifyEncryption'
501 -- | Error raised by 'verifyEncryption'.
502 data ErrorVerifyEncryption
503 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
504 -- ^ When the number of proofs is different than
505 -- the number of 'Disjunction's.
506 deriving (Eq,Show)
507
508 -- * Type 'Question'
509 data Question v = Question
510 { question_text :: !Text
511 , question_choices :: ![Text]
512 , question_mini :: !Natural
513 , question_maxi :: !Natural
514 -- , question_blank :: Maybe Bool
515 } deriving (Eq,Show,Generic,NFData)
516 instance Reifies v Version => ToJSON (Question v) where
517 toJSON Question{..} =
518 JSON.object
519 [ "question" .= question_text
520 , "answers" .= question_choices
521 , "min" .= question_mini
522 , "max" .= question_maxi
523 ]
524 toEncoding Question{..} =
525 JSON.pairs
526 ( "question" .= question_text
527 <> "answers" .= question_choices
528 <> "min" .= question_mini
529 <> "max" .= question_maxi
530 )
531 instance Reifies v Version => FromJSON (Question v) where
532 parseJSON = JSON.withObject "Question" $ \o -> do
533 question_text <- o .: "question"
534 question_choices <- o .: "answers"
535 question_mini <- o .: "min"
536 question_maxi <- o .: "max"
537 return Question{..}
538
539 -- * Type 'Answer'
540 data Answer crypto v c = Answer
541 { answer_opinions :: ![(Encryption crypto v c, DisjProof crypto v c)]
542 -- ^ Encrypted 'Opinion' for each 'question_choices'
543 -- with a 'DisjProof' that they belong to [0,1].
544 , answer_sumProof :: !(DisjProof crypto v c)
545 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
546 -- is an element of @[mini..maxi]@.
547 -- , answer_blankProof ::
548 } deriving (Generic)
549 deriving instance Eq (FieldElement crypto c) => Eq (Answer crypto v c)
550 deriving instance (Show (FieldElement crypto c), Show (G crypto c)) => Show (Answer crypto v c)
551 deriving instance NFData (FieldElement crypto c) => NFData (Answer crypto v c)
552 instance
553 ( Reifies v Version
554 , Reifies c crypto
555 , ToJSON (FieldElement crypto c)
556 , Group crypto
557 ) => ToJSON (Answer crypto v c) where
558 toJSON Answer{..} =
559 let (answer_choices, answer_individual_proofs) =
560 List.unzip answer_opinions in
561 JSON.object
562 [ "choices" .= answer_choices
563 , "individual_proofs" .= answer_individual_proofs
564 , "overall_proof" .= answer_sumProof
565 ]
566 toEncoding Answer{..} =
567 let (answer_choices, answer_individual_proofs) =
568 List.unzip answer_opinions in
569 JSON.pairs
570 ( "choices" .= answer_choices
571 <> "individual_proofs" .= answer_individual_proofs
572 <> "overall_proof" .= answer_sumProof
573 )
574 instance
575 ( Reifies v Version
576 , Reifies c crypto
577 , FromJSON (G crypto c)
578 , Group crypto
579 ) => FromJSON (Answer crypto v c) where
580 parseJSON = JSON.withObject "Answer" $ \o -> do
581 answer_choices <- o .: "choices"
582 answer_individual_proofs <- o .: "individual_proofs"
583 let answer_opinions = List.zip answer_choices answer_individual_proofs
584 answer_sumProof <- o .: "overall_proof"
585 return Answer{..}
586
587 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
588 -- returns an 'Answer' validable by 'verifyAnswer',
589 -- unless an 'ErrorAnswer' is returned.
590 encryptAnswer ::
591 Reifies v Version =>
592 Reifies c crypto =>
593 Group crypto =>
594 Multiplicative (FieldElement crypto c) =>
595 ToNatural (FieldElement crypto c) =>
596 Monad m => RandomGen r =>
597 PublicKey crypto c -> ZKP ->
598 Question v -> [Bool] ->
599 S.StateT r (ExceptT ErrorAnswer m) (Answer crypto v c)
600 encryptAnswer elecPubKey zkp Question{..} opinionByChoice
601 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
602 lift $ throwE $
603 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
604 | List.length opinions /= List.length question_choices =
605 lift $ throwE $
606 ErrorAnswer_WrongNumberOfOpinions
607 (fromIntegral $ List.length opinions)
608 (fromIntegral $ List.length question_choices)
609 | otherwise = do
610 encryptions <- encrypt elecPubKey `mapM` opinions
611 individualProofs <- zipWithM
612 (\opinion -> proveEncryption elecPubKey zkp $
613 if opinion
614 then (List.init booleanDisjunctions,[])
615 else ([],List.tail booleanDisjunctions))
616 opinionByChoice encryptions
617 sumProof <- proveEncryption elecPubKey zkp
618 (List.tail <$> List.genericSplitAt
619 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
620 (intervalDisjunctions question_mini question_maxi))
621 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
622 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
623 )
624 return $ Answer
625 { answer_opinions = List.zip
626 (snd <$> encryptions) -- NOTE: drop encNonce
627 individualProofs
628 , answer_sumProof = sumProof
629 }
630 where
631 opinionsSum = sum $ nat <$> opinions
632 opinions = (\o -> if o then one else zero) <$> opinionByChoice
633
634 verifyAnswer ::
635 Reifies v Version =>
636 Reifies c crypto =>
637 Group crypto =>
638 Multiplicative (FieldElement crypto c) =>
639 ToNatural (FieldElement crypto c) =>
640 PublicKey crypto c -> ZKP ->
641 Question v -> Answer crypto v c -> Bool
642 verifyAnswer elecPubKey zkp Question{..} Answer{..}
643 | List.length question_choices /= List.length answer_opinions = False
644 | otherwise = either (const False) id $ runExcept $ do
645 validOpinions <-
646 verifyEncryption elecPubKey zkp booleanDisjunctions
647 `traverse` answer_opinions
648 validSum <- verifyEncryption elecPubKey zkp
649 (intervalDisjunctions question_mini question_maxi)
650 ( sum (fst <$> answer_opinions)
651 , answer_sumProof )
652 return (and validOpinions && validSum)
653
654 -- ** Type 'ErrorAnswer'
655 -- | Error raised by 'encryptAnswer'.
656 data ErrorAnswer
657 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
658 -- ^ When the number of opinions is different than
659 -- the number of choices ('question_choices').
660 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
661 -- ^ When the sum of opinions is not within the bounds
662 -- of 'question_mini' and 'question_maxi'.
663 deriving (Eq,Show,Generic,NFData)
664
665 -- * Type 'Election'
666 data Election crypto v c = Election
667 { election_name :: !Text
668 , election_description :: !Text
669 , election_questions :: ![Question v]
670 , election_uuid :: !UUID
671 , election_hash :: Base64SHA256
672 , election_crypto :: !crypto
673 , election_version :: !(Maybe Version)
674 , election_public_key :: !(PublicKey crypto c)
675 } deriving (Generic)
676 deriving instance (Eq crypto, Eq (FieldElement crypto c)) => Eq (Election crypto v c)
677 deriving instance (Show crypto, Show (FieldElement crypto c)) => Show (Election crypto v c)
678 deriving instance (NFData crypto, NFData (FieldElement crypto c)) => NFData (Election crypto v c)
679 instance
680 ( ToJSON crypto
681 , ToJSON (FieldElement crypto c)
682 , Reifies v Version
683 , Reifies c crypto
684 ) => ToJSON (Election crypto v c) where
685 toJSON Election{..} =
686 JSON.object $
687 [ "name" .= election_name
688 , "description" .= election_description
689 , ("public_key", JSON.object
690 [ "group" .= election_crypto
691 , "y" .= election_public_key
692 ])
693 , "questions" .= election_questions
694 , "uuid" .= election_uuid
695 ] <>
696 maybe [] (\version -> [ "version" .= version ]) election_version
697 toEncoding Election{..} =
698 JSON.pairs $
699 ( "name" .= election_name
700 <> "description" .= election_description
701 <> JSON.pair "public_key" (JSON.pairs $
702 "group" .= election_crypto
703 <> "y" .= election_public_key
704 )
705 <> "questions" .= election_questions
706 <> "uuid" .= election_uuid
707 ) <>
708 maybe mempty ("version" .=) election_version
709
710 readElection ::
711 ReifyCrypto crypto =>
712 FromJSON crypto =>
713 FilePath ->
714 (forall v c.
715 Reifies v Version =>
716 Reifies c crypto =>
717 FieldElementConstraints crypto c =>
718 Election crypto v c -> r) ->
719 ExceptT String IO r
720 readElection filePath k = do
721 fileData <- lift $ BS.readFile filePath
722 ExceptT $ return $
723 jsonEitherFormatError $
724 JSON.eitherDecodeStrictWith JSON.jsonEOF
725 (JSON.iparse (parseElection fileData))
726 fileData
727 where
728 parseElection fileData = JSON.withObject "Election" $ \o -> do
729 election_version <- o .:? "version"
730 reify (fromMaybe stableVersion election_version) $ \(_v::Proxy v) -> do
731 (election_crypto, elecPubKey) <-
732 JSON.explicitParseField
733 (JSON.withObject "public_key" $ \obj -> do
734 crypto <- obj .: "group"
735 pubKey :: JSON.Value <- obj .: "y"
736 return (crypto, pubKey)
737 ) o "public_key"
738 reifyCrypto election_crypto $ \(_c::Proxy c) -> do
739 election_name <- o .: "name"
740 election_description <- o .: "description"
741 election_questions <- o .: "questions" :: JSON.Parser [Question v]
742 election_uuid <- o .: "uuid"
743 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
744 return $ k $ Election
745 { election_questions = election_questions
746 , election_public_key = election_public_key
747 , election_hash = base64SHA256 fileData
748 , ..
749 }
750
751 hashElection ::
752 ToJSON crypto =>
753 Reifies c crypto =>
754 Reifies v Version =>
755 ToJSON (FieldElement crypto c) =>
756 Election crypto v c -> Base64SHA256
757 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
758
759 -- ** Class 'ReifyCrypto'
760 -- | @('reifyCrypto' crypto k)@ is like @('reify' crypto k)@
761 -- but gives to @(k)@ more constraints than just @('Reifies' c crypto)@,
762 -- which is used when defining classes on @(crypto)@
763 -- where @(c)@ (the type variable guarantying the same
764 -- @crypto@graphic parameters are used throughout)
765 -- is not yet in scope and thus where one cannot
766 -- add those constraints requiring to have @(c)@ in scope.
767 -- See for instance the 'QuickcheckElection' class, in the tests.
768 --
769 -- For convenience, the 'ReifyCrypto' class also implies the pervasive
770 -- constraint 'Group'.
771 class
772 ( Group crypto
773 , Key crypto
774 , Show crypto
775 , NFData crypto
776 , JSON.ToJSON crypto
777 , JSON.FromJSON crypto
778 ) => ReifyCrypto crypto where
779 reifyCrypto ::
780 crypto -> (forall c.
781 Reifies c crypto =>
782 FieldElementConstraints crypto c =>
783 Proxy c -> r) -> r
784 instance ReifyCrypto FFC where
785 reifyCrypto = reify
786
787 -- ** Class 'FieldElementConstraints'
788 -- | List the 'Constraint's on the element of the field
789 -- when the @(crypto)@ has not been instantiated to a specific type yet.
790 -- It concerns only 'Constraint's whose method act on @(a)@,
791 -- not @(x c)@ (eg. 'Group').
792 type FieldElementConstraints crypto c =
793 ( Multiplicative (FieldElement crypto c)
794 , FromNatural (FieldElement crypto c)
795 , ToNatural (FieldElement crypto c)
796 , Eq (FieldElement crypto c)
797 , Ord (FieldElement crypto c)
798 , Show (FieldElement crypto c)
799 , NFData (FieldElement crypto c)
800 , FromJSON (FieldElement crypto c)
801 , ToJSON (FieldElement crypto c)
802 , FromJSON (G crypto c)
803 , ToJSON (G crypto c)
804 )
805
806 -- * Type 'Ballot'
807 data Ballot crypto v c = Ballot
808 { ballot_answers :: ![Answer crypto v c]
809 , ballot_signature :: !(Maybe (Signature crypto v c))
810 , ballot_election_uuid :: !UUID
811 , ballot_election_hash :: !Base64SHA256
812 } deriving (Generic)
813 deriving instance (NFData (FieldElement crypto c), NFData crypto) => NFData (Ballot crypto v c)
814 instance
815 ( Reifies v Version
816 , Reifies c crypto
817 , Group crypto
818 , ToJSON (FieldElement crypto c)
819 ) => ToJSON (Ballot crypto v c) where
820 toJSON Ballot{..} =
821 JSON.object $
822 [ "answers" .= ballot_answers
823 , "election_uuid" .= ballot_election_uuid
824 , "election_hash" .= ballot_election_hash
825 ] <>
826 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
827 toEncoding Ballot{..} =
828 JSON.pairs $
829 ( "answers" .= ballot_answers
830 <> "election_uuid" .= ballot_election_uuid
831 <> "election_hash" .= ballot_election_hash
832 ) <>
833 maybe mempty ("signature" .=) ballot_signature
834 instance
835 ( Reifies v Version
836 , Reifies c crypto
837 , Group crypto
838 , FromJSON (G crypto c)
839 ) => FromJSON (Ballot crypto v c) where
840 parseJSON = JSON.withObject "Ballot" $ \o -> do
841 ballot_answers <- o .: "answers"
842 ballot_signature <- o .:? "signature"
843 ballot_election_uuid <- o .: "election_uuid"
844 ballot_election_hash <- o .: "election_hash"
845 return Ballot{..}
846
847 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
848 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
849 -- where 'opinionsByQuest' is a list of 'Opinion's
850 -- on each 'question_choices' of each 'election_questions'.
851 encryptBallot ::
852 forall crypto m v c r.
853 Reifies c crypto =>
854 Reifies v Version =>
855 Group crypto =>
856 Key crypto =>
857 Multiplicative (FieldElement crypto c) =>
858 ToNatural (FieldElement crypto c) =>
859 Monad m => RandomGen r =>
860 Election crypto v c ->
861 Maybe (SecretKey crypto c) -> [[Bool]] ->
862 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
863 encryptBallot Election{..} ballotSecKeyMay opinionsByQuest
864 | List.length election_questions /= List.length opinionsByQuest =
865 lift $ throwE $
866 ErrorBallot_WrongNumberOfAnswers
867 (fromIntegral $ List.length opinionsByQuest)
868 (fromIntegral $ List.length election_questions)
869 | otherwise = do
870 let (voterKeys, voterZKP) =
871 case ballotSecKeyMay of
872 Nothing -> (Nothing, ZKP "")
873 Just ballotSecKey ->
874 ( Just (ballotSecKey, ballotPubKey)
875 , ZKP (bytesNat ballotPubKey) )
876 where ballotPubKey = publicKey ballotSecKey
877 ballot_answers <-
878 S.mapStateT (withExceptT ErrorBallot_Answer) $
879 zipWithM (encryptAnswer election_public_key voterZKP)
880 election_questions opinionsByQuest
881 ballot_signature <- case voterKeys of
882 Nothing -> return Nothing
883 Just (ballotSecKey, signature_publicKey) -> do
884 signature_proof <-
885 proveQuicker ballotSecKey (Identity groupGen) $
886 \(Identity commitment) ->
887 hash @_ @crypto
888 -- NOTE: the order is unusual, the commitments are first
889 -- then comes the statement. Best guess is that
890 -- this is easier to code due to their respective types.
891 (signatureCommitments @_ @crypto voterZKP commitment)
892 (signatureStatement @_ @crypto ballot_answers)
893 return $ Just Signature{..}
894 return Ballot
895 { ballot_answers
896 , ballot_election_hash = election_hash
897 , ballot_election_uuid = election_uuid
898 , ballot_signature
899 }
900
901 verifyBallot ::
902 forall crypto v c.
903 Reifies v Version =>
904 Reifies c crypto =>
905 Group crypto =>
906 Multiplicative (FieldElement crypto c) =>
907 ToNatural (FieldElement crypto c) =>
908 ToNatural (PublicKey crypto c) =>
909 Election crypto v c ->
910 Ballot crypto v c -> Bool
911 verifyBallot Election{..} Ballot{..} =
912 ballot_election_uuid == election_uuid &&
913 ballot_election_hash == election_hash &&
914 List.length election_questions == List.length ballot_answers &&
915 let (isValidSign, zkpSign) =
916 case ballot_signature of
917 Nothing -> (True, ZKP "")
918 Just Signature{..} ->
919 let zkp = ZKP (bytesNat signature_publicKey) in
920 (, zkp) $
921 proof_challenge signature_proof == hash
922 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
923 (signatureStatement @_ @crypto ballot_answers)
924 in
925 and $ isValidSign :
926 List.zipWith (verifyAnswer election_public_key zkpSign)
927 election_questions ballot_answers
928
929 -- ** Type 'Signature'
930 -- | Schnorr-like signature.
931 --
932 -- Used by each voter to sign his/her encrypted 'Ballot'
933 -- using his/her 'Credential',
934 -- in order to avoid ballot stuffing.
935 data Signature crypto v c = Signature
936 { signature_publicKey :: !(PublicKey crypto c)
937 -- ^ Verification key.
938 , signature_proof :: !(Proof crypto v c)
939 } deriving (Generic)
940 deriving instance
941 ( NFData crypto
942 , NFData (FieldElement crypto c)
943 ) => NFData (Signature crypto v c)
944 instance
945 ( Reifies c crypto
946 , Reifies v Version
947 , ToJSON (FieldElement crypto c)
948 ) => ToJSON (Signature crypto v c) where
949 toJSON (Signature pubKey Proof{..}) =
950 JSON.object
951 [ "public_key" .= pubKey
952 , "challenge" .= proof_challenge
953 , "response" .= proof_response
954 ]
955 toEncoding (Signature pubKey Proof{..}) =
956 JSON.pairs
957 ( "public_key" .= pubKey
958 <> "challenge" .= proof_challenge
959 <> "response" .= proof_response
960 )
961 instance
962 ( Reifies c crypto
963 , Reifies v Version
964 , Group crypto
965 , FromJSON (PublicKey crypto c)
966 ) => FromJSON (Signature crypto v c) where
967 parseJSON = JSON.withObject "Signature" $ \o -> do
968 signature_publicKey <- o .: "public_key"
969 proof_challenge <- o .: "challenge"
970 proof_response <- o .: "response"
971 let signature_proof = Proof{..}
972 return Signature{..}
973
974 -- *** Hashing
975
976 -- | @('signatureStatement' answers)@
977 -- returns the encrypted material to be signed:
978 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
979 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
980 signatureStatement =
981 foldMap $ \Answer{..} ->
982 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
983 [encryption_nonce, encryption_vault]
984
985 -- | @('signatureCommitments' voterZKP commitment)@
986 signatureCommitments ::
987 Reifies c crypto =>
988 ToNatural (FieldElement crypto c) =>
989 ZKP -> Commitment crypto c -> BS.ByteString
990 signatureCommitments (ZKP voterZKP) commitment =
991 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
992 <> bytesNat commitment<>"|"
993
994 -- ** Type 'ErrorBallot'
995 -- | Error raised by 'encryptBallot'.
996 data ErrorBallot
997 = ErrorBallot_WrongNumberOfAnswers Natural Natural
998 -- ^ When the number of answers
999 -- is different than the number of questions.
1000 | ErrorBallot_Answer ErrorAnswer
1001 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
1002 | ErrorBallot_Wrong
1003 -- ^ TODO: to be more precise.
1004 deriving (Eq,Show,Generic,NFData)
1005
1006 -- * Type 'Version'
1007 -- | Version of the Helios-C protocol.
1008 data Version = Version
1009 { version_branch :: [Natural]
1010 , version_tags :: [(Text, Natural)]
1011 } deriving (Eq,Ord,Generic,NFData)
1012 instance IsString Version where
1013 fromString = fromJust . readVersion
1014 instance Show Version where
1015 showsPrec _p Version{..} =
1016 List.foldr (.) id
1017 (List.intersperse (showChar '.') $
1018 showsPrec 0 <$> version_branch) .
1019 List.foldr (.) id
1020 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
1021 if n > 0 then showsPrec 0 n else id)
1022 <$> version_tags)
1023 instance ToJSON Version where
1024 toJSON = toJSON . show
1025 toEncoding = toEncoding . show
1026 instance FromJSON Version where
1027 parseJSON (JSON.String s)
1028 | Just v <- readVersion (Text.unpack s)
1029 = return v
1030 parseJSON json = JSON.typeMismatch "Version" json
1031
1032 hasVersionTag :: Version -> Text -> Bool
1033 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
1034
1035 experimentalVersion :: Version
1036 experimentalVersion = stableVersion
1037 {version_tags = [(versionTagQuicker,0)]}
1038
1039 stableVersion :: Version
1040 stableVersion = "1.6"
1041
1042 versionTagQuicker :: Text
1043 versionTagQuicker = "quicker"
1044
1045 readVersion :: String -> Maybe Version
1046 readVersion = parseReadP $ do
1047 version_branch <- Read.sepBy1
1048 (Read.read <$> Read.munch1 Char.isDigit)
1049 (Read.char '.')
1050 version_tags <- Read.many $ (,)
1051 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
1052 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
1053 return Version{..}
1054
1055 parseReadP :: Read.ReadP a -> String -> Maybe a
1056 parseReadP p s =
1057 let p' = Read.readP_to_S p in
1058 listToMaybe $ do
1059 (x, "") <- p' s
1060 return x