]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/src/Voting/Protocol/Election.hs
protocol: replace reifyCrypto by groupDict
[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, Dict <- groupDict (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, Dict <- groupDict (Proxy @c) =
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 $ \case
724 (c::Proxy c) | Dict <- groupDict c -> do
725 election_name <- o .: "name"
726 election_description <- o .: "description"
727 election_questions <- o .: "questions" :: JSON.Parser [Question v]
728 election_uuid <- o .: "uuid"
729 election_public_key :: PublicKey crypto c <- parseJSON elecPubKey
730 return $ k $ Election
731 { election_questions = election_questions
732 , election_public_key = election_public_key
733 , election_hash = base64SHA256 fileData
734 , ..
735 }
736
737 hashElection ::
738 ToJSON crypto =>
739 Reifies c crypto =>
740 Reifies v Version =>
741 ToJSON (G crypto c) =>
742 Election crypto v c -> Base64SHA256
743 hashElection = base64SHA256 . BSL.toStrict . JSON.encode
744
745 -- * Type 'Ballot'
746 data Ballot crypto v c = Ballot
747 { ballot_answers :: ![Answer crypto v c]
748 , ballot_signature :: !(Maybe (Signature crypto v c))
749 , ballot_election_uuid :: !UUID
750 , ballot_election_hash :: !Base64SHA256
751 } deriving (Generic)
752 deriving instance (NFData (G crypto c), NFData crypto) => NFData (Ballot crypto v c)
753 instance
754 ( Reifies v Version
755 , Reifies c crypto
756 , Group crypto
757 , ToJSON (G crypto c)
758 ) => ToJSON (Ballot crypto v c) where
759 toJSON Ballot{..} =
760 JSON.object $
761 [ "answers" .= ballot_answers
762 , "election_uuid" .= ballot_election_uuid
763 , "election_hash" .= ballot_election_hash
764 ] <>
765 maybe [] (\sig -> [ "signature" .= sig ]) ballot_signature
766 toEncoding Ballot{..} =
767 JSON.pairs $
768 ( "answers" .= ballot_answers
769 <> "election_uuid" .= ballot_election_uuid
770 <> "election_hash" .= ballot_election_hash
771 ) <>
772 maybe mempty ("signature" .=) ballot_signature
773 instance
774 ( Reifies v Version
775 , Reifies c crypto
776 , Group crypto
777 , FromJSON (G crypto c)
778 ) => FromJSON (Ballot crypto v c) where
779 parseJSON = JSON.withObject "Ballot" $ \o -> do
780 ballot_answers <- o .: "answers"
781 ballot_signature <- o .:? "signature"
782 ballot_election_uuid <- o .: "election_uuid"
783 ballot_election_hash <- o .: "election_hash"
784 return Ballot{..}
785
786 -- | @('encryptBallot' c ('Just' ballotSecKey) opinionsByQuest)@
787 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
788 -- where 'opinionsByQuest' is a list of 'Opinion's
789 -- on each 'question_choices' of each 'election_questions'.
790 encryptBallot ::
791 Reifies v Version =>
792 Reifies c crypto => Group crypto => Key crypto =>
793 Monad m => RandomGen r =>
794 Election crypto v c ->
795 Maybe (SecretKey crypto c) -> [[Bool]] ->
796 S.StateT r (ExceptT ErrorBallot m) (Ballot crypto v c)
797 encryptBallot (Election{..}::Election crypto v c) ballotSecKeyMay opinionsByQuest
798 | List.length election_questions /= List.length opinionsByQuest =
799 lift $ throwE $
800 ErrorBallot_WrongNumberOfAnswers
801 (fromIntegral $ List.length opinionsByQuest)
802 (fromIntegral $ List.length election_questions)
803 | otherwise, Dict <- groupDict (Proxy @c) = do
804 let (voterKeys, voterZKP) =
805 case ballotSecKeyMay of
806 Nothing -> (Nothing, ZKP "")
807 Just ballotSecKey ->
808 ( Just (ballotSecKey, ballotPubKey)
809 , ZKP (bytesNat ballotPubKey) )
810 where ballotPubKey = publicKey ballotSecKey
811 ballot_answers <-
812 S.mapStateT (withExceptT ErrorBallot_Answer) $
813 zipWithM (encryptAnswer election_public_key voterZKP)
814 election_questions opinionsByQuest
815 ballot_signature <- case voterKeys of
816 Nothing -> return Nothing
817 Just (ballotSecKey, signature_publicKey) -> do
818 signature_proof <-
819 proveQuicker ballotSecKey (Identity groupGen) $
820 \(Identity commitment) ->
821 hash @_ @crypto
822 -- NOTE: the order is unusual, the commitments are first
823 -- then comes the statement. Best guess is that
824 -- this is easier to code due to their respective types.
825 (signatureCommitments @_ @crypto voterZKP commitment)
826 (signatureStatement @_ @crypto ballot_answers)
827 return $ Just Signature{..}
828 return Ballot
829 { ballot_answers
830 , ballot_election_hash = election_hash
831 , ballot_election_uuid = election_uuid
832 , ballot_signature
833 }
834
835 verifyBallot ::
836 Reifies v Version =>
837 Reifies c crypto => Group crypto =>
838 Election crypto v c ->
839 Ballot crypto v c -> Bool
840 verifyBallot (Election{..}::Election crypto v c) Ballot{..}
841 | Dict <- groupDict (Proxy @c) =
842 ballot_election_uuid == election_uuid &&
843 ballot_election_hash == election_hash &&
844 List.length election_questions == List.length ballot_answers &&
845 let (isValidSign, zkpSign) =
846 case ballot_signature of
847 Nothing -> (True, ZKP "")
848 Just Signature{..} ->
849 let zkp = ZKP (bytesNat signature_publicKey) in
850 (, zkp) $
851 proof_challenge signature_proof == hash
852 (signatureCommitments @_ @crypto zkp (commitQuicker signature_proof groupGen signature_publicKey))
853 (signatureStatement @_ @crypto ballot_answers)
854 in
855 and $ isValidSign :
856 List.zipWith (verifyAnswer election_public_key zkpSign)
857 election_questions ballot_answers
858
859 -- ** Type 'Signature'
860 -- | Schnorr-like signature.
861 --
862 -- Used by each voter to sign his/her encrypted 'Ballot'
863 -- using his/her 'Credential',
864 -- in order to avoid ballot stuffing.
865 data Signature crypto v c = Signature
866 { signature_publicKey :: !(PublicKey crypto c)
867 -- ^ Verification key.
868 , signature_proof :: !(Proof crypto v c)
869 } deriving (Generic)
870 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Signature crypto v c)
871 instance
872 ( Reifies c crypto
873 , Reifies v Version
874 , ToJSON (G crypto c)
875 ) => ToJSON (Signature crypto v c) where
876 toJSON (Signature pubKey Proof{..}) =
877 JSON.object
878 [ "public_key" .= pubKey
879 , "challenge" .= proof_challenge
880 , "response" .= proof_response
881 ]
882 toEncoding (Signature pubKey Proof{..}) =
883 JSON.pairs
884 ( "public_key" .= pubKey
885 <> "challenge" .= proof_challenge
886 <> "response" .= proof_response
887 )
888 instance
889 ( Reifies c crypto
890 , Reifies v Version
891 , Group crypto
892 , FromJSON (PublicKey crypto c)
893 ) => FromJSON (Signature crypto v c) where
894 parseJSON = JSON.withObject "Signature" $ \o -> do
895 signature_publicKey <- o .: "public_key"
896 proof_challenge <- o .: "challenge"
897 proof_response <- o .: "response"
898 let signature_proof = Proof{..}
899 return Signature{..}
900
901 -- *** Hashing
902
903 -- | @('signatureStatement' answers)@
904 -- returns the encrypted material to be signed:
905 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
906 signatureStatement :: Reifies c crypto => Foldable f => f (Answer crypto v c) -> [G crypto c]
907 signatureStatement =
908 foldMap $ \Answer{..} ->
909 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
910 [encryption_nonce, encryption_vault]
911
912 -- | @('signatureCommitments' voterZKP commitment)@
913 signatureCommitments ::
914 Reifies c crypto =>
915 ToNatural (G crypto c) =>
916 ZKP -> Commitment crypto c -> BS.ByteString
917 signatureCommitments (ZKP voterZKP) commitment =
918 "sig|"<>voterZKP<>"|" -- NOTE: this is actually part of the statement
919 <> bytesNat commitment<>"|"
920
921 -- ** Type 'ErrorBallot'
922 -- | Error raised by 'encryptBallot'.
923 data ErrorBallot
924 = ErrorBallot_WrongNumberOfAnswers Natural Natural
925 -- ^ When the number of answers
926 -- is different than the number of questions.
927 | ErrorBallot_Answer ErrorAnswer
928 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
929 | ErrorBallot_Wrong
930 -- ^ TODO: to be more precise.
931 deriving (Eq,Show,Generic,NFData)
932
933 -- * Type 'Version'
934 -- | Version of the Helios-C protocol.
935 data Version = Version
936 { version_branch :: [Natural]
937 , version_tags :: [(Text, Natural)]
938 } deriving (Eq,Ord,Generic,NFData)
939 instance IsString Version where
940 fromString = fromJust . readVersion
941 instance Show Version where
942 showsPrec _p Version{..} =
943 List.foldr (.) id
944 (List.intersperse (showChar '.') $
945 showsPrec 0 <$> version_branch) .
946 List.foldr (.) id
947 ((\(t,n) -> showChar '-' . showString (Text.unpack t) .
948 if n > 0 then showsPrec 0 n else id)
949 <$> version_tags)
950 instance ToJSON Version where
951 toJSON = toJSON . show
952 toEncoding = toEncoding . show
953 instance FromJSON Version where
954 parseJSON (JSON.String s)
955 | Just v <- readVersion (Text.unpack s)
956 = return v
957 parseJSON json = JSON.typeMismatch "Version" json
958
959 hasVersionTag :: Version -> Text -> Bool
960 hasVersionTag v tag = List.any (\(t,_n) -> t == tag) (version_tags v)
961
962 experimentalVersion :: Version
963 experimentalVersion = stableVersion
964 {version_tags = [(versionTagQuicker,0)]}
965
966 stableVersion :: Version
967 stableVersion = "1.6"
968
969 versionTagQuicker :: Text
970 versionTagQuicker = "quicker"
971
972 readVersion :: String -> Maybe Version
973 readVersion = parseReadP $ do
974 version_branch <- Read.sepBy1
975 (Read.read <$> Read.munch1 Char.isDigit)
976 (Read.char '.')
977 version_tags <- Read.many $ (,)
978 <$> (Text.pack <$ Read.char '-' <*> Read.munch1 Char.isAlpha)
979 <*> (Read.read <$> Read.munch1 Char.isDigit <|> return 0)
980 return Version{..}
981
982 parseReadP :: Read.ReadP a -> String -> Maybe a
983 parseReadP p s =
984 let p' = Read.readP_to_S p in
985 listToMaybe $ do
986 (x, "") <- p' s
987 return x