packages feed

stripe-signature 1.0.0.15 → 1.0.0.16

raw patch · 4 files changed

+137/−129 lines, 4 filesdep ~basedep ~bytestringdep ~textPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, bytestring, text

API changes (from Hackage documentation)

Files

changelog.md view
@@ -1,6 +1,12 @@ Changelog ========= +1.0.0.16 - 2023-06-26+--------------------------------------------------++Update stuff++ 1.0.0.15 - 2023-02-01 -------------------------------------------------- 
library/Stripe/Signature.hs view
@@ -1,113 +1,98 @@-{- | https://stripe.com/docs/webhooks/signatures#verify-manually -}-+-- | 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---- base16-bytestring-import qualified Data.ByteString.Base16 as Base16+  ( Sig (..),+    isSigValid,+    digest,+    signedPayload,+    natBytes,+    parseSig,+  )+where --- bytestring+import Crypto.Hash.SHA256 qualified import Data.ByteString (ByteString)---- cryptohash-sha256-import qualified Crypto.Hash.SHA256---- stripe-concepts+import Data.ByteString.Base16 qualified as Base16+import Data.List qualified+import Data.Maybe qualified+import Data.String qualified+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Numeric.Natural (Natural) import Stripe.Concepts (WebhookSecretKey (..))---- text-import           Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text+import Text.Read qualified  isSigValid :: Sig -> WebhookSecretKey -> ByteString -> Bool isSigValid x secret body =-    Data.List.any ((==) correctDigest) (sigV1 x)+  Data.List.any ((==) correctDigest) (sigV1 x)   where     correctDigest = digest secret (sigTime x) body  digest :: WebhookSecretKey -> Natural -> ByteString -> ByteString digest (WebhookSecretKey secret) time body =-    Crypto.Hash.SHA256.hmac secret (signedPayload time body)+  Crypto.Hash.SHA256.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. -}+  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. -}+-- | 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 (Text.pack "t") parts-                    >>= (readNatural . Text.unpack)+  let parts :: [(Text, Text)]+      parts = splitSig txt+   in do+        time <-+          Data.List.lookup (Text.pack "t") parts+            >>= (readNatural . Text.unpack) -      let-          v1 = Data.Maybe.mapMaybe-              ( \(k, v) ->-                  if k == Text.pack "v1"-                  then decodeHex v-                  else Nothing-              )-              parts+        let v1 =+              Data.Maybe.mapMaybe+                ( \(k, v) ->+                    if k == Text.pack "v1"+                      then decodeHex v+                      else Nothing+                )+                parts -      pure Sig{ sigTime = time, sigV1 = v1 }+        pure Sig {sigTime = time, sigV1 = v1}  splitSig :: Text -> [(Text, Text)] splitSig =-    Data.Maybe.catMaybes+  Data.Maybe.catMaybes     . fmap (split2 (Text.pack "="))     . Text.splitOn (Text.pack ",")  split2 :: Text -> Text -> Maybe (Text, Text) split2 pat src =-    let-        (x, y) = Text.breakOn pat src-        y' = Text.drop (Text.length pat) y-    in-        if Text.null y then Nothing else Just (x, y')--{- | Parse a number consisting of one or more digits 0 through 9. -}+  let (x, y) = Text.breakOn pat src+      y' = Text.drop (Text.length pat) y+   in if 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 -{- | Decodes hexidecimal text as a byte string. The result is a 'Just' value iff-the text contains an even number of characters and consists only of the digits-@0@ through @9@ and letters @a@ through @f@. -}-+-- | Decodes hexadecimal text as a byte string. The result is a 'Just' value iff+-- the text contains an even number of characters and consists only of the digits+-- @0@ through @9@ and letters @a@ through @f@. decodeHex :: Text -> Maybe ByteString decodeHex = either (const Nothing) Just . Base16.decode . Text.encodeUtf8
stripe-signature.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0  name: stripe-signature-version: 1.0.0.15+version: 1.0.0.16 synopsis: Verification of Stripe webhook signatures category: Web @@ -24,15 +24,15 @@ extra-source-files: *.md  common base-    default-language: Haskell2010+    default-language: GHC2021     ghc-options: -Wall     build-depends:-      , base ^>= 4.14 || ^>= 4.15 || ^>= 4.16 || ^>= 4.17+      , base ^>= 4.16 || ^>= 4.17 || ^>= 4.18       , base16-bytestring ^>= 1.0.2-      , bytestring ^>= 0.10.12 || ^>= 0.11+      , bytestring ^>= 0.11       , cryptohash-sha256 ^>= 0.11.102       , stripe-concepts ^>= 1.0.3-      , text ^>= 1.2.4 || ^>= 2.0+      , text ^>= 1.2.5 || ^>= 2.0  library     import: base
test/test.hs view
@@ -1,77 +1,94 @@-import Stripe.Signature---- base-import Control.Monad   (unless)-import Data.Foldable   (for_)-import Data.Word       (Word8)+import Control.Monad (unless)+import Data.ByteString qualified+import Data.ByteString.Base16 qualified as Base16+import Data.ByteString.Char8 qualified+import Data.Foldable (for_)+import Data.Text qualified+import Data.Word (Word8) import Numeric.Natural (Natural)-import System.Exit     (exitFailure)---- base16-bytestring-import qualified Data.ByteString.Base16 as Base16---- bytestring-import qualified Data.ByteString-import qualified Data.ByteString.Char8---- stripe-concepts import Stripe.Concepts (WebhookSecretKey (..))---- text-import qualified Data.Text+import Stripe.Signature+import System.Exit (exitFailure)  data Failure = Failure String  main :: IO () main =-    unless (null failures) $ do-        for_ failures $ \(Failure x) -> putStrLn ("🔥 " ++ x)-        exitFailure+  unless (null failures) $ do+    for_ failures $ \(Failure x) -> putStrLn ("🔥 " ++ x)+    exitFailure  failures :: [Failure] failures = parseFailures ++ validationFailures  parseFailures :: [Failure] parseFailures =-    case parseSig (Data.Text.pack test_string) of-        Nothing -> [Failure "Signature parsing failed"]-        Just sig ->-            case sigTime sig == test_time of-                True -> []-                False -> [Failure $ "sigTime: " ++ show (sigTime sig)]-            ++-            case (Data.ByteString.unpack <$> sigV1 sig) == [test_v1Bytes] of-                True -> []-                False -> [Failure $ "sigV1: " ++ show (Data.ByteString.unpack <$> sigV1 sig)]-+  case parseSig (Data.Text.pack test_string) of+    Nothing -> [Failure "Signature parsing failed"]+    Just sig ->+      case sigTime sig == test_time of+        True -> []+        False -> [Failure $ "sigTime: " ++ show (sigTime sig)]+        ++ case (Data.ByteString.unpack <$> sigV1 sig) == [test_v1Bytes] of+          True -> []+          False -> [Failure $ "sigV1: " ++ show (Data.ByteString.unpack <$> sigV1 sig)]   where     test_string :: String     test_string =-        "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e\-        \77a0e56ff536d0ce8e108d8bd,v0=6ffbb59b2300aae63f27240606\-        \9a9788598b792a944a07aba816edb039989a39"+      "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]+      [ 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+      ]  validationFailures :: [Failure] validationFailures =-    case Base16.decode (Data.ByteString.Char8.pack "f89c0a0e96fa6304a7ebd5ecaf13bc1beb859577cd7cafb7bdbc43c3bc2f6afa") of-        Left _ -> [Failure "Digest decoding failed"]-        Right d ->-            case isSigValid Sig{ sigTime = 123, sigV1 = [d] } (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello") of-                True -> []-                False -> [Failure "isSigValid rejects a valid signature"]-            ++-            case isSigValid Sig{ sigTime = 123, sigV1 = [d] } (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello!") of-                False -> []-                True -> [Failure "isSigValid accepts an invalid signature"]-            ++-            case isSigValid Sig{ sigTime = 123, sigV1 = [Data.ByteString.Char8.pack "whatever", d] } (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello") of-                True -> []-                False -> [Failure "isSigValid should accept if *any* of the sigs are valid"]+  case Base16.decode (Data.ByteString.Char8.pack "f89c0a0e96fa6304a7ebd5ecaf13bc1beb859577cd7cafb7bdbc43c3bc2f6afa") of+    Left _ -> [Failure "Digest decoding failed"]+    Right d ->+      case isSigValid Sig {sigTime = 123, sigV1 = [d]} (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello") of+        True -> []+        False -> [Failure "isSigValid rejects a valid signature"]+        ++ case isSigValid Sig {sigTime = 123, sigV1 = [d]} (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello!") of+          False -> []+          True -> [Failure "isSigValid accepts an invalid signature"]+        ++ case isSigValid Sig {sigTime = 123, sigV1 = [Data.ByteString.Char8.pack "whatever", d]} (WebhookSecretKey (Data.ByteString.Char8.pack "secret")) (Data.ByteString.Char8.pack "hello") of+          True -> []+          False -> [Failure "isSigValid should accept if *any* of the sigs are valid"]