diff --git a/PaymentServer.cabal b/PaymentServer.cabal index 037d14b1b1792d62306ad50d30c28a248a6fa634..165887ef0111186ff8eda3db078cdda6c634fc6d 100644 --- a/PaymentServer.cabal +++ b/PaymentServer.cabal @@ -16,12 +16,14 @@ cabal-version: >=1.10 library hs-source-dirs: src exposed-modules: PaymentServer.Processors.Stripe + , PaymentServer.Ristretto , PaymentServer.Issuer , PaymentServer.Persistence , PaymentServer.Redemption , PaymentServer.Server , PaymentServer.Main build-depends: base >= 4.7 && < 5 + , optparse-applicative , aeson , servant , servant-server @@ -45,6 +47,15 @@ executable PaymentServer-exe , PaymentServer default-language: Haskell2010 +executable PaymentServer-generate-key + hs-source-dirs: generate-key + main-is: Main.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wmissing-import-lists -Wunused-imports + build-depends: base + , text + , PaymentServer + default-language: Haskell2010 + source-repository head type: git location: https://github.com/privatestorageio/PaymentServer diff --git a/generate-key/Main.hs b/generate-key/Main.hs new file mode 100644 index 0000000000000000000000000000000000000000..723c9aae211ddb03d50bfc218d3cec299cd13e1c --- /dev/null +++ b/generate-key/Main.hs @@ -0,0 +1,19 @@ +-- | Generate a random Ristretto-flavored PrivacyPass signing key. +module Main + ( main + ) where + +import Prelude hiding + ( putStrLn + ) + +import Data.Text.IO + ( putStrLn + ) + +import PaymentServer.Ristretto + ( randomSigningKey + ) + +main :: IO () +main = randomSigningKey >>= putStrLn diff --git a/nix/PaymentServer.nix b/nix/PaymentServer.nix new file mode 100644 index 0000000000000000000000000000000000000000..ec9fe4474bab6c9190d1d61dcf1d1a1d0a90b718 --- /dev/null +++ b/nix/PaymentServer.nix @@ -0,0 +1,93 @@ +let + buildDepError = pkg: + builtins.throw '' + The Haskell package set does not contain the package: ${pkg} (build dependency). + + If you are using Stackage, make sure that you are using a snapshot that contains the package. Otherwise you may need to update the Hackage snapshot you are using, usually by updating haskell.nix. + ''; + sysDepError = pkg: + builtins.throw '' + The Nixpkgs package set does not contain the package: ${pkg} (system dependency). + + You may need to augment the system package mapping in haskell.nix so that it can be found. + ''; + pkgConfDepError = pkg: + builtins.throw '' + The pkg-conf packages does not contain the package: ${pkg} (pkg-conf dependency). + + You may need to augment the pkg-conf package mapping in haskell.nix so that it can be found. + ''; + exeDepError = pkg: + builtins.throw '' + The local executable components do not include the component: ${pkg} (executable dependency). + ''; + legacyExeDepError = pkg: + builtins.throw '' + The Haskell package set does not contain the package: ${pkg} (executable dependency). + + If you are using Stackage, make sure that you are using a snapshot that contains the package. Otherwise you may need to update the Hackage snapshot you are using, usually by updating haskell.nix. + ''; + buildToolDepError = pkg: + builtins.throw '' + Neither the Haskell package set or the Nixpkgs package set contain the package: ${pkg} (build tool dependency). + + If this is a system dependency: + You may need to augment the system package mapping in haskell.nix so that it can be found. + + If this is a Haskell dependency: + If you are using Stackage, make sure that you are using a snapshot that contains the package. Otherwise you may need to update the Hackage snapshot you are using, usually by updating haskell.nix. + ''; +in { system, compiler, flags, pkgs, hsPkgs, pkgconfPkgs, ... }: + { + flags = {}; + package = { + specVersion = "1.10"; + identifier = { name = "PaymentServer"; version = "0.1.0.0"; }; + license = "Apache-2.0"; + copyright = "2019 Private Storage.io, LLC."; + maintainer = "support@privatestorage.io"; + author = "Jean-Paul Calderone"; + homepage = "https://github.com/privatestorageio/PaymentServer#readme"; + url = ""; + synopsis = "Coordinate entities for the purchase of PrivateStorage.io vouchers."; + description = ""; + buildType = "Simple"; + }; + components = { + "library" = { + depends = [ + (hsPkgs."base" or (buildDepError "base")) + (hsPkgs."optparse-applicative" or (buildDepError "optparse-applicative")) + (hsPkgs."aeson" or (buildDepError "aeson")) + (hsPkgs."servant" or (buildDepError "servant")) + (hsPkgs."servant-server" or (buildDepError "servant-server")) + (hsPkgs."wai" or (buildDepError "wai")) + (hsPkgs."wai-extra" or (buildDepError "wai-extra")) + (hsPkgs."data-default" or (buildDepError "data-default")) + (hsPkgs."warp" or (buildDepError "warp")) + (hsPkgs."stripe-core" or (buildDepError "stripe-core")) + (hsPkgs."text" or (buildDepError "text")) + (hsPkgs."containers" or (buildDepError "containers")) + (hsPkgs."cryptonite" or (buildDepError "cryptonite")) + ]; + pkgconfig = [ + (pkgconfPkgs."ristretto" or (pkgConfDepError "ristretto")) + ]; + }; + exes = { + "PaymentServer-exe" = { + depends = [ + (hsPkgs."base" or (buildDepError "base")) + (hsPkgs."PaymentServer" or (buildDepError "PaymentServer")) + ]; + }; + "PaymentServer-generate-key" = { + depends = [ + (hsPkgs."base" or (buildDepError "base")) + (hsPkgs."text" or (buildDepError "text")) + (hsPkgs."PaymentServer" or (buildDepError "PaymentServer")) + ]; + }; + }; + }; + } // rec { src = (pkgs.lib).mkDefault ../.; } \ No newline at end of file diff --git a/nix/README b/nix/README new file mode 100644 index 0000000000000000000000000000000000000000..d4de15ba9c275f6882e3ed8fe5c597e312c27370 --- /dev/null +++ b/nix/README @@ -0,0 +1,8 @@ +This directory contains expressions for packaging PaymentServer on NixOS. +The contents of this directory are almost entirely automatically generated by stack-to-nix. +Editing them is probably a bad idea since it will make future automated updates more difficult. + +However, I did edit default.nix to replace the master tarball of haskell.nix with a particular revision. +Read the comment there for details. + +See https://github.com/input-output-hk/haskell.nix for details of stack-to-nix. diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..9605b5b0c6e1a7ab470edbce26fa13aee556cd2b --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,18 @@ +{ pkgs ? import <nixpkgs> {} }: + +let + # Pin a particular version of haskell.nix. The particular version isn't + # special. It's just recent at the time this expression was written and it + # is known to work with PaymentServer. It could be bumped if necessary but + # this would probably only happen as a result of bumping the resolver in + # stack.yaml. + haskell = import (builtins.fetchTarball https://github.com/input-output-hk/haskell.nix/archive/0cb32e695d7014908fb01fd7e3d225ea33dbdc98.tar.gz) { inherit pkgs; }; + + pkgSet = haskell.mkStackPkgSet { + stack-pkgs = import ./pkgs.nix; + pkg-def-extras = []; + modules = []; + }; + +in + pkgSet.config.hsPkgs diff --git a/nix/pkgs.nix b/nix/pkgs.nix new file mode 100644 index 0000000000000000000000000000000000000000..26ae4881edad1f52e936df5fe53ff4d23f3ab841 --- /dev/null +++ b/nix/pkgs.nix @@ -0,0 +1,9 @@ +{ + extras = hackage: + { + packages = ({ + "stripe-core" = (((hackage.stripe-core)."2.5.0").revisions).default; + } // { PaymentServer = ./PaymentServer.nix; }) // {}; + }; + resolver = "lts-14.1"; + } \ No newline at end of file diff --git a/src/PaymentServer/Issuer.hs b/src/PaymentServer/Issuer.hs index 0f47c47a2807523663269cab63db9183bd2c5f72..1041d768d4479b03e45cd69af01fe3fac328d7c2 100644 --- a/src/PaymentServer/Issuer.hs +++ b/src/PaymentServer/Issuer.hs @@ -10,13 +10,23 @@ module PaymentServer.Issuer , ChallengeBypass(ChallengeBypass) , Issuer , trivialIssue + , ristrettoIssue ) where +import PaymentServer.Ristretto + ( Issuance(Issuance) + , ristretto + ) + import Data.Text ( Text + , pack ) --- | A public key corresponding to our private key. +-- | A private key for signing. +type SigningKey = Text + +-- | A public key corresponding to a SigningKey. type PublicKey = Text -- | A cryptographic signature of a blinded token created using our private @@ -38,13 +48,26 @@ data ChallengeBypass = -- | An issuer accepts a list of blinded tokens and returns signatures of -- those tokens along with proof that it used a particular key to construct -- the signatures. -type Issuer = [BlindedToken] -> ChallengeBypass +type Issuer = [BlindedToken] -> Either Text ChallengeBypass -- | trivialIssue makes up and returns some nonsense values that satisfy the -- structural requirements but not the semantic ones. trivialIssue :: Issuer trivialIssue tokens = + Right $ ChallengeBypass "fake-public-key" (replicate (length tokens) "fake-signature") "fake-proof" + +-- | ristrettoIssue uses Ristretto-flavored PrivacyPass (aka +-- `challenge-bypass-ristretto`) to create token signatures in a +-- privacy-preserving manner. +ristrettoIssue + :: SigningKey -- ^ The key to provide to the PrivacyPass signer. + -> Issuer -- ^ An issuer using the given key. +ristrettoIssue signingKey tokens = do + let issuance = ristretto signingKey tokens + case issuance of + Right (Issuance publicKey tokens proof) -> Right $ ChallengeBypass publicKey tokens proof + Left err -> Left . pack . show $ err diff --git a/src/PaymentServer/Main.hs b/src/PaymentServer/Main.hs index 7d9ee2a744eb96d4f977e728298e21da1385742a..8c52604b95aa793cf9f539e9884b19e09c43ad36 100644 --- a/src/PaymentServer/Main.hs +++ b/src/PaymentServer/Main.hs @@ -1,8 +1,17 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + -- | This module implements the main entrypoint to the PaymentServer. module PaymentServer.Main ( main ) where +import Text.Printf + ( printf + ) +import Data.Text + ( Text + ) import Data.Default ( def ) @@ -17,13 +26,111 @@ import Network.Wai.Middleware.RequestLogger import PaymentServer.Persistence ( memory ) +import PaymentServer.Issuer + ( trivialIssue + , ristrettoIssue + ) import PaymentServer.Server ( paymentServerApp ) +import Options.Applicative + ( Parser + , ParserInfo + , option + , auto + , str + , optional + , long + , help + , value + , showDefault + , execParser + , info + , helper + , fullDesc + , progDesc + , header + , (<**>) + ) +import System.Exit + ( exitFailure + ) +import Data.Semigroup ((<>)) + +data Issuer = + Trivial + | Ristretto + deriving (Show, Eq, Ord, Read) + +data Database = + Memory + | SQLite3 + deriving (Show, Eq, Ord, Read) + +data ServerConfig = ServerConfig + { issuer :: Issuer + , signingKey :: Maybe Text + , database :: Database + , databasePath :: Maybe Text + } + deriving (Show, Eq) + +sample :: Parser ServerConfig +sample = ServerConfig + <$> option auto + ( long "issuer" + <> help "Which issuer to use: trivial or ristretto" + <> showDefault + <> value Trivial ) + <*> optional (option str + ( long "signing-key" + <> help "The base64 encoded signing key (ristretto only)" + <> showDefault ) ) + <*> option auto + ( long "database" + <> help "Which database to use: sqlite3 or memory" + <> showDefault + <> value Memory ) + <*> optional ( option str + ( long "database-path" + <> help "Path to on-disk database (sqlite3 only)" + <> showDefault ) ) + +opts :: ParserInfo ServerConfig +opts = info (sample <**> helper) + ( fullDesc + <> progDesc "" + <> header "" + ) + main :: IO () -main = do - db <- memory - let app = paymentServerApp db - logger <- mkRequestLogger $ def { outputFormat = Detailed True} - run 8081 $ logger app +main = + let + getIssuer ServerConfig{ issuer, signingKey } = + case (issuer, signingKey) of + (Trivial, Nothing) -> Right trivialIssue + (Ristretto, Just key) -> Right $ ristrettoIssue key + _ -> Left "invalid options" + getDatabase ServerConfig{ database, databasePath } = + case (database, databasePath) of + (Memory, Nothing) -> Right memory + _ -> Left "invalid options" + in do + config <- execParser opts + case getIssuer config of + Left err -> do + print err + exitFailure + Right issuer -> + case getDatabase config of + Left err ->do + print err + exitFailure + Right getDB -> do + db <- getDB + let port = 8081 + let app = paymentServerApp issuer db + logger <- mkRequestLogger (def { outputFormat = Detailed True}) + putStrLn (printf "Listening on %d" port :: String) + run port $ logger app diff --git a/src/PaymentServer/Redemption.hs b/src/PaymentServer/Redemption.hs index c2cbe06f077bb5682c7a36139ab82921d0f92330..e75cb0f49f2b2456f7f41f6aae9a2c9b8703f2a3 100644 --- a/src/PaymentServer/Redemption.hs +++ b/src/PaymentServer/Redemption.hs @@ -21,6 +21,7 @@ import Control.Monad.IO.Class import Data.Text ( pack ) +import qualified Data.Text.IO as TextIO import Data.Text.Encoding ( encodeUtf8 ) @@ -54,6 +55,7 @@ import Crypto.Hash ) import PaymentServer.Persistence ( VoucherDatabase(redeemVoucher) + , RedeemError(NotPaid, AlreadyRedeemed) , Fingerprint , Voucher ) @@ -122,12 +124,20 @@ redeem issue database (Redeem voucher tokens) = do let fingerprint = fingerprintFromTokens tokens result <- liftIO $ PaymentServer.Persistence.redeemVoucher database voucher fingerprint case result of - Left err -> throwError jsonErr400 - Right () -> - let - (ChallengeBypass key signatures proof) = issue tokens - in - return $ Succeeded key signatures proof + Left NotPaid -> do + liftIO $ putStrLn "Attempt to redeem unpaid voucher" + throwError jsonErr400 + Left AlreadyRedeemed -> do + liftIO $ putStrLn "Attempt to double-spend paid voucher" + throwError jsonErr400 + Right () -> do + let result = issue tokens + case result of + Left reason -> do + liftIO $ TextIO.putStrLn reason + throwError jsonErr400 + Right (ChallengeBypass key signatures proof) -> + return $ Succeeded key signatures proof -- | Compute a cryptographic hash (fingerprint) of a list of tokens which can -- be used as an identifier for this exact sequence of tokens. diff --git a/src/PaymentServer/Ristretto.hs b/src/PaymentServer/Ristretto.hs new file mode 100644 index 0000000000000000000000000000000000000000..a2968331eb42368772901ade53344f447ff7b84b --- /dev/null +++ b/src/PaymentServer/Ristretto.hs @@ -0,0 +1,163 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE EmptyDataDecls #-} + +module PaymentServer.Ristretto + ( Issuance(Issuance) + , randomSigningKey + , ristretto + ) where + +import Control.Exception + ( bracket + , assert + ) +import System.IO.Unsafe + ( unsafePerformIO + ) +import Data.Text + ( Text + , unpack + , pack + ) + +import Foreign.Ptr + ( Ptr + , nullPtr + ) +import Foreign.C.String + ( CString + , withCString + , newCString + , peekCString + ) +import Foreign.Marshal.Alloc + ( free + ) +import Foreign.Marshal.Array + ( withArray + ) + +data C_BlindedToken +data C_SignedToken +data C_SigningKey +data C_PublicKey +data C_BatchDLEQProof + +foreign import ccall "blinded_token_decode_base64" blinded_token_decode_base64 :: CString -> IO (Ptr C_BlindedToken) +foreign import ccall "blinded_token_destroy" blinded_token_destroy :: Ptr C_BlindedToken -> IO () + +foreign import ccall "public_key_encode_base64" public_key_encode_base64 :: Ptr C_PublicKey -> IO CString + +foreign import ccall "signing_key_random" signing_key_random :: IO (Ptr C_SigningKey) +foreign import ccall "signing_key_decode_base64" signing_key_decode_base64 :: CString -> IO (Ptr C_SigningKey) +foreign import ccall "signing_key_encode_base64" signing_key_encode_base64 :: Ptr C_SigningKey -> IO CString +foreign import ccall "signing_key_destroy" signing_key_destroy :: Ptr C_SigningKey -> IO () +foreign import ccall "signing_key_get_public_key" signing_key_get_public_key :: Ptr C_SigningKey -> IO (Ptr C_PublicKey) +foreign import ccall "signing_key_sign" signing_key_sign :: Ptr C_SigningKey -> Ptr C_BlindedToken -> IO (Ptr C_SignedToken) + +foreign import ccall "signed_token_encode_base64" signed_token_encode_base64 :: Ptr C_SignedToken -> IO CString + +foreign import ccall "batch_dleq_proof_new" batch_dleq_proof_new :: Ptr (Ptr C_BlindedToken) -> Ptr (Ptr C_SignedToken) -> Int -> Ptr C_SigningKey -> IO (Ptr C_BatchDLEQProof) +foreign import ccall "batch_dleq_proof_encode_base64" batch_dleq_proof_encode_base64 :: Ptr C_BatchDLEQProof -> IO CString +foreign import ccall "batch_dleq_proof_destroy" batch_dleq_proof_destroy :: Ptr C_BatchDLEQProof -> IO () + +-- | Private type to represent the return value of ristretto. +data Issuance = + Issuance + { publicKey :: Text -- ^ The base64-encoded public key corresponding to the + -- signing key which generated the signatures. + , signatures :: [Text] -- ^ A list of base64-encoded token signatures. + , proof :: Text -- ^ The base64-encoded batch DLEQ proof that the signatures + -- were made with the signing key corresponding to the public + -- key. + } + +data RistrettoFailure + = SigningKeyAllocation + | SigningKeyDecoding + | BlindedTokenAllocation + | BlindedTokenDecoding + | SignedTokenAllocation + | TokenSigning + | SignedTokenEncoding + | ProofCreation + | SignedTokenPeek + | PublicKeyLookup + | PublicKeyEncoding + deriving (Show, Eq) + +ristretto + :: Text -- ^ The base64 encoded signing key. + -> [Text] -- ^ A list of the base64 blinded tokens. + -> Either RistrettoFailure Issuance -- ^ Left for an error, otherwise + -- Right with the ristretto results +ristretto textSigningKey textTokens = + let + newProof blindedTokens signedTokens signingKey = + withArray blindedTokens $ \cBlindedTokensArray -> + withArray signedTokens $ \cSignedTokensArray -> + batch_dleq_proof_new cBlindedTokensArray cSignedTokensArray (length blindedTokens) signingKey + + newEncodedProof blindedTokens signedTokens signingKey = + bracket (newProof blindedTokens signedTokens signingKey) batch_dleq_proof_destroy $ \proof -> + bracket (batch_dleq_proof_encode_base64 proof) free peekCString + + stringSigningKey = unpack textSigningKey + stringTokens = map unpack textTokens + + extractKeyMaterial :: String -> IO (Either RistrettoFailure (Ptr C_SigningKey, Ptr C_PublicKey)) + extractKeyMaterial stringSigningKey = do + cStringSigningKey <- newCString stringSigningKey + if cStringSigningKey == nullPtr + then return $ Left SigningKeyAllocation + else do + signingKey <- signing_key_decode_base64 cStringSigningKey + if signingKey == nullPtr + then return $ Left SigningKeyDecoding + else do + publicKey <- signing_key_get_public_key signingKey + if publicKey == nullPtr + then return $ Left PublicKeyLookup + else return $ Right (signingKey, publicKey) + in + unsafePerformIO $ do + keys <- extractKeyMaterial stringSigningKey + case keys of + Left err -> return $ Left err + Right (signingKey, publicKey) -> do + cStringEncodedPublicKey <- public_key_encode_base64 publicKey + if cStringEncodedPublicKey == nullPtr + then return $ Left PublicKeyEncoding + else do + encodedPublicKey <- peekCString cStringEncodedPublicKey + cStringTokens <- mapM newCString stringTokens + if nullPtr `elem` cStringTokens + then return $ Left BlindedTokenAllocation + else do + blindedTokens <- mapM blinded_token_decode_base64 cStringTokens + if nullPtr `elem` blindedTokens + then return $ Left BlindedTokenDecoding + else do + signedTokens <- mapM (signing_key_sign signingKey) blindedTokens + if nullPtr `elem` signedTokens + then return $ Left TokenSigning + else do + encodedCStringSignedTokens <- mapM signed_token_encode_base64 signedTokens + if nullPtr `elem` encodedCStringSignedTokens + then return $ Left SignedTokenEncoding + else do + encodedSignedTokens <- mapM peekCString encodedCStringSignedTokens + encodedProof <- newEncodedProof blindedTokens signedTokens signingKey + return . Right $ Issuance (pack encodedPublicKey) (map pack encodedSignedTokens) (pack encodedProof) + + +-- | randomSigningKey generates a new signing key at random and returns it +-- encoded as a base64 string. +randomSigningKey :: IO Text +randomSigningKey = do + cSigningKey <- signing_key_random + cString <- signing_key_encode_base64 cSigningKey + signing_key_destroy cSigningKey + result <- peekCString cString + free cString + return $ pack result diff --git a/src/PaymentServer/Server.hs b/src/PaymentServer/Server.hs index 6eea6abed47c088abf55b9aea4b66bb3658c53c7..973ef126d9d005873016d075dedff5276796e8b9 100644 --- a/src/PaymentServer/Server.hs +++ b/src/PaymentServer/Server.hs @@ -24,7 +24,7 @@ import PaymentServer.Redemption , redemptionServer ) import PaymentServer.Issuer - ( trivialIssue + ( Issuer ) import PaymentServer.Persistence ( VoucherDatabase @@ -36,15 +36,15 @@ type PaymentServerAPI :<|> "v1" :> "redeem" :> RedemptionAPI -- | Create a server which uses the given database. -paymentServer :: VoucherDatabase d => d -> Server PaymentServerAPI -paymentServer d = - stripeServer d - :<|> redemptionServer trivialIssue d +paymentServer :: VoucherDatabase d => Issuer -> d -> Server PaymentServerAPI +paymentServer issuer database = + stripeServer database + :<|> redemptionServer issuer database paymentServerAPI :: Proxy PaymentServerAPI paymentServerAPI = Proxy -- | Create a Servant Application which serves the payment server API using -- the given database. -paymentServerApp :: VoucherDatabase d => d -> Application -paymentServerApp = serve paymentServerAPI . paymentServer +paymentServerApp :: VoucherDatabase d => Issuer -> d -> Application +paymentServerApp issuer = serve paymentServerAPI . paymentServer issuer