]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: work around to avoid ConstraintKinds
[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
57 -- * Type 'Encryption'
58 -- | ElGamal-like encryption.
59 -- Its security relies on the /Discrete Logarithm problem/.
60 --
61 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
62 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
63 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
64 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
65 -- to enable the additive homomorphism.
66 --
67 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
68 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
69 data Encryption crypto v c = Encryption
70 { encryption_nonce :: !(G crypto c)
71 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
72 -- equal to @('groupGen' '^'encNonce)@
73 , encryption_vault :: !(G crypto c)
74 -- ^ Encrypted 'clear' text,
75 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
76 } deriving (Generic)
77 deriving instance Eq (G crypto c) => Eq (Encryption crypto v c)
78 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Encryption crypto v c)
79 deriving instance NFData (G crypto c) => NFData (Encryption crypto v c)
80 instance
81 ( Reifies v Version
82 , Reifies c crypto
83 , ToJSON (G crypto c)
84 ) => ToJSON (Encryption crypto v c) where
85 toJSON Encryption{..} =
86 JSON.object
87 [ "alpha" .= encryption_nonce
88 , "beta" .= encryption_vault
89 ]
90 toEncoding Encryption{..} =
91 JSON.pairs
92 ( "alpha" .= encryption_nonce
93 <> "beta" .= encryption_vault
94 )
95 instance
96 ( Reifies v Version
97 , Reifies c crypto
98 , FromJSON (G crypto c)
99 ) => FromJSON (Encryption crypto v c) where
100 parseJSON = JSON.withObject "Encryption" $ \o -> do
101 encryption_nonce <- o .: "alpha"
102 encryption_vault <- o .: "beta"
103 return Encryption{..}
104
105 -- | Additive homomorphism.
106 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
107 instance
108 ( Reifies c crypto
109 , Multiplicative (G crypto c)
110 ) => Additive (Encryption crypto v c) where
111 zero = Encryption one one
112 x+y = Encryption
113 (encryption_nonce x * encryption_nonce y)
114 (encryption_vault x * encryption_vault y)
115
116 -- *** Type 'EncryptionNonce'
117 type EncryptionNonce = E
118
119 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
120 --
121 -- WARNING: the secret encryption nonce (@encNonce@)
122 -- is returned alongside the 'Encryption'
123 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
124 -- but this secret @encNonce@ MUST be forgotten after that,
125 -- as it may be used to decipher the 'Encryption'
126 -- without the 'SecretKey' associated with 'pubKey'.
127 encrypt ::
128 Reifies v Version =>
129 Reifies c crypto =>
130 Group crypto =>
131 Multiplicative (G crypto c) =>
132 Monad m => RandomGen r =>
133 PublicKey crypto c -> E crypto c ->
134 S.StateT r m (EncryptionNonce crypto c, Encryption crypto v c)
135 encrypt pubKey clear = do
136 encNonce <- random
137 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
138 return $ (encNonce,)
139 Encryption
140 { encryption_nonce = groupGen^encNonce
141 , encryption_vault = pubKey ^encNonce * groupGen^clear
142 }
143
144 -- * Type 'Proof'
145 -- | Non-Interactive Zero-Knowledge 'Proof'
146 -- of knowledge of a discrete logarithm:
147 -- @(secret == logBase base (base^secret))@.
148 data Proof crypto v c = Proof
149 { proof_challenge :: !(Challenge crypto c)
150 -- ^ 'Challenge' sent by the verifier to the prover
151 -- to ensure that the prover really has knowledge
152 -- of the secret and is not replaying.
153 -- Actually, 'proof_challenge' is not sent to the prover,
154 -- but derived from the prover's 'Commitment's and statements
155 -- with a collision resistant 'hash'.
156 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
157 , proof_response :: !(E crypto c)
158 -- ^ A discrete logarithm sent by the prover to the verifier,
159 -- as a response to 'proof_challenge'.
160 --
161 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
162 --
163 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
164 -- * @commitment '==' 'commit' proof base basePowSec '=='
165 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
166 -- * and @basePowSec '==' base'^'sec@,
167 --
168 -- then, with overwhelming probability (due to the 'hash' function),
169 -- the prover was not able to choose 'proof_challenge'
170 -- yet was able to compute a 'proof_response' such that
171 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
172 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
173 -- therefore the prover knows 'sec'.
174 --
175 -- The prover choses 'commitment' to be a random power of @base@,
176 -- to ensure that each 'prove' does not reveal any information
177 -- about its secret.
178 } deriving (Eq,Show,NFData,Generic)
179 instance Group crypto => ToJSON (Proof crypto v c) where
180 toJSON Proof{..} =
181 JSON.object
182 [ "challenge" .= proof_challenge
183 , "response" .= proof_response
184 ]
185 toEncoding Proof{..} =
186 JSON.pairs
187 ( "challenge" .= proof_challenge
188 <> "response" .= proof_response
189 )
190 instance (Reifies c crypto, Group crypto) => FromJSON (Proof crypto v c) where
191 parseJSON = JSON.withObject "TrusteePublicKey" $ \o -> do
192 proof_challenge <- o .: "challenge"
193 proof_response <- o .: "response"
194 return Proof{..}
195
196 -- ** Type 'ZKP'
197 -- | Zero-knowledge proof.
198 --
199 -- A protocol is /zero-knowledge/ if the verifier
200 -- learns nothing from the protocol except that the prover
201 -- knows the secret.
202 --
203 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
204 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
205 newtype ZKP = ZKP BS.ByteString
206
207 -- ** Type 'Challenge'
208 type Challenge = E
209
210 -- ** Type 'Oracle'
211 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
212 -- by 'hash'ing them (eventually with other 'Commitment's).
213 --
214 -- Used in 'prove' it enables a Fiat-Shamir transformation
215 -- of an /interactive zero-knowledge/ (IZK) proof
216 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
217 -- That is to say that the verifier does not have
218 -- to send a 'Challenge' to the prover.
219 -- Indeed, the prover now handles the 'Challenge'
220 -- which becomes a (collision resistant) 'hash'
221 -- of the prover's commitments (and statements to be a stronger proof).
222 type Oracle list crypto c = list (Commitment crypto c) -> Challenge crypto c
223
224 -- | @('prove' sec commitmentBases oracle)@
225 -- returns a 'Proof' that @sec@ is known
226 -- (by proving the knowledge of its discrete logarithm).
227 --
228 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
229 -- raised to the power of the secret nonce of the 'Proof',
230 -- as those are the 'Commitment's that the verifier will obtain
231 -- when composing the 'proof_challenge' and 'proof_response' together
232 -- (with 'commit').
233 --
234 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
235 -- the statement must be included in the 'hash' (along with the commitments).
236 --
237 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
238 -- does not reveal any information regarding the secret @sec@,
239 -- because two 'Proof's using the same 'Commitment'
240 -- can be used to deduce @sec@ (using the special-soundness).
241 prove ::
242 forall crypto v c list m r.
243 Reifies c crypto =>
244 Reifies v Version =>
245 Group crypto =>
246 Multiplicative (G crypto c) =>
247 Monad m => RandomGen r => Functor list =>
248 E crypto c ->
249 list (G crypto c) ->
250 Oracle list crypto c ->
251 S.StateT r m (Proof crypto v c)
252 prove sec commitmentBases oracle = do
253 nonce <- random
254 let commitments = (^ nonce) <$> commitmentBases
255 let proof_challenge = oracle commitments
256 return Proof
257 { proof_challenge
258 , proof_response = nonce `op` (sec*proof_challenge)
259 }
260 where
261 -- | See comments in 'commit'.
262 op =
263 if reflect (Proxy @v) `hasVersionTag` versionTagQuicker
264 then (-)
265 else (+)
266
267 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
268 -- when Helios-C specifications will be fixed.
269 proveQuicker ::
270 Reifies c crypto =>
271 Reifies v Version =>
272 Group crypto =>
273 Multiplicative (G crypto c) =>
274 Monad m => RandomGen r => Functor list =>
275 E crypto c ->
276 list (G crypto c) ->
277 Oracle list crypto c ->
278 S.StateT r m (Proof crypto v c)
279 proveQuicker sec commitmentBases oracle = do
280 nonce <- random
281 let commitments = (^ nonce) <$> commitmentBases
282 let proof_challenge = oracle commitments
283 return Proof
284 { proof_challenge
285 , proof_response = nonce - sec*proof_challenge
286 }
287
288 -- | @('fakeProof')@ returns a 'Proof'
289 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
290 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
291 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
292 -- as a 'Proof' returned by 'prove'.
293 --
294 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
295 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
296 fakeProof ::
297 Reifies c crypto =>
298 Group crypto =>
299 Monad m => RandomGen r =>
300 S.StateT r m (Proof crypto v c)
301 fakeProof = do
302 proof_challenge <- random
303 proof_response <- random
304 return Proof{..}
305
306 -- ** Type 'Commitment'
307 -- | A commitment from the prover to the verifier.
308 -- It's a power of 'groupGen' chosen randomly by the prover
309 -- when making a 'Proof' with 'prove'.
310 type Commitment = G
311
312 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
313 -- from the given 'Proof' with the knowledge of the verifier.
314 commit ::
315 forall crypto v c.
316 Reifies v Version =>
317 Reifies c crypto =>
318 Group crypto =>
319 Multiplicative (G crypto c) =>
320 Invertible (G 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 Group 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 Invertible (G crypto c) =>
364 [Disjunction crypto c]
365 booleanDisjunctions = List.take 2 $ groupGenInverses @crypto
366
367 intervalDisjunctions ::
368 forall crypto c.
369 Reifies c crypto =>
370 Group crypto =>
371 Invertible (G crypto c) =>
372 Natural -> Natural -> [Disjunction crypto c]
373 intervalDisjunctions mini maxi =
374 List.genericTake (fromJust $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
375 List.genericDrop (nat mini) $
376 groupGenInverses @crypto
377
378 -- ** Type 'Opinion'
379 -- | Index of a 'Disjunction' within a list of them.
380 -- It is encrypted as a 'GroupExponent' by 'encrypt'.
381 type Opinion = E
382
383 -- ** Type 'DisjProof'
384 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
385 -- is indexing a 'Disjunction' within a list of them,
386 -- without revealing which 'Opinion' it is.
387 newtype DisjProof crypto v c = DisjProof [Proof crypto v c]
388 deriving (Eq,Show,Generic)
389 deriving newtype (NFData,ToJSON,FromJSON)
390
391 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
392 -- returns a 'DisjProof' that 'enc' 'encrypt's
393 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
394 --
395 -- The prover proves that it knows an 'encNonce', such that:
396 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
397 --
398 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
399 --
400 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
401 proveEncryption ::
402 Reifies v Version =>
403 Reifies c crypto =>
404 Group crypto =>
405 ToNatural (G crypto c) =>
406 Multiplicative (G crypto c) =>
407 Invertible (G crypto c) =>
408 Monad m => RandomGen r =>
409 PublicKey crypto c -> ZKP ->
410 ([Disjunction crypto c],[Disjunction crypto c]) ->
411 (EncryptionNonce crypto c, Encryption crypto v c) ->
412 S.StateT r m (DisjProof crypto v c)
413 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
414 -- Fake proofs for all 'Disjunction's except the genuine one.
415 prevFakeProofs <- replicateM (List.length prevDisjs) fakeProof
416 nextFakeProofs <- replicateM (List.length nextDisjs) fakeProof
417 let fakeChallengeSum =
418 sum (proof_challenge <$> prevFakeProofs) +
419 sum (proof_challenge <$> nextFakeProofs)
420 let statement = encryptionStatement voterZKP enc
421 genuineProof <- prove encNonce [groupGen, elecPubKey] $ \genuineCommitments ->
422 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) in
423 let prevCommitments = validCommitments prevDisjs prevFakeProofs in
424 let nextCommitments = validCommitments nextDisjs nextFakeProofs in
425 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments in
426 let challenge = hash statement commitments in
427 let genuineChallenge = challenge - fakeChallengeSum in
428 genuineChallenge
429 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
430 -- thus (sum (proof_challenge <$> proofs) == challenge)
431 -- as checked in 'verifyEncryption'.
432 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
433 return (DisjProof proofs)
434
435 verifyEncryption ::
436 Reifies v Version =>
437 Reifies c crypto =>
438 Group crypto =>
439 ToNatural (G crypto c) =>
440 Multiplicative (G crypto c) =>
441 Invertible (G crypto c) =>
442 Monad m =>
443 PublicKey crypto c -> ZKP ->
444 [Disjunction crypto c] -> (Encryption crypto v c, DisjProof crypto v c) ->
445 ExceptT ErrorVerifyEncryption m Bool
446 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs) =
447 case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of
448 Nothing ->
449 throwE $ ErrorVerifyEncryption_InvalidProofLength
450 (fromIntegral $ List.length proofs)
451 (fromIntegral $ List.length disjs)
452 Just commitments ->
453 return $ challengeSum ==
454 hash (encryptionStatement voterZKP enc) (join commitments)
455 where
456 challengeSum = sum (proof_challenge <$> proofs)
457
458 -- ** Hashing
459 encryptionStatement ::
460 Reifies c crypto =>
461 ToNatural (G crypto c) =>
462 ZKP -> Encryption crypto v c -> BS.ByteString
463 encryptionStatement (ZKP voterZKP) Encryption{..} =
464 "prove|"<>voterZKP<>"|"
465 <> bytesNat encryption_nonce<>","
466 <> bytesNat encryption_vault<>"|"
467
468 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
469 -- returns the 'Commitment's with only the knowledge of the verifier.
470 --
471 -- For the prover the 'Proof' comes from @fakeProof@,
472 -- and for the verifier the 'Proof' comes from the prover.
473 encryptionCommitments ::
474 Reifies v Version =>
475 Reifies c crypto =>
476 Group crypto =>
477 Invertible (G crypto c) =>
478 PublicKey crypto c -> Encryption crypto v c ->
479 Disjunction crypto c -> Proof crypto v c -> [G crypto c]
480 encryptionCommitments elecPubKey Encryption{..} disj proof =
481 [ commit proof groupGen encryption_nonce
482 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
483 -- base==groupGen, basePowSec==groupGen^encNonce.
484 , commit proof elecPubKey (encryption_vault*disj)
485 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
486 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
487 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
488 ]
489
490 -- ** Type 'ErrorVerifyEncryption'
491 -- | Error raised by 'verifyEncryption'.
492 data ErrorVerifyEncryption
493 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
494 -- ^ When the number of proofs is different than
495 -- the number of 'Disjunction's.
496 deriving (Eq,Show)
497
498 -- * Type 'Question'
499 data Question v = Question
500 { question_text :: !Text
501 , question_choices :: ![Text]
502 , question_mini :: !Natural
503 , question_maxi :: !Natural
504 -- , question_blank :: Maybe Bool
505 } deriving (Eq,Show,Generic,NFData)
506 instance Reifies v Version => ToJSON (Question v) where
507 toJSON Question{..} =
508 JSON.object
509 [ "question" .= question_text
510 , "answers" .= question_choices
511 , "min" .= question_mini
512 , "max" .= question_maxi
513 ]
514 toEncoding Question{..} =
515 JSON.pairs
516 ( "question" .= question_text
517 <> "answers" .= question_choices
518 <> "min" .= question_mini
519 <> "max" .= question_maxi
520 )
521 instance Reifies v Version => FromJSON (Question v) where
522 parseJSON = JSON.withObject "Question" $ \o -> do
523 question_text <- o .: "question"
524 question_choices <- o .: "answers"
525 question_mini <- o .: "min"
526 question_maxi <- o .: "max"
527 return Question{..}
528
529 -- * Type 'Answer'
530 data Answer crypto v c = Answer
531 { answer_opinions :: ![(Encryption crypto v c, DisjProof crypto v c)]
532 -- ^ Encrypted 'Opinion' for each 'question_choices'
533 -- with a 'DisjProof' that they belong to [0,1].
534 , answer_sumProof :: !(DisjProof crypto v c)
535 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
536 -- is an element of @[mini..maxi]@.
537 -- , answer_blankProof ::
538 } deriving (Generic)
539 deriving instance Eq (G crypto c) => Eq (Answer crypto v c)
540 deriving instance (Show (G crypto c), Show (G crypto c)) => Show (Answer crypto v c)
541 deriving instance NFData (G crypto c) => NFData (Answer crypto v c)
542 instance
543 ( Reifies v Version
544 , Reifies c crypto
545 , ToJSON (G crypto c)
546 , Group crypto
547 ) => ToJSON (Answer crypto v c) where
548 toJSON Answer{..} =
549 let (answer_choices, answer_individual_proofs) =
550 List.unzip answer_opinions in
551 JSON.object
552 [ "choices" .= answer_choices
553 , "individual_proofs" .= answer_individual_proofs
554 , "overall_proof" .= answer_sumProof
555 ]
556 toEncoding Answer{..} =
557 let (answer_choices, answer_individual_proofs) =
558 List.unzip answer_opinions in
559 JSON.pairs
560 ( "choices" .= answer_choices
561 <> "individual_proofs" .= answer_individual_proofs
562 <> "overall_proof" .= answer_sumProof
563 )
564 instance
565 ( Reifies v Version
566 , Reifies c crypto
567 , FromJSON (G crypto c)
568 , Group crypto
569 ) => FromJSON (Answer crypto v c) where
570 parseJSON = JSON.withObject "Answer" $ \o -> do
571 answer_choices <- o .: "choices"
572 answer_individual_proofs <- o .: "individual_proofs"
573 let answer_opinions = List.zip answer_choices answer_individual_proofs
574 answer_sumProof <- o .: "overall_proof"
575 return Answer{..}
576
577 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
578 -- returns an 'Answer' validable by 'verifyAnswer',
579 -- unless an 'ErrorAnswer' is returned.
580 encryptAnswer ::
581 Reifies v Version =>
582 Reifies c crypto => Group crypto =>
583 Monad m => RandomGen r =>
584 PublicKey crypto c -> ZKP ->
585 Question v -> [Bool] ->
586 S.StateT r (ExceptT ErrorAnswer m) (Answer crypto v c)
587 encryptAnswer (elecPubKey::PublicKey crypto c) zkp Question{..} opinionByChoice
588 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
589 lift $ throwE $
590 ErrorAnswer_WrongSumOfOpinions opinionsSum question_mini question_maxi
591 | List.length opinions /= List.length question_choices =
592 lift $ throwE $
593 ErrorAnswer_WrongNumberOfOpinions
594 (fromIntegral $ List.length opinions)
595 (fromIntegral $ List.length question_choices)
596 | otherwise = groupReify (Proxy @c) $ do
597 encryptions <- encrypt elecPubKey `mapM` opinions
598 individualProofs <- zipWithM
599 (\opinion -> proveEncryption elecPubKey zkp $
600 if opinion
601 then (List.init booleanDisjunctions,[])
602 else ([],List.tail booleanDisjunctions))
603 opinionByChoice encryptions
604 sumProof <- proveEncryption elecPubKey zkp
605 (List.tail <$> List.genericSplitAt
606 (fromJust $ opinionsSum`minusNaturalMaybe`question_mini)
607 (intervalDisjunctions question_mini question_maxi))
608 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
609 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
610 )
611 return $ Answer
612 { answer_opinions = List.zip
613 (snd <$> encryptions) -- NOTE: drop encNonce
614 individualProofs
615 , answer_sumProof = sumProof
616 }
617 where
618 opinionsSum = sum $ nat <$> opinions
619 opinions = (\o -> if o then one else zero) <$> opinionByChoice
620
621 verifyAnswer ::
622 Reifies v Version =>
623 Reifies c crypto => Group crypto =>
624 PublicKey crypto c -> ZKP ->
625 Question v -> Answer crypto v c -> Bool
626 verifyAnswer (elecPubKey::PublicKey crypto c) zkp Question{..} Answer{..}
627 | List.length question_choices /= List.length answer_opinions = False
628 | otherwise = groupReify (Proxy @c) $ do
629 either (const False) id $ runExcept $ do
630 validOpinions <-
631 verifyEncryption elecPubKey zkp booleanDisjunctions
632 `traverse` answer_opinions
633 validSum <- verifyEncryption elecPubKey zkp
634 (intervalDisjunctions question_mini question_maxi)
635 ( sum (fst <$> answer_opinions)
636 , answer_sumProof )
637 return (and validOpinions && validSum)
638
639 -- ** Type 'ErrorAnswer'
640 -- | Error raised by 'encryptAnswer'.
641 data ErrorAnswer
642 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
643 -- ^ When the number of opinions is different than
644 -- the number of choices ('question_choices').
645 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
646 -- ^ When the sum of opinions is not within the bounds
647 -- of 'question_mini' and 'question_maxi'.
648 deriving (Eq,Show,Generic,NFData)
649
650 -- * Type 'Election'
651 data Election crypto v c = Election
652 { election_name :: !Text
653 , election_description :: !Text
654 , election_questions :: ![Question v]
655 , election_uuid :: !UUID
656 , election_hash :: Base64SHA256
657 , election_crypto :: !crypto
658 , election_version :: !(Maybe Version)
659 , election_public_key :: !(PublicKey crypto c)
660 } deriving (Generic)
661 deriving instance (Eq crypto, Eq (G crypto c)) => Eq (Election crypto v c)
662 deriving instance (Show crypto, Show (G crypto c)) => Show (Election crypto v c)
663 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Election crypto v c)
664 instance
665 ( ToJSON crypto
666 , ToJSON (G crypto c)
667 , Reifies v Version
668 , Reifies c crypto
669 ) => ToJSON (Election crypto v c) where
670 toJSON Election{..} =
671 JSON.object $
672 [ "name" .= election_name
673 , "description" .= election_description
674 , ("public_key", JSON.object
675 [ "group" .= election_crypto
676 , "y" .= election_public_key
677 ])
678 , "questions" .= election_questions
679 , "uuid" .= election_uuid
680 ] <>
681 maybe [] (\version -> [ "version" .= version ]) election_version
682 toEncoding Election{..} =
683 JSON.pairs $
684 ( "name" .= election_name
685 <> "description" .= election_description
686 <> JSON.pair "public_key" (JSON.pairs $
687 "group" .= election_crypto
688 <> "y" .= election_public_key
689 )
690 <> "questions" .= election_questions
691 <> "uuid" .= election_uuid
692 ) <>
693 maybe mempty ("version" .=) election_version
694
695 readElection ::
696 FromJSON crypto =>
697 Group crypto =>
698 FilePath ->
699 (forall v c.
700 Reifies v Version =>
701 Reifies c crypto =>
702 GroupDict crypto c =>
703 Election crypto v c -> r) ->
704 ExceptT String IO r
705 readElection filePath k = do
706 fileData <- lift $ BS.readFile filePath
707 ExceptT $ return $
708 jsonEitherFormatError $
709 JSON.eitherDecodeStrictWith JSON.jsonEOF
710 (JSON.iparse (parseElection fileData))
711 fileData
712 where
713 parseElection fileData = JSON.withObject "Election" $ \o -> do
714 election_version <- o .:? "version"
715 reify (fromMaybe stableVersion election_version) $ \(_v::Proxy v) -> do
716 (election_crypto, elecPubKey) <-
717 JSON.explicitParseField
718 (JSON.withObject "public_key" $ \obj -> do
719 crypto <- obj .: "group"
720 pubKey :: JSON.Value <- obj .: "y"
721 return (crypto, pubKey)
722 ) o "public_key"
723 reify election_crypto $ \(c::Proxy c) -> groupReify c $ do
724 election_name <- o .: "name"
725 election_description <- o .: "description"
726 election_questions <- o .: "questions" :: JSON.Parser [Question v]
727 election_uuid <- o .: "uuid"
728 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
729 return $ k $ Election
730 { election_questions = election_questions
731 , election_public_key = election_public_key
732 , election_hash = base64SHA256 fileData
733 , ..
734 }
735
736 hashElection ::
737 ToJSON crypto =>
738 Reifies c crypto =>
739 Reifies v Version =>
740 ToJSON (G crypto c) =>
741 Election crypto v c -> Base64SHA256
742 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
743
744 -- * Type 'Ballot'
745 data Ballot crypto v c = Ballot
746 { ballot_answers :: ![Answer crypto v c]
747 , ballot_signature :: !(Maybe (Signature crypto v c))
748 , ballot_election_uuid :: !UUID
749 , ballot_election_hash :: !Base64SHA256
750 } deriving (Generic)
751 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
752 instance
753 ( Reifies v Version
754 , Reifies c crypto
755 , Group crypto
756 , ToJSON (G crypto c)
757 ) => ToJSON (Ballot crypto v c) where
758 toJSON Ballot{..} =
759 JSON.object $
760 [ "answers" .= ballot_answers
761 , "election_uuid" .= ballot_election_uuid
762 , "election_hash" .= ballot_election_hash
763 ] <>
764 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
765 toEncoding Ballot{..} =
766 JSON.pairs $
767 ( "answers" .= ballot_answers
768 <> "election_uuid" .= ballot_election_uuid
769 <> "election_hash" .= ballot_election_hash
770 ) <>
771 maybe mempty ("signature" .=) ballot_signature
772 instance
773 ( Reifies v Version
774 , Reifies c crypto
775 , Group crypto
776 , FromJSON (G crypto c)
777 ) => FromJSON (Ballot crypto v c) where
778 parseJSON = JSON.withObject "Ballot" $ \o -> do
779 ballot_answers <- o .: "answers"
780 ballot_signature <- o .:? "signature"
781 ballot_election_uuid <- o .: "election_uuid"
782 ballot_election_hash <- o .: "election_hash"
783 return Ballot{..}
784
785 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
786 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
787 -- where 'opinionsByQuest' is a list of 'Opinion's
788 -- on each 'question_choices' of each 'election_questions'.
789 encryptBallot ::
790 Reifies v Version =>
791 Reifies c crypto => Group crypto => Key crypto =>
792 Monad m => RandomGen r =>
793 Election crypto v c ->
794 Maybe (SecretKey crypto c) -> [[Bool]] ->
795 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
796 encryptBallot (Election{..}::Election crypto v c) ballotSecKeyMay opinionsByQuest
797 | List.length election_questions /= List.length opinionsByQuest =
798 lift $ throwE $
799 ErrorBallot_WrongNumberOfAnswers
800 (fromIntegral $ List.length opinionsByQuest)
801 (fromIntegral $ List.length election_questions)
802 | otherwise = groupReify (Proxy @c) $ do
803 let (voterKeys, voterZKP) =
804 case ballotSecKeyMay of
805 Nothing -> (Nothing, ZKP "")
806 Just ballotSecKey ->
807 ( Just (ballotSecKey, ballotPubKey)
808 , ZKP (bytesNat ballotPubKey) )
809 where ballotPubKey = publicKey ballotSecKey
810 ballot_answers <-
811 S.mapStateT (withExceptT ErrorBallot_Answer) $
812 zipWithM (encryptAnswer election_public_key voterZKP)
813 election_questions opinionsByQuest
814 ballot_signature <- case voterKeys of
815 Nothing -> return Nothing
816 Just (ballotSecKey, signature_publicKey) -> do
817 signature_proof <-
818 proveQuicker ballotSecKey (Identity groupGen) $
819 \(Identity commitment) ->
820 hash @_ @crypto
821 -- NOTE: the order is unusual, the commitments are first
822 -- then comes the statement. Best guess is that
823 -- this is easier to code due to their respective types.
824 (signatureCommitments @_ @crypto voterZKP commitment)
825 (signatureStatement @_ @crypto ballot_answers)
826 return $ Just Signature{..}
827 return Ballot
828 { ballot_answers
829 , ballot_election_hash = election_hash
830 , ballot_election_uuid = election_uuid
831 , ballot_signature
832 }
833
834 verifyBallot ::
835 Reifies v Version =>
836 Reifies c crypto => Group crypto =>
837 Election crypto v c ->
838 Ballot crypto v c -> Bool
839 verifyBallot (Election{..}::Election crypto v c) Ballot{..} = groupReify (Proxy @c) $
840 ballot_election_uuid == election_uuid &&
841 ballot_election_hash == election_hash &&
842 List.length election_questions == List.length ballot_answers &&
843 let (isValidSign, zkpSign) =
844 case ballot_signature of
845 Nothing -> (True, ZKP "")
846 Just Signature{..} ->
847 let zkp = ZKP (bytesNat signature_publicKey) in
848 (, zkp) $
849 proof_challenge signature_proof == hash
850 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
851 (signatureStatement @_ @crypto ballot_answers)
852 in
853 and $ isValidSign :
854 List.zipWith (verifyAnswer election_public_key zkpSign)
855 election_questions ballot_answers
856
857 -- ** Type 'Signature'
858 -- | Schnorr-like signature.
859 --
860 -- Used by each voter to sign his/her encrypted 'Ballot'
861 -- using his/her 'Credential',
862 -- in order to avoid ballot stuffing.
863 data Signature crypto v c = Signature
864 { signature_publicKey :: !(PublicKey crypto c)
865 -- ^ Verification key.
866 , signature_proof :: !(Proof crypto v c)
867 } deriving (Generic)
868 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Signature crypto v c)
869 instance
870 ( Reifies c crypto
871 , Reifies v Version
872 , ToJSON (G crypto c)
873 ) => ToJSON (Signature crypto v c) where
874 toJSON (Signature pubKey Proof{..}) =
875 JSON.object
876 [ "public_key" .= pubKey
877 , "challenge" .= proof_challenge
878 , "response" .= proof_response
879 ]
880 toEncoding (Signature pubKey Proof{..}) =
881 JSON.pairs
882 ( "public_key" .= pubKey
883 <> "challenge" .= proof_challenge
884 <> "response" .= proof_response
885 )
886 instance
887 ( Reifies c crypto
888 , Reifies v Version
889 , Group crypto
890 , FromJSON (PublicKey crypto c)
891 ) => FromJSON (Signature crypto v c) where
892 parseJSON = JSON.withObject "Signature" $ \o -> do
893 signature_publicKey <- o .: "public_key"
894 proof_challenge <- o .: "challenge"
895 proof_response <- o .: "response"
896 let signature_proof = Proof{..}
897 return Signature{..}
898
899 -- *** Hashing
900
901 -- | @('signatureStatement' answers)@
902 -- returns the encrypted material to be signed:
903 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
904 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
905 signatureStatement =
906 foldMap $ \Answer{..} ->
907 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
908 [encryption_nonce, encryption_vault]
909
910 -- | @('signatureCommitments' voterZKP commitment)@
911 signatureCommitments ::
912 Reifies c crypto =>
913 ToNatural (G crypto c) =>
914 ZKP -> Commitment crypto c -> BS.ByteString
915 signatureCommitments (ZKP voterZKP) commitment =
916 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
917 <> bytesNat commitment<>"|"
918
919 -- ** Type 'ErrorBallot'
920 -- | Error raised by 'encryptBallot'.
921 data ErrorBallot
922 = ErrorBallot_WrongNumberOfAnswers Natural Natural
923 -- ^ When the number of answers
924 -- is different than the number of questions.
925 | ErrorBallot_Answer ErrorAnswer
926 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
927 | ErrorBallot_Wrong
928 -- ^ TODO: to be more precise.
929 deriving (Eq,Show,Generic,NFData)
930
931 -- * Type 'Version'
932 -- | Version of the Helios-C protocol.
933 data Version = Version
934 { version_branch :: [Natural]
935 , version_tags :: [(Text, Natural)]
936 } deriving (Eq,Ord,Generic,NFData)
937 instance IsString Version where
938 fromString = fromJust . readVersion
939 instance Show Version where
940 showsPrec _p Version{..} =
941 List.foldr (.) id
942 (List.intersperse (showChar '.') $
943 showsPrec 0 <$> version_branch) .
944 List.foldr (.) id
945 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
946 if n > 0 then showsPrec 0 n else id)
947 <$> version_tags)
948 instance ToJSON Version where
949 toJSON = toJSON . show
950 toEncoding = toEncoding . show
951 instance FromJSON Version where
952 parseJSON (JSON.String s)
953 | Just v <- readVersion (Text.unpack s)
954 = return v
955 parseJSON json = JSON.typeMismatch "Version" json
956
957 hasVersionTag :: Version -> Text -> Bool
958 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
959
960 experimentalVersion :: Version
961 experimentalVersion = stableVersion
962 {version_tags = [(versionTagQuicker,0)]}
963
964 stableVersion :: Version
965 stableVersion = "1.6"
966
967 versionTagQuicker :: Text
968 versionTagQuicker = "quicker"
969
970 readVersion :: String -> Maybe Version
971 readVersion = parseReadP $ do
972 version_branch <- Read.sepBy1
973 (Read.read <$> Read.munch1 Char.isDigit)
974 (Read.char '.')
975 version_tags <- Read.many $ (,)
976 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
977 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
978 return Version{..}
979
980 parseReadP :: Read.ReadP a -> String -> Maybe a
981 parseReadP p s =
982 let p' = Read.readP_to_S p in
983 listToMaybe $ do
984 (x, "") <- p' s
985 return x