module Voting.Protocol.Cryptography where import Control.Applicative (pure, (<*>)) import Control.Monad (bind, join) import Control.Monad.Trans.Class (lift) import Control.Monad.Except.Trans (ExceptT, throwError) import Data.Argonaut.Core as JSON import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:)) import Data.Argonaut.Encode (class EncodeJson, encodeJson, (:=), (:=?), (~>), (~>?)) import Data.Argonaut.Parser as JSON import Data.BigInt (BigInt) import Data.BigInt as BigInt import Data.Boolean (otherwise) import Data.Bounded (class Bounded, top) import Data.Either (Either(..)) import Data.Eq (class Eq, (==), (/=)) import Data.EuclideanRing (class EuclideanRing, (/), mod) import Data.Foldable (intercalate) import Data.Function (($), identity, (<<<), flip) import Data.Functor (class Functor, (<$>)) import Data.HeytingAlgebra ((&&)) import Data.Int as Int import Data.List (List, (:)) import Data.List as List import Data.List.Lazy as LL import Data.Maybe (Maybe(..), maybe, fromJust) import Data.Monoid (class Monoid, mempty, (<>)) import Data.Newtype (class Newtype, wrap, unwrap) import Data.Ord (class Ord, (>=)) import Data.Reflection (class Reifies, reflect) import Data.Ring (class Ring, (-), negate) import Data.Semiring (class Semiring, zero, (+), one, (*)) import Data.Show (class Show, show) import Data.String.CodeUnits as String import Data.Traversable (sum) import Data.Tuple (Tuple(..)) import Data.Unfoldable (replicateA) import Effect (Effect) import Effect.Random (randomInt) import Node.Crypto as Crypto import Node.Crypto.Hash as Crypto import Partial.Unsafe (unsafePartial) import Type.Proxy (Proxy(..)) import Voting.Protocol.Utils import Voting.Protocol.Arithmetic import Voting.Protocol.Version -- * Type 'PublicKey' type PublicKey = G -- * Type 'SecretKey' type SecretKey = E -- * Type 'Hash' newtype Hash crypto c = Hash (E crypto c) derive newtype instance eqHash :: Eq (Hash crypto c) derive newtype instance ordHash :: Ord (Hash crypto c) derive newtype instance showHash :: Show (Hash crypto c) {- -- | @('hash' bs gs)@ returns as a number in 'G' -- the 'Crypto.SHA256' hash of the given 'BS.ByteString' 'bs' -- prefixing the decimal representation of given subgroup elements 'gs', -- with a comma (",") intercalated between them. -- -- NOTE: to avoid any collision when the 'hash' function is used in different contexts, -- a message 'gs' is actually prefixed by a 'bs' indicating the context. -- -- Used by 'proveEncryption' and 'verifyEncryption', -- where the 'bs' usually contains the 'statement' to be proven, -- and the 'gs' contains the 'commitments'. -} hash :: forall crypto c. CryptoParams crypto c => String -> List (G crypto c) -> Effect (E crypto c) hash bs gs = do let s = bs <> intercalate "," (bytesNat <$> gs) h <- Crypto.hex Crypto.SHA256 s pure $ fromNatural $ Natural $ unsafePartial $ fromJust $ BigInt.fromBase 16 h -- | `('bytesNat' x)` returns the serialization of `x`. bytesNat :: forall n. ToNatural n => n -> String bytesNat = show <<< nat -- | `'randomBigInt' low high` returns a random 'BigInt' within `low` and `high`. -- -- NOTE: adapted from GHC's 'randomIvalInteger' randomBigInt :: BigInt -> BigInt -> Effect BigInt randomBigInt l h = do v <- f one zero pure (l + v `mod` k) where srcLow = one :: Int srcHigh = top :: Int -- | source interval b = BigInt.fromInt srcHigh - BigInt.fromInt srcLow + one -- | target interval k = h - l + one -- Probabilities of the most likely and least likely result -- will differ at most by a factor of (1 +- 1/q). -- Assuming the 'random' is uniform, of course. -- On average, log q / log b more random values will be generated -- than the minimum. q = BigInt.fromInt 1000 targetMagnitude = k * q -- Generate random values until we exceed the target magnitude. f mag acc | mag >= targetMagnitude = pure acc | otherwise = do r <- randomInt srcLow srcHigh f (mag * b) (acc * b + (BigInt.fromInt r - BigInt.fromInt srcLow)) randomE :: forall crypto c. CryptoParams crypto c => Effect (E crypto c) randomE = E <<< Natural <$> randomBigInt zero (unwrap (nat (top::E crypto c))) -- * Type 'Encryption' -- | ElGamal-like encryption. -- Its security relies on the /Discrete Logarithm problem/. -- -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce), -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@ -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable, -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like") -- to enable the additive homomorphism. -- -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@, -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@. data Encryption crypto v c = Encryption { encryption_nonce :: G crypto c -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text, -- equal to @('groupGen' '^'encNonce)@ , encryption_vault :: G crypto c -- ^ Encrypted 'clear' text, -- equal to @('pubKey' '^'encNone '*' 'groupGen' '^'clear)@ } derive instance eqEncryption :: Eq (G crypto c) => Eq (Encryption crypto v c) instance showEncryption :: Show (G crypto c) => Show (Encryption crypto v c) where show (Encryption e) = show e instance encodeJsonEncryption :: ( Reifies v Version , CryptoParams crypto c ) => EncodeJson (Encryption crypto v c) where encodeJson (Encryption{encryption_nonce, encryption_vault}) = "alpha" := encryption_nonce ~> "beta" := encryption_vault ~> JSON.jsonEmptyObject instance decodeJsonEncryption :: ( Reifies v Version , CryptoParams crypto c ) => DecodeJson (Encryption crypto v c) where decodeJson json = do obj <- decodeJson json encryption_nonce <- obj .: "alpha" encryption_vault <- obj .: "beta" pure $ Encryption{encryption_nonce, encryption_vault} -- | Additive homomorphism. -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@. instance additiveEncryption :: CryptoParams crypto c => Additive (Encryption crypto v c) where gzero = Encryption{encryption_nonce:one, encryption_vault:one} gadd (Encryption x) (Encryption y) = Encryption { encryption_nonce: x.encryption_nonce * y.encryption_nonce , encryption_vault: x.encryption_vault * y.encryption_vault } -- *** Type 'EncryptionNonce' type EncryptionNonce = E -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'. -- -- WARNING: the secret encryption nonce (@encNonce@) -- is returned alongside the 'Encryption' -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption', -- but this secret @encNonce@ MUST be forgotten after that, -- as it may be used to decipher the 'Encryption' -- without the 'SecretKey' associated with 'pubKey'. encrypt :: forall crypto v c. Reifies v Version => CryptoParams crypto c => PublicKey crypto c -> E crypto c -> Effect (Tuple (EncryptionNonce crypto c) (Encryption crypto v c)) encrypt pubKey clear = do encNonce <- randomE -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'. pure $ Tuple encNonce $ Encryption { encryption_nonce: groupGen^encNonce , encryption_vault: pubKey ^encNonce * groupGen^clear } -- * Type 'Proof' -- | Non-Interactive Zero-Knowledge 'Proof' -- of knowledge of a discrete logarithm: -- @(secret == logBase base (base^secret))@. data Proof crypto v c = Proof { proof_challenge :: Challenge crypto c -- ^ 'Challenge' sent by the verifier to the prover -- to ensure that the prover really has knowledge -- of the secret and is not replaying. -- Actually, 'proof_challenge' is not sent to the prover, -- but derived from the prover's 'Commitment's and statements -- with a collision resistant 'hash'. -- Hence the prover cannot chose the 'proof_challenge' to his/her liking. , proof_response :: E crypto c -- ^ A discrete logarithm sent by the prover to the verifier, -- as a response to 'proof_challenge'. -- -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@, where: -- -- * @statement@ is a serialization of a tag, @base@ and @basePowSec@, -- * @commitment '==' 'commit' proof base basePowSec '==' -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@, -- * and @basePowSec '==' base'^'sec@, -- -- then, with overwhelming probability (due to the 'hash' function), -- the prover was not able to choose 'proof_challenge' -- yet was able to compute a 'proof_response' such that -- (@commitment '==' base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge'@), -- that is to say: @('proof_response' '==' logBase base 'commitment' '-' sec '*' 'proof_challenge')@, -- therefore the prover knows 'sec'. -- -- The prover choses 'commitment' to be a random power of @base@, -- to ensure that each 'prove' does not reveal any information -- about its secret. } derive instance eqProof :: Eq (Proof crypto v c) instance showProof :: Show (Proof crypto v c) where show (Proof e) = show e instance encodeJsonProof :: ( Reifies v Version , CryptoParams crypto c ) => EncodeJson (Proof crypto v c) where encodeJson (Proof{proof_challenge, proof_response}) = "challenge" := proof_challenge ~> "response" := proof_response ~> JSON.jsonEmptyObject instance decodeJsonProof :: ( Reifies v Version , CryptoParams crypto c ) => DecodeJson (Proof crypto v c) where decodeJson json = do obj <- decodeJson json proof_challenge <- obj .: "challenge" proof_response <- obj .: "response" pure $ Proof{proof_challenge, proof_response} -- ** Type 'ZKP' -- | Zero-knowledge proof. -- -- A protocol is /zero-knowledge/ if the verifier -- learns nothing from the protocol except that the prover -- knows the secret. -- -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical: -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993. newtype ZKP = ZKP String -- ** Type 'Challenge' type Challenge = E -- ** Type 'Oracle' -- An 'Oracle' returns the 'Challenge' of the 'Commitment's -- by 'hash'ing them (eventually with other 'Commitment's). -- -- Used in 'prove' it enables a Fiat-Shamir transformation -- of an /interactive zero-knowledge/ (IZK) proof -- into a /non-interactive zero-knowledge/ (NIZK) proof. -- That is to say that the verifier does not have -- to send a 'Challenge' to the prover. -- Indeed, the prover now handles the 'Challenge' -- which becomes a (collision resistant) 'hash' -- of the prover's commitments (and statements to be a stronger proof). -- -- NOTE: the returned 'Challenge' is within 'Effect' because in PureScript -- 'hash'ing needs this (due to the use of Node.Buffer). type Oracle list crypto c = list (Commitment crypto c) -> Effect (Challenge crypto c) -- | @('prove' sec commitmentBases oracle)@ -- returns a 'Proof' that @sec@ is known -- (by proving the knowledge of its discrete logarithm). -- -- The 'Oracle' is given 'Commitment's equal to the 'commitmentBases' -- raised to the power of the secret nonce of the 'Proof', -- as those are the 'Commitment's that the verifier will obtain -- when composing the 'proof_challenge' and 'proof_response' together -- (with 'commit'). -- -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak): -- the statement must be included in the 'hash' (along with the commitments). -- -- NOTE: a 'random' @nonce@ is used to ensure each 'prove' -- does not reveal any information regarding the secret @sec@, -- because two 'Proof's using the same 'Commitment' -- can be used to deduce @sec@ (using the special-soundness). prove :: forall crypto v c list. Reifies v Version => CryptoParams crypto c => Functor list => E crypto c -> list (G crypto c) -> Oracle list crypto c -> Effect (Proof crypto v c) prove sec commitmentBases oracle = do nonce <- randomE let commitments = (_ ^ nonce) <$> commitmentBases proof_challenge <- oracle commitments pure $ Proof { proof_challenge , proof_response: nonce `op` (sec*proof_challenge) } where -- | See comments in 'commit'. op = if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker then (-) else (+) -- | Like 'prove' but quicker. It chould replace 'prove' entirely -- when Helios-C specifications will be fixed. proveQuicker :: forall crypto v c list. Reifies v Version => CryptoParams crypto c => Functor list => E crypto c -> list (G crypto c) -> Oracle list crypto c -> Effect (Proof crypto v c) proveQuicker sec commitmentBases oracle = do nonce <- randomE let commitments = (_ ^ nonce) <$> commitmentBases proof_challenge <- oracle commitments pure $ Proof { proof_challenge , proof_response: nonce - sec*proof_challenge } -- | @('fakeProof')@ returns a 'Proof' -- whose 'proof_challenge' and 'proof_response' are uniformly chosen at random, -- instead of @('proof_challenge' '==' 'hash' statement commitments)@ -- and @('proof_response' '==' nonce '+' sec '*' 'proof_challenge')@ -- as a 'Proof' returned by 'prove'. -- -- Used in 'proveEncryption' to fill the returned 'DisjProof' -- with fake 'Proof's for all 'Disjunction's but the encrypted one. fakeProof :: forall crypto v c. CryptoParams crypto c => Effect (Proof crypto v c) fakeProof = do proof_challenge <- randomE proof_response <- randomE pure $ Proof{proof_challenge, proof_response} -- ** Type 'Commitment' -- | A commitment from the prover to the verifier. -- It's a power of 'groupGen' chosen randomly by the prover -- when making a 'Proof' with 'prove'. type Commitment = G -- | @('commit' proof base basePowSec)@ returns a 'Commitment' -- from the given 'Proof' with the knowledge of the verifier. commit :: forall crypto v c. Reifies v Version => CryptoParams crypto c => Proof crypto v c -> G crypto c -> G crypto c -> Commitment crypto c commit (Proof p) base basePowSec = (base^p.proof_response) `op` (basePowSec^p.proof_challenge) where op = if reflect (Proxy::Proxy v) `hasVersionTag` versionTagQuicker then (*) else (/) -- TODO: contrary to some textbook presentations, -- @('*')@ should be used instead of @('/')@ to avoid the performance cost -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@, -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'. -- | Like 'commit' but quicker. It chould replace 'commit' entirely -- when Helios-C specifications will be fixed. commitQuicker :: forall crypto v c. CryptoParams crypto c => Proof crypto v c -> G crypto c -> G crypto c -> Commitment crypto c commitQuicker (Proof p) base basePowSec = base^p.proof_response * basePowSec^p.proof_challenge -- * Type 'Disjunction' -- | A 'Disjunction' is an 'inverse'd @('groupGen' '^'opinion)@ -- it's used in 'proveEncryption' to generate a 'Proof' -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@, type Disjunction = G booleanDisjunctions :: forall crypto c. CryptoParams crypto c => LL.List (Disjunction crypto c) booleanDisjunctions = LL.take 2 $ groupGenInverses::LL.List (G crypto c) intervalDisjunctions :: forall crypto c. CryptoParams crypto c => Natural -> Natural -> LL.List (Disjunction crypto c) intervalDisjunctions mini maxi = LL.take (int $ (unwrap (nat maxi) + one)-unwrap (nat mini)) $ LL.drop (int $ unwrap (nat mini)) $ groupGenInverses :: LL.List (G crypto c) where int = Int.round <<< BigInt.toNumber -- ** Type 'DisjProof' -- | A list of 'Proof's to prove that the opinion within an 'Encryption' -- is indexing a 'Disjunction' within a list of them, -- without revealing which opinion it is. newtype DisjProof crypto v c = DisjProof (List (Proof crypto v c)) derive newtype instance eqDisjProof :: Eq (DisjProof crypto v c) derive newtype instance showDisjProof :: Show (DisjProof crypto v c) derive newtype instance encodeJsonDisjProof :: (Reifies v Version, CryptoParams crypto c) => EncodeJson (DisjProof crypto v c) derive newtype instance decodeJsonDisjProof :: (Reifies v Version, CryptoParams crypto c) => DecodeJson (DisjProof crypto v c) instance newtypeDisjProof :: Newtype (DisjProof crypto v c) (List (Proof crypto v c)) where wrap = DisjProof unwrap (DisjProof x) = x -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@ -- returns a 'DisjProof' that 'enc' 'encrypt's -- the 'Disjunction' 'd' between 'prevDisjs' and 'nextDisjs'. -- -- The prover proves that it knows an 'encNonce', such that: -- @(enc '==' Encryption{encryption_nonce='groupGen' '^'encNonce, encryption_vault=elecPubKey'^'encNonce '*' groupGen'^'d})@ -- -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used. -- -- DOC: Pierrick Gaudry. , 2017. proveEncryption :: forall crypto v c. Reifies v Version => CryptoParams crypto c => PublicKey crypto c -> ZKP -> Tuple (List (Disjunction crypto c)) (List (Disjunction crypto c)) -> Tuple (EncryptionNonce crypto c) (Encryption crypto v c) -> Effect (DisjProof crypto v c) proveEncryption elecPubKey voterZKP (Tuple prevDisjs nextDisjs) (Tuple encNonce enc) = do -- Fake proofs for all 'Disjunction's except the genuine one. prevFakeProofs <- replicateA (List.length prevDisjs) fakeProof nextFakeProofs <- replicateA (List.length nextDisjs) fakeProof let fakeChallengeSum = sum ((\(Proof p) -> p.proof_challenge) <$> prevFakeProofs) + sum ((\(Proof p) -> p.proof_challenge) <$> nextFakeProofs) let statement = encryptionStatement voterZKP enc genuineProof <- prove encNonce (groupGen : elecPubKey : List.Nil) $ \genuineCommitments -> do let validCommitments = List.zipWith (encryptionCommitments elecPubKey enc) let prevCommitments = validCommitments prevDisjs prevFakeProofs let nextCommitments = validCommitments nextDisjs nextFakeProofs let commitments = join prevCommitments <> genuineCommitments <> join nextCommitments challenge <- hash statement commitments let genuineChallenge = challenge - fakeChallengeSum pure genuineChallenge -- NOTE: here by construction (genuineChallenge == challenge - fakeChallengeSum) -- thus (sum ((\(Proof p) -> p.proof_challenge) <$> proofs) == challenge) -- as checked in 'verifyEncryption'. let proofs = prevFakeProofs <> (genuineProof : nextFakeProofs) pure (DisjProof proofs) verifyEncryption :: forall crypto v c. Reifies v Version => CryptoParams crypto c => PublicKey crypto c -> ZKP -> List (Disjunction crypto c) -> Tuple (Encryption crypto v c) (DisjProof crypto v c) -> ExceptT ErrorVerifyEncryption Effect Boolean verifyEncryption elecPubKey voterZKP disjs (Tuple enc (DisjProof proofs)) = case isoZipWith (encryptionCommitments elecPubKey enc) disjs proofs of Nothing -> throwError $ ErrorVerifyEncryption_InvalidProofLength (nat $ List.length proofs) (nat $ List.length disjs) Just commitments -> do h <- lift $ hash (encryptionStatement voterZKP enc) (join commitments) pure (challengeSum == h) where challengeSum = sum ((\(Proof p) -> p.proof_challenge) <$> proofs) -- ** Hashing encryptionStatement :: forall crypto v c. CryptoParams crypto c => ZKP -> Encryption crypto v c -> String encryptionStatement (ZKP voterZKP) (Encryption enc) = "prove|"<>voterZKP<>"|" <> bytesNat enc.encryption_nonce<>"," <> bytesNat enc.encryption_vault<>"|" -- | @('encryptionCommitments' elecPubKey enc disj proof)@ -- returns the 'Commitment's with only the knowledge of the verifier. -- -- For the prover the 'Proof' comes from @fakeProof@, -- and for the verifier the 'Proof' comes from the prover. encryptionCommitments :: forall crypto v c. Reifies v Version => CryptoParams crypto c => PublicKey crypto c -> Encryption crypto v c -> Disjunction crypto c -> Proof crypto v c -> List (G crypto c) encryptionCommitments elecPubKey (Encryption enc) disj proof = commit proof groupGen enc.encryption_nonce : -- == groupGen ^ nonce if 'Proof' comes from 'prove'. -- base==groupGen, basePowSec==groupGen^encNonce. commit proof elecPubKey (enc.encryption_vault*disj) : -- == elecPubKey ^ nonce if 'Proof' comes from 'prove' -- and 'encryption_vault' encrypts (- logBase groupGen disj). -- base==elecPubKey, basePowSec==elecPubKey^encNonce. List.Nil -- ** Type 'ErrorVerifyEncryption' -- | Error raised by 'verifyEncryption'. data ErrorVerifyEncryption = ErrorVerifyEncryption_InvalidProofLength Natural Natural -- ^ When the number of proofs is different than -- the number of 'Disjunction's. -- deriving (Eq,Show) -- * Type 'Signature' -- | Schnorr-like signature. -- -- Used by each voter to sign his/her encrypted 'Ballot' -- using his/her 'Credential', -- in order to avoid ballot stuffing. data Signature crypto v c = Signature { signature_publicKey :: PublicKey crypto c -- ^ Verification key. , signature_proof :: Proof crypto v c } instance encodeJsonSignature :: ( Reifies v Version , CryptoParams crypto c ) => EncodeJson (Signature crypto v c) where encodeJson (Signature{signature_publicKey:pubKey, signature_proof:Proof p}) = "public_key" := pubKey ~> "challenge" := p.proof_challenge ~> "response" := p.proof_response ~> JSON.jsonEmptyObject instance decodeJsonSignature :: ( Reifies v Version , CryptoParams crypto c ) => DecodeJson (Signature crypto v c) where decodeJson json = do obj <- decodeJson json signature_publicKey <- obj .: "public_key" proof_challenge <- obj .: "challenge" proof_response <- obj .: "response" let signature_proof = Proof{proof_challenge, proof_response} pure $ Signature{signature_publicKey, signature_proof}