1 module Voting.Protocol.Cryptography where
3 import Control.Applicative (pure, (<*>))
4 import Control.Monad (bind)
5 import Data.Argonaut.Core as JSON
6 import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:))
7 import Data.Argonaut.Encode (class EncodeJson, encodeJson, (:=), (:=?), (~>), (~>?))
8 import Data.Argonaut.Parser as JSON
9 import Data.BigInt (BigInt)
10 import Data.BigInt as BigInt
11 import Data.Boolean (otherwise)
12 import Data.Bounded (class Bounded, top)
13 import Data.Either (Either(..))
14 import Data.Eq (class Eq, (==), (/=))
15 import Data.EuclideanRing (class EuclideanRing, (/), mod)
16 import Data.Foldable (intercalate)
17 import Data.Function (($), identity, (<<<), flip)
18 import Data.Functor (class Functor, (<$>))
19 import Data.HeytingAlgebra ((&&))
20 import Data.List (List, (:))
21 import Data.List.Lazy as LL
22 import Data.Maybe (Maybe(..), maybe, fromJust)
23 import Data.Monoid (class Monoid, mempty, (<>))
24 import Data.Newtype (class Newtype, wrap, unwrap)
25 import Data.Ord (class Ord, (>=))
26 import Data.Reflection (class Reifies, reflect)
27 import Data.Ring (class Ring, (-), negate)
28 import Data.Semiring (class Semiring, zero, (+), one, (*))
29 import Data.Show (class Show, show)
30 import Data.String.CodeUnits as String
31 import Data.Tuple (Tuple(..))
32 import Effect (Effect)
33 import Effect.Random (randomInt)
34 import Node.Crypto as Crypto
35 import Node.Crypto.Hash as Crypto
36 import Partial.Unsafe (unsafePartial)
37 import Type.Proxy (Proxy(..))
39 import Voting.Protocol.Arithmetic
40 import Voting.Protocol.Version
48 newtype Hash crypto c = Hash (E crypto c)
49 derive newtype instance eqHash :: Eq (Hash crypto c)
50 derive newtype instance ordHash :: Ord (Hash crypto c)
51 derive newtype instance showHash :: Show (Hash crypto c)
54 -- | @('hash' bs gs)@ returns as a number in 'G'
55 -- the 'Crypto.SHA256' hash of the given 'BS.ByteString' 'bs'
56 -- prefixing the decimal representation of given subgroup elements 'gs',
57 -- with a comma (",") intercalated between them.
59 -- NOTE: to avoid any collision when the 'hash' function is used in different contexts,
60 -- a message 'gs' is actually prefixed by a 'bs' indicating the context.
62 -- Used by 'proveEncryption' and 'verifyEncryption',
63 -- where the 'bs' usually contains the 'statement' to be proven,
64 -- and the 'gs' contains the 'commitments'.
68 CryptoParams crypto c =>
69 String -> List (G crypto c) ->
72 let s = bs <> intercalate "," (bytesNat <$> gs)
73 h <- Crypto.hex Crypto.SHA256 s
74 pure $ fromNatural $ Natural $
75 unsafePartial $ fromJust $
78 -- | `('bytesNat' x)` returns the serialization of `x`.
79 bytesNat :: forall n. ToNatural n => n -> String
80 bytesNat = show <<< nat
82 -- | `'randomBigInt' low high` returns a random 'BigInt' within `low` and `high`.
84 -- NOTE: adapted from GHC's 'randomIvalInteger'
85 randomBigInt :: BigInt -> BigInt -> Effect BigInt
93 b = BigInt.fromInt srcHigh - BigInt.fromInt srcLow + one
96 -- Probabilities of the most likely and least likely result
97 -- will differ at most by a factor of (1 +- 1/q).
98 -- Assuming the 'random' is uniform, of course.
99 -- On average, log q / log b more random values will be generated
101 q = BigInt.fromInt 1000
102 targetMagnitude = k * q
103 -- Generate random values until we exceed the target magnitude.
105 | mag >= targetMagnitude = pure acc
107 r <- randomInt srcLow srcHigh
109 (acc * b + (BigInt.fromInt r - BigInt.fromInt srcLow))
113 CryptoParams crypto c =>
115 randomE = E <<< Natural <$> randomBigInt zero (unwrap (nat (top::E crypto c)))
117 -- * Type 'Encryption'
118 -- | ElGamal-like encryption.
119 -- Its security relies on the /Discrete Logarithm problem/.
121 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
122 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
123 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
124 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
125 -- to enable the additive homomorphism.
127 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
128 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
129 data Encryption crypto v c = Encryption
130 { encryption_nonce :: G crypto c
131 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
132 -- equal to @('groupGen' '^'encNonce)@
133 , encryption_vault :: G crypto c
134 -- ^ Encrypted 'clear' text,
135 -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@
137 derive instance eqEncryption :: Eq (G crypto c) => Eq (Encryption crypto v c)
138 instance showEncryption :: Show (G crypto c) => Show (Encryption crypto v c) where
139 show (Encryption e) = show e
140 instance encodeJsonEncryption ::
142 , CryptoParams crypto c
143 ) => EncodeJson (Encryption crypto v c) where
144 encodeJson (Encryption{encryption_nonce, encryption_vault}) =
145 "alpha" := encryption_nonce ~>
146 "beta" := encryption_vault ~>
148 instance decodeJsonEncryption ::
150 , CryptoParams crypto c
151 ) => DecodeJson (Encryption crypto v c) where
153 obj <- decodeJson json
154 encryption_nonce <- obj .: "alpha"
155 encryption_vault <- obj .: "beta"
156 pure $ Encryption{encryption_nonce, encryption_vault}
158 -- | Additive homomorphism.
159 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
160 instance additiveEncryption ::
161 CryptoParams crypto c =>
162 Additive (Encryption crypto v c) where
163 gzero = Encryption{encryption_nonce:one, encryption_vault:one}
164 gadd (Encryption x) (Encryption y) = Encryption
165 { encryption_nonce: x.encryption_nonce * y.encryption_nonce
166 , encryption_vault: x.encryption_vault * y.encryption_vault
169 -- *** Type 'EncryptionNonce'
170 type EncryptionNonce = E
172 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
174 -- WARNING: the secret encryption nonce (@encNonce@)
175 -- is returned alongside the 'Encryption'
176 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
177 -- but this secret @encNonce@ MUST be forgotten after that,
178 -- as it may be used to decipher the 'Encryption'
179 -- without the 'SecretKey' associated with 'pubKey'.
183 CryptoParams crypto c =>
184 PublicKey crypto c -> E crypto c ->
185 Effect (Tuple (EncryptionNonce crypto c) (Encryption crypto v c))
186 encrypt pubKey clear = do
188 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
189 pure $ Tuple encNonce $
191 { encryption_nonce: groupGen^encNonce
192 , encryption_vault: pubKey ^encNonce * groupGen^clear
196 -- | Non-Interactive Zero-Knowledge 'Proof'
197 -- of knowledge of a discrete logarithm:
198 -- @(secret == logBase base (base^secret))@.
199 data Proof crypto v c = Proof
200 { proof_challenge :: Challenge crypto c
201 -- ^ 'Challenge' sent by the verifier to the prover
202 -- to ensure that the prover really has knowledge
203 -- of the secret and is not replaying.
204 -- Actually, 'proof_challenge' is not sent to the prover,
205 -- but derived from the prover's 'Commitment's and statements
206 -- with a collision resistant 'hash'.
207 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
208 , proof_response :: E crypto c
209 -- ^ A discrete logarithm sent by the prover to the verifier,
210 -- as a response to 'proof_challenge'.
212 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where:
214 -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@,
215 -- * @commitment '==' 'commit' proof base basePowSec '=='
216 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@,
217 -- * and @basePowSec '==' base'^'sec@,
219 -- then, with overwhelming probability (due to the 'hash' function),
220 -- the prover was not able to choose 'proof_challenge'
221 -- yet was able to compute a 'proof_response' such that
222 -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@),
223 -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@,
224 -- therefore the prover knows 'sec'.
226 -- The prover choses 'commitment' to be a random power of @base@,
227 -- to ensure that each 'prove' does not reveal any information
230 derive instance eqProof :: Eq (Proof crypto v c)
231 instance showProof :: Show (Proof crypto v c) where
232 show (Proof e) = show e
233 instance encodeJsonProof ::
235 , CryptoParams crypto c
236 ) => EncodeJson (Proof crypto v c) where
237 encodeJson (Proof{proof_challenge, proof_response}) =
238 "challenge" := proof_challenge ~>
239 "response" := proof_response ~>
241 instance decodeJsonProof ::
243 , CryptoParams crypto c
244 ) => DecodeJson (Proof crypto v c) where
246 obj <- decodeJson json
247 proof_challenge <- obj .: "challenge"
248 proof_response <- obj .: "response"
249 pure $ Proof{proof_challenge, proof_response}
252 -- | Zero-knowledge proof.
254 -- A protocol is /zero-knowledge/ if the verifier
255 -- learns nothing from the protocol except that the prover
258 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
259 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
260 newtype ZKP = ZKP String
262 -- ** Type 'Challenge'
266 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
267 -- by 'hash'ing them (eventually with other 'Commitment's).
269 -- Used in 'prove' it enables a Fiat-Shamir transformation
270 -- of an /interactive zero-knowledge/ (IZK) proof
271 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
272 -- That is to say that the verifier does not have
273 -- to send a 'Challenge' to the prover.
274 -- Indeed, the prover now handles the 'Challenge'
275 -- which becomes a (collision resistant) 'hash'
276 -- of the prover's commitments (and statements to be a stronger proof).
277 type Oracle list crypto c = list (Commitment crypto c) -> Challenge crypto c
279 -- | @('prove' sec commitmentBases oracle)@
280 -- returns a 'Proof' that @sec@ is known
281 -- (by proving the knowledge of its discrete logarithm).
283 -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases'
284 -- raised to the power of the secret nonce of the 'Proof',
285 -- as those are the 'Commitment's that the verifier will obtain
286 -- when composing the 'proof_challenge' and 'proof_response' together
289 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
290 -- the statement must be included in the 'hash' (along with the commitments).
292 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
293 -- does not reveal any information regarding the secret @sec@,
294 -- because two 'Proof's using the same 'Commitment'
295 -- can be used to deduce @sec@ (using the special-soundness).
297 forall crypto v c list.
299 CryptoParams crypto c =>
303 Oracle list crypto c ->
304 Effect (Proof crypto v c)
305 prove sec commitmentBases oracle = do
307 let commitments = (_ ^ nonce) <$> commitmentBases
308 let proof_challenge = oracle commitments
311 , proof_response: nonce `op` (sec*proof_challenge)
314 -- | See comments in 'commit'.
316 if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker
320 -- | Like 'prove' but quicker. It chould replace 'prove' entirely
321 -- when Helios-C specifications will be fixed.
323 forall crypto v c list.
325 CryptoParams crypto c =>
329 Oracle list crypto c ->
330 Effect (Proof crypto v c)
331 proveQuicker sec commitmentBases oracle = do
333 let commitments = (_ ^ nonce) <$> commitmentBases
334 let proof_challenge = oracle commitments
337 , proof_response: nonce - sec*proof_challenge
340 -- | @('fakeProof')@ returns a 'Proof'
341 -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random,
342 -- instead of @('proof_challenge' '==' 'hash' statement commitments)@
343 -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@
344 -- as a 'Proof' returned by 'prove'.
346 -- Used in 'proveEncryption' to fill the returned 'DisjProof'
347 -- with fake 'Proof's for all 'Disjunction's but the encrypted one.
350 CryptoParams crypto c =>
351 Effect (Proof crypto v c)
353 proof_challenge <- randomE
354 proof_response <- randomE
355 pure $ Proof{proof_challenge, proof_response}
357 -- ** Type 'Commitment'
358 -- | A commitment from the prover to the verifier.
359 -- It's a power of 'groupGen' chosen randomly by the prover
360 -- when making a 'Proof' with 'prove'.
363 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
364 -- from the given 'Proof' with the knowledge of the verifier.
368 CryptoParams crypto c =>
373 commit (Proof p) base basePowSec =
374 (base^p.proof_response) `op`
375 (basePowSec^p.proof_challenge)
378 if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker
381 -- TODO: contrary to some textbook presentations,
382 -- @('*')@ should be used instead of @('/')@ to avoid the performance cost
383 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
384 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
386 -- | Like 'commit' but quicker. It chould replace 'commit' entirely
387 -- when Helios-C specifications will be fixed.
390 CryptoParams crypto c =>
395 commitQuicker (Proof p) base basePowSec =
396 base^p.proof_response *
397 basePowSec^p.proof_challenge
399 -- * Type 'Disjunction'
400 -- | A 'Disjunction' is an 'inverse'd @('groupGen' '^'opinion)@
401 -- it's used in 'proveEncryption' to generate a 'Proof'
402 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
405 booleanDisjunctions :: forall crypto c. CryptoParams crypto c => LL.List (Disjunction crypto c)
406 booleanDisjunctions = LL.take 2 $ groupGenInverses::LL.List (G crypto c)