stripe-signature (empty) → 1.0.0.0
raw patch · 5 files changed
+239/−0 lines, 5 filesdep +basedep +bytestringdep +cryptonite
Dependencies added: base, bytestring, cryptonite, hex-text, memory, stripe-concepts, stripe-signature, text
Files
- changelog.md +12/−0
- library/Stripe/Signature.hs +111/−0
- license.txt +18/−0
- stripe-signature.cabal +58/−0
- test/test.hs +40/−0
+ changelog.md view
@@ -0,0 +1,12 @@+# Changelog++All notable changes to this package will be documented in this file.++The format is based on+[Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 1.0.0.0 - 2018-12-20++- Initial release
+ library/Stripe/Signature.hs view
@@ -0,0 +1,111 @@+{-# OPTIONS_GHC -Wall #-}++{- | https://stripe.com/docs/webhooks/signatures#verify-manually -}++module Stripe.Signature+ ( Sig (..), isSigValid, digest, signedPayload, natBytes, parseSig+ ) where++-- base+import qualified Data.List+import qualified Data.Maybe+import qualified Data.String+import Numeric.Natural (Natural)+import qualified Text.Read++-- bytestring+import Data.ByteString (ByteString)++-- cryptonite+import Crypto.Hash (SHA256)+import Crypto.MAC.HMAC as HMAC++-- hex-text+import qualified Text.Hex++-- memory+import qualified Data.ByteArray++-- stripe-concepts+import Stripe.Concepts (WebhookSecretKey (..))++-- text+import Data.Text (Text)+import qualified Data.Text++isSigValid :: Sig -> WebhookSecretKey -> ByteString -> Bool+isSigValid x secret body =+ Data.List.any (Data.ByteArray.eq correctDigest) (sigV1 x)+ where+ correctDigest = digest secret (sigTime x) body++digest :: WebhookSecretKey -> Natural -> ByteString -> HMAC SHA256+digest (WebhookSecretKey secret) time body =+ HMAC.hmac secret (signedPayload time body)++signedPayload :: Natural -> ByteString -> ByteString+signedPayload time body =+ mconcat+ [ natBytes time+ , encodeAscii "."+ , body+ ]++{- | Convert a natural number to the ASCII encoding of its decimal+representation. -}++natBytes :: Natural -> ByteString+natBytes = encodeAscii . (show :: Natural -> String)++encodeAscii :: String -> ByteString+encodeAscii = Data.String.fromString++{- | The relevant bits of data extracted from the Stripe signature header. -}++data Sig =+ Sig+ { sigTime :: Natural+ , sigV1 :: [ByteString]+ }++{- | Parse the Stripe signature header, returning 'Nothing' if parsing fails. -}++parseSig :: Text -> Maybe Sig+parseSig txt =+ let+ parts :: [(Text, Text)]+ parts = splitSig txt+ in+ do+ time <- Data.List.lookup (Data.Text.pack "t") parts+ >>= (readNatural . Data.Text.unpack)++ let+ v1 = Data.Maybe.mapMaybe+ ( \(k, v) ->+ if k == Data.Text.pack "v1"+ then Text.Hex.decodeHex v+ else Nothing+ )+ parts++ pure Sig{ sigTime = time, sigV1 = v1 }++splitSig :: Text -> [(Text, Text)]+splitSig =+ Data.Maybe.catMaybes+ . fmap (split2 (Data.Text.pack "="))+ . Data.Text.splitOn (Data.Text.pack ",")++split2 :: Text -> Text -> Maybe (Text, Text)+split2 pat src =+ let+ (x, y) = Data.Text.breakOn pat src+ y' = Data.Text.drop (Data.Text.length pat) y+ in+ if Data.Text.null y then Nothing else Just (x, y')++{- | Parse a number consisting of one or more digits 0 through 9. -}++readNatural :: String -> Maybe Natural+readNatural = Text.Read.readMaybe
+ license.txt view
@@ -0,0 +1,18 @@+Copyright 2018 Typeclass Consulting, LLC++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the Software is furnished to do so,+subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ stripe-signature.cabal view
@@ -0,0 +1,58 @@+name: stripe-signature+version: 1.0.0.0++synopsis: Verification of Stripe webhook signatures+category: Web++description:+ When <https://stripe.com/ Stripe> sends an event to your webhook, it+ includes an HTTP header named @Stripe-Signature@. You should use this+ to verify the authenticity of the request to ensure that you are not+ acting upon forged events originating from some source other than+ Stripe.++homepage: https://github.com/typeclasses/stripe+bug-reports: https://github.com/typeclasses/stripe/issues++author: Chris Martin+maintainer: Chris Martin, Julie Moronuki++copyright: 2018 Typeclass Consulting, LLC+license: MIT+license-file: license.txt++build-type: Simple+cabal-version: >=1.10+tested-with: GHC==8.2.2, GHC==8.4.3, GHC==8.6.1++extra-source-files:+ changelog.md++library+ hs-source-dirs: library+ default-language: Haskell2010++ exposed-modules:+ Stripe.Signature++ build-depends:+ base >=4.10 && <5+ , bytestring+ , cryptonite+ , hex-text+ , memory+ , stripe-concepts+ , text++test-suite test+ default-language: Haskell2010+ hs-source-dirs: test+ type: exitcode-stdio-1.0+ main-is: test.hs+ ghc-options: -threaded++ build-depends:+ base >=4.10 && <5+ , bytestring+ , stripe-signature+ , text
+ test/test.hs view
@@ -0,0 +1,40 @@+{-# OPTIONS_GHC -Wall #-}++import Stripe.Signature++-- base+import Control.Monad (unless)+import Data.Word (Word8)+import Numeric.Natural (Natural)+import System.Exit (die)++-- bytestring+import qualified Data.ByteString++-- text+import qualified Data.Text++test_string :: String+test_string =+ "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e\+ \77a0e56ff536d0ce8e108d8bd,v0=6ffbb59b2300aae63f27240606\+ \9a9788598b792a944a07aba816edb039989a39"++test_time :: Natural+test_time = 1492774577++test_v1Bytes :: [Word8]+test_v1Bytes =+ [82,87,168,105,231,236,235,237,163,42,255,166,44,220,163,250+ ,81,202,215,231,122,14,86,255,83,109,12,232,225,8,216,189]++main :: IO ()+main =+ case parseSig (Data.Text.pack test_string) of+ Nothing -> die "Signature parsing failed"+ Just sig ->+ do+ unless (sigTime sig == test_time) $+ die ("sigTime: " ++ show (sigTime sig))+ unless ((Data.ByteString.unpack <$> sigV1 sig) == [test_v1Bytes]) $+ die ("sigV1: " ++ show (Data.ByteString.unpack <$> sigV1 sig))