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 +6/−0
- library/Stripe/Signature.hs +57/−72
- stripe-signature.cabal +5/−5
- test/test.hs +69/−52
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"]