1 module Voting.Protocol.Cryptography where
3 import Control.Applicative (pure, (<*>))
4 import Control.Monad (bind, join)
5 import Control.Monad.Trans.Class (lift)
6 import Control.Monad.Except.Trans (ExceptT)
7 import Data.Argonaut.Core as JSON
8 import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:))
9 import Data.Argonaut.Encode (class EncodeJson, encodeJson, (:=), (:=?), (~>), (~>?))
10 import Data.Argonaut.Parser as JSON
11 import Data.BigInt (BigInt)
12 import Data.BigInt as BigInt
13 import Data.Boolean (otherwise)
14 import Data.Bounded (class Bounded, top)
15 import Data.Either (Either(..))
16 import Data.Eq (class Eq, (==), (/=))
17 import Data.EuclideanRing (class EuclideanRing, (/), mod)
18 import Data.Foldable (intercalate)
19 import Data.Function (($), identity, (<<<), flip)
20 import Data.Functor (class Functor, (<$>))
21 import Data.HeytingAlgebra ((&&))
22 import Data.Int as Int
23 import Data.List (List, (:))
24 import Data.List as List
25 import Data.List.Lazy as LL
26 import Data.Maybe (Maybe(..), maybe, fromJust)
27 import Data.Monoid (class Monoid, mempty, (<>))
28 import Data.Newtype (class Newtype, wrap, unwrap)
29 import Data.Ord (class Ord, (>=))
30 import Data.Reflection (class Reifies, reflect)
31 import Data.Ring (class Ring, (-), negate)
32 import Data.Semiring (class Semiring, zero, (+), one, (*))
33 import Data.Show (class Show, show)
34 import Data.String.CodeUnits as String
35 import Data.Traversable (sum)
36 import Data.Tuple (Tuple(..))
37 import Data.Unfoldable (replicateA)
38 import Effect (Effect)
39 import Effect.Random (randomInt)
40 import Node.Crypto as Crypto
41 import Node.Crypto.Hash as Crypto
42 import Partial.Unsafe (unsafePartial)
43 import Type.Proxy (Proxy(..))
45 import Voting.Protocol.Arithmetic
46 import Voting.Protocol.Version
54 newtype Hash crypto c = Hash (E crypto c)
55 derive newtype instance eqHash :: Eq (Hash crypto c)
56 derive newtype instance ordHash :: Ord (Hash crypto c)
57 derive newtype instance showHash :: Show (Hash crypto c)
60 -- | @('hash' bs gs)@ returns as a number in 'G'
61 -- the 'Crypto.SHA256' hash of the given 'BS.ByteString' 'bs'
62 -- prefixing the decimal representation of given subgroup elements 'gs',
63 -- with a comma (",") intercalated between them.
65 -- NOTE: to avoid any collision when the 'hash' function is used in different contexts,
66 -- a message 'gs' is actually prefixed by a 'bs' indicating the context.
68 -- Used by 'proveEncryption' and 'verifyEncryption',
69 -- where the 'bs' usually contains the 'statement' to be proven,
70 -- and the 'gs' contains the 'commitments'.
74 CryptoParams crypto c =>
75 String -> List (G crypto c) ->
78 let s = bs <> intercalate "," (bytesNat <$> gs)
79 h <- Crypto.hex Crypto.SHA256 s
80 pure $ fromNatural $ Natural $
81 unsafePartial $ fromJust $
84 -- | `('bytesNat' x)` returns the serialization of `x`.
85 bytesNat :: forall n. ToNatural n => n -> String
86 bytesNat = show <<< nat
88 -- | `'randomBigInt' low high` returns a random 'BigInt' within `low` and `high`.
90 -- NOTE: adapted from GHC's 'randomIvalInteger'
91 randomBigInt :: BigInt -> BigInt -> Effect BigInt
99 b = BigInt.fromInt srcHigh - BigInt.fromInt srcLow + one
102 -- Probabilities of the most likely and least likely result
103 -- will differ at most by a factor of (1 +- 1/q).
104 -- Assuming the 'random' is uniform, of course.
105 -- On average, log q / log b more random values will be generated
107 q = BigInt.fromInt 1000
108 targetMagnitude = k * q
109 -- Generate random values until we exceed the target magnitude.
111 | mag >= targetMagnitude = pure acc
113 r <- randomInt srcLow srcHigh
115 (acc * b + (BigInt.fromInt r - BigInt.fromInt srcLow))
119 CryptoParams crypto c =>
121 randomE = E <<< Natural <$> randomBigInt zero (unwrap (nat (top::E crypto c)))
123 -- * Type 'Encryption'
124 -- | ElGamal-like encryption.
125 -- Its security relies on the /Discrete Logarithm problem/.
127 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
128 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
129 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
130 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
131 -- to enable the additive homomorphism.
133 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
134 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
135 data Encryption crypto v c = Encryption
136 { encryption_nonce :: G crypto c
137 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
138 -- equal to @('groupGen' '^'encNonce)@
139 , encryption_vault :: G crypto c
140 -- ^ Encrypted 'clear' text,
141 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
143 derive instance eqEncryption :: Eq (G crypto c) => Eq (Encryption crypto v c)
144 instance showEncryption :: Show (G crypto c) => Show (Encryption crypto v c) where
145 show (Encryption e) = show e
146 instance encodeJsonEncryption ::
148 , CryptoParams crypto c
149 ) => EncodeJson (Encryption crypto v c) where
150 encodeJson (Encryption{encryption_nonce, encryption_vault}) =
151 "alpha" := encryption_nonce ~>
152 "beta" := encryption_vault ~>
154 instance decodeJsonEncryption ::
156 , CryptoParams crypto c
157 ) => DecodeJson (Encryption crypto v c) where
159 obj <- decodeJson json
160 encryption_nonce <- obj .: "alpha"
161 encryption_vault <- obj .: "beta"
162 pure $ Encryption{encryption_nonce, encryption_vault}
164 -- | Additive homomorphism.
165 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
166 instance additiveEncryption ::
167 CryptoParams crypto c =>
168 Additive (Encryption crypto v c) where
169 gzero = Encryption{encryption_nonce:one, encryption_vault:one}
170 gadd (Encryption x) (Encryption y) = Encryption
171 { encryption_nonce: x.encryption_nonce * y.encryption_nonce
172 , encryption_vault: x.encryption_vault * y.encryption_vault
175 -- *** Type 'EncryptionNonce'
176 type EncryptionNonce = E
178 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
180 -- WARNING: the secret encryption nonce (@encNonce@)
181 -- is returned alongside the 'Encryption'
182 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
183 -- but this secret @encNonce@ MUST be forgotten after that,
184 -- as it may be used to decipher the 'Encryption'
185 -- without the 'SecretKey' associated with 'pubKey'.
189 CryptoParams crypto c =>
190 PublicKey crypto c -> E crypto c ->
191 Effect (Tuple (EncryptionNonce crypto c) (Encryption crypto v c))
192 encrypt pubKey clear = do
194 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
195 pure $ Tuple encNonce $
197 { encryption_nonce: groupGen^encNonce
198 , encryption_vault: pubKey ^encNonce * groupGen^clear
202 -- | Non-Interactive Zero-Knowledge 'Proof'
203 -- of knowledge of a discrete logarithm:
204 -- @(secret == logBase base (base^secret))@.
205 data Proof crypto v c = Proof
206 { proof_challenge :: Challenge crypto c
207 -- ^ 'Challenge' sent by the verifier to the prover
208 -- to ensure that the prover really has knowledge
209 -- of the secret and is not replaying.
210 -- Actually, 'proof_challenge' is not sent to the prover,
211 -- but derived from the prover's 'Commitment's and statements
212 -- with a collision resistant 'hash'.
213 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
214 , proof_response :: E crypto c
215 -- ^ A discrete logarithm sent by the prover to the verifier,
216 -- as a response to 'proof_challenge'.
218 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
220 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
221 -- * @commitment '==' 'commit' proof base basePowSec '=='
222 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
223 -- * and @basePowSec '==' base'^'sec@,
225 -- then, with overwhelming probability (due to the 'hash' function),
226 -- the prover was not able to choose 'proof_challenge'
227 -- yet was able to compute a 'proof_response' such that
228 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
229 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
230 -- therefore the prover knows 'sec'.
232 -- The prover choses 'commitment' to be a random power of @base@,
233 -- to ensure that each 'prove' does not reveal any information
236 derive instance eqProof :: Eq (Proof crypto v c)
237 instance showProof :: Show (Proof crypto v c) where
238 show (Proof e) = show e
239 instance encodeJsonProof ::
241 , CryptoParams crypto c
242 ) => EncodeJson (Proof crypto v c) where
243 encodeJson (Proof{proof_challenge, proof_response}) =
244 "challenge" := proof_challenge ~>
245 "response" := proof_response ~>
247 instance decodeJsonProof ::
249 , CryptoParams crypto c
250 ) => DecodeJson (Proof crypto v c) where
252 obj <- decodeJson json
253 proof_challenge <- obj .: "challenge"
254 proof_response <- obj .: "response"
255 pure $ Proof{proof_challenge, proof_response}
258 -- | Zero-knowledge proof.
260 -- A protocol is /zero-knowledge/ if the verifier
261 -- learns nothing from the protocol except that the prover
264 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
265 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
266 newtype ZKP = ZKP String
268 -- ** Type 'Challenge'
272 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
273 -- by 'hash'ing them (eventually with other 'Commitment's).
275 -- Used in 'prove' it enables a Fiat-Shamir transformation
276 -- of an /interactive zero-knowledge/ (IZK) proof
277 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
278 -- That is to say that the verifier does not have
279 -- to send a 'Challenge' to the prover.
280 -- Indeed, the prover now handles the 'Challenge'
281 -- which becomes a (collision resistant) 'hash'
282 -- of the prover's commitments (and statements to be a stronger proof).
284 -- NOTE: the returned 'Challenge' is within 'Effect' because in PureScript
285 -- 'hash'ing needs this (due to the use of Node.Buffer).
286 type Oracle list crypto c = list (Commitment crypto c) -> Effect (Challenge crypto c)
288 -- | @('prove' sec commitmentBases oracle)@
289 -- returns a 'Proof' that @sec@ is known
290 -- (by proving the knowledge of its discrete logarithm).
292 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
293 -- raised to the power of the secret nonce of the 'Proof',
294 -- as those are the 'Commitment's that the verifier will obtain
295 -- when composing the 'proof_challenge' and 'proof_response' together
298 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
299 -- the statement must be included in the 'hash' (along with the commitments).
301 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
302 -- does not reveal any information regarding the secret @sec@,
303 -- because two 'Proof's using the same 'Commitment'
304 -- can be used to deduce @sec@ (using the special-soundness).
306 forall crypto v c list.
308 CryptoParams crypto c =>
312 Oracle list crypto c ->
313 Effect (Proof crypto v c)
314 prove sec commitmentBases oracle = do
316 let commitments = (_ ^ nonce) <$> commitmentBases
317 proof_challenge <- oracle commitments
320 , proof_response: nonce `op` (sec*proof_challenge)
323 -- | See comments in 'commit'.
325 if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker
329 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
330 -- when Helios-C specifications will be fixed.
332 forall crypto v c list.
334 CryptoParams crypto c =>
338 Oracle list crypto c ->
339 Effect (Proof crypto v c)
340 proveQuicker sec commitmentBases oracle = do
342 let commitments = (_ ^ nonce) <$> commitmentBases
343 proof_challenge <- oracle commitments
346 , proof_response: nonce - sec*proof_challenge
349 -- | @('fakeProof')@ returns a 'Proof'
350 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
351 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
352 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
353 -- as a 'Proof' returned by 'prove'.
355 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
356 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
359 CryptoParams crypto c =>
360 Effect (Proof crypto v c)
362 proof_challenge <- randomE
363 proof_response <- randomE
364 pure $ Proof{proof_challenge, proof_response}
366 -- ** Type 'Commitment'
367 -- | A commitment from the prover to the verifier.
368 -- It's a power of 'groupGen' chosen randomly by the prover
369 -- when making a 'Proof' with 'prove'.
372 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
373 -- from the given 'Proof' with the knowledge of the verifier.
377 CryptoParams crypto c =>
382 commit (Proof p) base basePowSec =
383 (base^p.proof_response) `op`
384 (basePowSec^p.proof_challenge)
387 if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker
390 -- TODO: contrary to some textbook presentations,
391 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
392 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
393 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
395 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
396 -- when Helios-C specifications will be fixed.
399 CryptoParams crypto c =>
404 commitQuicker (Proof p) base basePowSec =
405 base^p.proof_response *
406 basePowSec^p.proof_challenge
408 -- * Type 'Disjunction'
409 -- | A 'Disjunction' is an 'inverse'd @('groupGen' '^'opinion)@
410 -- it's used in 'proveEncryption' to generate a 'Proof'
411 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
414 booleanDisjunctions :: forall crypto c. CryptoParams crypto c => LL.List (Disjunction crypto c)
415 booleanDisjunctions = LL.take 2 $ groupGenInverses::LL.List (G crypto c)
417 intervalDisjunctions ::
419 CryptoParams crypto c =>
420 Natural -> Natural -> LL.List (Disjunction crypto c)
421 intervalDisjunctions mini maxi =
422 LL.take (int $ (unwrap (nat maxi) + one)-unwrap (nat mini)) $
423 LL.drop (int $ unwrap (nat mini)) $
424 groupGenInverses :: LL.List (G crypto c)
426 int = Int.round <<< BigInt.toNumber
428 -- ** Type 'DisjProof'
429 -- | A list of 'Proof's to prove that the opinion within an 'Encryption'
430 -- is indexing a 'Disjunction' within a list of them,
431 -- without revealing which opinion it is.
432 newtype DisjProof crypto v c = DisjProof (List (Proof crypto v c))
433 derive newtype instance eqDisjProof :: Eq (DisjProof crypto v c)
434 derive newtype instance showDisjProof :: Show (DisjProof crypto v c)
435 derive newtype instance encodeJsonDisjProof :: (Reifies v Version, CryptoParams crypto c) => EncodeJson (DisjProof crypto v c)
436 derive newtype instance decodeJsonDisjProof :: (Reifies v Version, CryptoParams crypto c) => DecodeJson (DisjProof crypto v c)
437 instance newtypeDisjProof :: Newtype (DisjProof crypto v c) (List (Proof crypto v c)) where
439 unwrap (DisjProof x) = x
441 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
442 -- returns a 'DisjProof' that 'enc' 'encrypt's
443 -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'.
445 -- The prover proves that it knows an 'encNonce', such that:
446 -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@
448 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
450 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
454 CryptoParams crypto c =>
455 PublicKey crypto c -> ZKP ->
456 Tuple (List (Disjunction crypto c)) (List (Disjunction crypto c)) ->
457 Tuple (EncryptionNonce crypto c) (Encryption crypto v c) ->
458 Effect (DisjProof crypto v c)
459 proveEncryption elecPubKey voterZKP (Tuple prevDisjs nextDisjs) (Tuple encNonce enc) = do
460 -- Fake proofs for all 'Disjunction's except the genuine one.
461 prevFakeProofs <- replicateA (List.length prevDisjs) fakeProof
462 nextFakeProofs <- replicateA (List.length nextDisjs) fakeProof
463 let fakeChallengeSum = sum ((\(Proof p) -> p.proof_challenge) <$> prevFakeProofs) +
464 sum ((\(Proof p) -> p.proof_challenge) <$> nextFakeProofs)
465 let statement = encryptionStatement voterZKP enc
466 genuineProof <- prove encNonce (groupGen : elecPubKey : List.Nil) $ \genuineCommitments -> do
467 let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc)
468 let prevCommitments = validCommitments prevDisjs prevFakeProofs
469 let nextCommitments = validCommitments nextDisjs nextFakeProofs
470 let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments
471 challenge <- hash statement commitments
472 let genuineChallenge = challenge - fakeChallengeSum
473 pure genuineChallenge
474 -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum)
475 -- thus (sum ((\(Proof p) -> p.proof_challenge) <$> proofs) == challenge)
476 -- as checked in 'verifyEncryption'.
477 let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs)
478 pure (DisjProof proofs)
484 CryptoParams crypto c =>
485 PublicKey crypto c -> ZKP ->
486 List (Disjunction crypto c) -> Tuple (Encryption crypto v c) (DisjProof crypto v c) ->
487 ExceptT ErrorVerifyEncryption Effect Boolean
488 verifyEncryption elecPubKey voterZKP disjs (Tuple enc (DisjProof proofs)) =
489 case isoZipWith (encryptionCommitments elecPubKEy enc) disjs proofs of
491 throwE $ ErrorVerifyEncryption_InvalidProofLength
492 (fromIntegral $ List.length proofs)
493 (fromIntegral $ List.length disjs)
494 Just commitments -> do
495 h <- lift $ hash (encryptionStatement voterZKP enc) (join commitments)
496 pure (challengeSum == h)
498 challengeSum = sum ((\(Proof p) -> p.proof_challenge) <$> proofs)
502 encryptionStatement ::
504 CryptoParams crypto c =>
505 ZKP -> Encryption crypto v c -> String
506 encryptionStatement (ZKP voterZKP) (Encryption enc) =
507 "prove|"<>voterZKP<>"|"
508 <> bytesNat enc.encryption_nonce<>","
509 <> bytesNat enc.encryption_vault<>"|"
511 -- | @('encryptionCommitments' elecPubKey enc disj proof)@
512 -- returns the 'Commitment's with only the knowledge of the verifier.
514 -- For the prover the 'Proof' comes from @fakeProof@,
515 -- and for the verifier the 'Proof' comes from the prover.
516 encryptionCommitments ::
519 CryptoParams crypto c =>
520 PublicKey crypto c -> Encryption crypto v c ->
521 Disjunction crypto c -> Proof crypto v c -> List (G crypto c)
522 encryptionCommitments elecPubKey (Encryption enc) disj proof =
523 commit proof groupGen enc.encryption_nonce :
524 -- == groupGen ^ nonce if 'Proof' comes from 'prove'.
525 -- base==groupGen, basePowSec==groupGen^encNonce.
526 commit proof elecPubKey (enc.encryption_vault*disj) :
527 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
528 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
529 -- base==elecPubKey, basePowSec==elecPubKey^encNonce.
532 -- ** Type 'ErrorVerifyEncryption'
533 -- | Error raised by 'verifyEncryption'.
534 data ErrorVerifyEncryption
535 = ErrorVerifyEncryption_InvalidProofLength Natural Natural
536 -- ^ When the number of proofs is different than
537 -- the number of 'Disjunction's.
538 -- deriving (Eq,Show)
541 -- * Type 'Signature'
542 -- | Schnorr-like signature.
544 -- Used by each voter to sign his/her encrypted 'Ballot'
545 -- using his/her 'Credential',
546 -- in order to avoid ballot stuffing.
547 data Signature crypto v c = Signature
548 { signature_publicKey :: !(PublicKey crypto c)
549 -- ^ Verification key.
550 , signature_proof :: !(Proof crypto v c)
552 deriving instance (NFData crypto, NFData (G crypto c)) => NFData (Signature crypto v c)
555 , CryptoParams crypto c
556 ) => ToJSON (Signature crypto v c) where
557 toJSON (Signature pubKey Proof{..}) =
559 [ "public_key" .= pubKey
560 , "challenge" .= proof_challenge
561 , "response" .= proof_response
563 toEncoding (Signature pubKey Proof{..}) =
565 ( "public_key" .= pubKey
566 <> "challenge" .= proof_challenge
567 <> "response" .= proof_response
571 , CryptoParams crypto c
572 ) => FromJSON (Signature crypto v c) where
573 parseJSON = JSON.withObject "Signature" $ \o -> do
574 signature_publicKey <- o .: "public_key"
575 proof_challenge <- o .: "challenge"
576 proof_response <- o .: "response"
577 let signature_proof = Proof{..}