packages feed

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 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))