packages feed

aws-sns-verify (empty) → 0.0.0.0

raw patch · 14 files changed

+729/−0 lines, 14 filesdep +aesondep +aeson-qqdep +async

Dependencies added: aeson, aeson-qq, async, aws-sns-verify, base, bytestring, errors, hspec, http-conduit, http-types, memory, pem, text, wai, warp, x509, x509-validation

Files

+ CHANGELOG.md view
@@ -0,0 +1,8 @@+## [_Unreleased_](https://github.com/freckle/aws-sns-verify/compare/v0.0.0.0...main)++## [v0.0.0.0](https://github.com/freckle/aws-sns-verify/releases/tag/v0.0.0.0)++- Initial release.+- Handle JSON parsing.+- Handle validation of payload signatures via X509.+- Handle response to subscription messages.
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2021 Renaissance Learning Inc++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.
+ README.md view
@@ -0,0 +1,31 @@+# aws-sns-verify++Consumers utilizing SNS need to do 3 tasks:++1. Parse the message JSON+2. Validate signed signatures+3. Handle subscriptions++This library encapsulates those actions.++```hs+myEchoWebhook :: MonadHandler m => m ()+myEchoWebhook = do+  message <- verifySNSMessage =<< requireInsecureJsonBody+  logDebugN message+```++## Sign For Test++Signatures for testing are produced with the self signed certificate in this+repository.++```sh+cat unsigned.txt | openssl dgst -sha1 -sign tests/key.pem | openssl base64+```++The certificate was produced with++```sh+openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout tests/key.pem -out tests/cert.pem+```
+ aws-sns-verify.cabal view
@@ -0,0 +1,116 @@+cabal-version:      1.18+name:               aws-sns-verify+version:            0.0.0.0+license:            MIT+license-file:       LICENSE+copyright:          2022 Freckle By Renaissance+maintainer:         engineering@freckle.com+author:             Freckle+homepage:           https://github.com/freckle/aws-sns-verify#readme+bug-reports:        https://github.com/freckle/aws-sns-verify/issues+description:+    Please see the README on GitHub at <https://github.com/freckle/aws-sns-verify#readme>++category:+    Network, Web, AWS, Amazon, Cloud, Cryptography, Distributed-Computing++build-type:         Simple+extra-source-files:+    tests/cert.pem+    tests/key.pem++extra-doc-files:+    README.md+    CHANGELOG.md++source-repository head+    type:     git+    location: https://github.com/freckle/aws-sns-verify++library+    exposed-modules:+        Amazon.SNS.Verify+        Amazon.SNS.Verify.Payload+        Amazon.SNS.Verify.Validate++    hs-source-dirs:     library+    other-modules:      Amazon.SNS.Verify.Prelude+    default-language:   Haskell2010+    default-extensions:+        BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor+        DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies+        FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving+        LambdaCase MultiParamTypeClasses NoImplicitPrelude+        NoMonomorphismRestriction OverloadedStrings RankNTypes+        RecordWildCards ScopedTypeVariables StandaloneDeriving+        TypeApplications TypeFamilies++    ghc-options:+        -Weverything -Wno-missing-exported-signatures+        -Wno-missing-import-lists -Wno-missing-local-signatures+        -Wno-monomorphism-restriction -Wno-unsafe -Wno-safe+        -Wno-all-missed-specialisations++    build-depends:+        aeson >=1.3.1.1,+        base >=4.7 && <5,+        bytestring >=0.10.8.2,+        errors >=2.3.0,+        http-conduit >=2.3.2,+        memory >=0.14.18,+        pem >=0.2.4,+        text >=1.2.3.1,+        x509 >=1.7.5,+        x509-validation >=1.6.11++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=9.2.2)+        ghc-options: -Wno-missing-kind-signatures++test-suite aws-sns-verify-test+    type:               exitcode-stdio-1.0+    main-is:            Main.hs+    hs-source-dirs:     tests+    other-modules:+        Amazon.SNS.Verify.TestPrelude+        Amazon.SNS.Verify.ValidateSpec+        Amazon.SNS.VerifySpec+        Paths_aws_sns_verify++    default-language:   Haskell2010+    default-extensions:+        BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor+        DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies+        FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving+        LambdaCase MultiParamTypeClasses NoImplicitPrelude+        NoMonomorphismRestriction OverloadedStrings RankNTypes+        RecordWildCards ScopedTypeVariables StandaloneDeriving+        TypeApplications TypeFamilies++    ghc-options:+        -Weverything -Wno-missing-exported-signatures+        -Wno-missing-import-lists -Wno-missing-local-signatures+        -Wno-monomorphism-restriction -Wno-unsafe -Wno-safe+        -Wno-all-missed-specialisations -threaded -rtsopts -with-rtsopts=-N++    build-depends:+        aeson-qq >=0.8.2,+        async >=2.2.1,+        aws-sns-verify -any,+        base >=4.7 && <5,+        hspec >=2.5.5,+        http-types >=0.12.2,+        text >=1.2.3.1,+        wai >=3.2.1.2,+        warp >=3.2.25,+        x509-validation >=1.6.11++    if impl(ghc >=8.10)+        ghc-options:+            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module++    if impl(ghc >=9.2.2)+        ghc-options: -Wno-missing-kind-signatures
+ library/Amazon/SNS/Verify.hs view
@@ -0,0 +1,65 @@+module Amazon.SNS.Verify+  ( verifySNSMessage+  , verifySNSMessageEither+  , verifySNSMessageJSON+  , verifySNSMessageJSONEither+  , SNSNotificationValidationError(..)+  ) where++import Amazon.SNS.Verify.Prelude++import Amazon.SNS.Verify.Payload+import Amazon.SNS.Verify.Validate+import Control.Error (hoistEither, runExceptT)+import Data.Aeson (FromJSON, Value, eitherDecode)+import Data.Aeson.Types (Result(Error, Success), fromJSON)+import Data.Bifunctor (first)+import Data.ByteString.Lazy (fromStrict)+import Data.Text.Encoding (encodeUtf8)++-- | Decode and verify an SNS message as JSON+--+-- The same as 'verifySNSMessage', but decodes the message as `JSON`.+--+verifySNSMessageJSON :: (FromJSON a, MonadIO m) => Value -> m a+verifySNSMessageJSON = unTry id <=< verifySNSMessageJSONEither++verifySNSMessageJSONEither+  :: (FromJSON a, MonadIO m)+  => Value+  -> m (Either SNSNotificationValidationError a)+verifySNSMessageJSONEither value =+  join+    . traverse (first BadJSONParse . eitherDecode . fromStrict . encodeUtf8)+    <$> verifySNSMessageEither value++-- | Decode and verify an SNS message+--+-- This function follows the process outlined in the following:+--+-- <https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html>+--+-- A `JSON` payload is:+--+-- 1. Parsed as an SNS message type 'Notification', `SubscriptionConfirmation`,+--    or `UnsubscribeConfirmation`.+-- 2. Verified against its signature.+-- 3. And in the case of subscription events responded to.+--+verifySNSMessage :: MonadIO m => Value -> m Text+verifySNSMessage = unTry id <=< verifySNSMessageEither++verifySNSMessageEither+  :: MonadIO m => Value -> m (Either SNSNotificationValidationError Text)+verifySNSMessageEither value = runExceptT $ do+  payload <- hoistEither $ parseSNSPayload value+  verified <- hoistEither =<< validateSnsMessage payload+  hoistEither =<< handleSubscription verified++parseSNSPayload :: Value -> Either SNSNotificationValidationError SNSPayload+parseSNSPayload = first BadJSONParse . fromResult . fromJSON++fromResult :: Result a -> Either String a+fromResult = \case+  Success a -> Right a+  Error str -> Left str
+ library/Amazon/SNS/Verify/Payload.hs view
@@ -0,0 +1,81 @@+module Amazon.SNS.Verify.Payload+  ( SNSPayload(..)+  , SNSType(..)+  , SNSNotification(..)+  , SNSSubscription(..)+  ) where++import Amazon.SNS.Verify.Prelude++import Data.Aeson+  ( FromJSON+  , defaultOptions+  , fieldLabelModifier+  , genericParseJSON+  , parseJSON+  , withObject+  , (.:)+  )+import GHC.Generics (Generic)++data SNSPayload = SNSPayload+  { snsMessage :: Text+  , snsMessageId :: Text+  , snsTimestamp :: Text+  , snsTopicArn :: Text+  , snsType :: Text+  , snsSignatureVersion :: Text+  , snsSignature :: Text+  , snsSigningCertURL :: Text+  , snsTypePayload :: SNSType+  }++instance FromJSON SNSPayload where+  parseJSON v = parse v+   where+    parse = withObject "SNSPayload" $ \o -> do+      payloadType <- o .: "Type"+      SNSPayload+        <$> o+        .: "Message"+        <*> o+        .: "MessageId"+        <*> o+        .: "Timestamp"+        <*> o+        .: "TopicArn"+        <*> pure payloadType+        <*> o+        .: "SignatureVersion"+        <*> o+        .: "Signature"+        <*> o+        .: "SigningCertURL"+        <*> parseType payloadType+    parseType = \case+      "SubscriptionConfirmation" -> SubscriptionConfirmation <$> parseJSON v+      "UnsubscribeConfirmation" -> UnsubscribeConfirmation <$> parseJSON v+      "Notification" -> Notification <$> parseJSON v+      msg -> fail $ "Unknown message type " <> show msg++data SNSType+  = Notification SNSNotification+  | SubscriptionConfirmation SNSSubscription+  | UnsubscribeConfirmation SNSSubscription++newtype SNSNotification = SNSNotification+  { snsSubject :: Maybe Text+  }+  deriving stock (Show, Eq, Generic)++instance FromJSON SNSNotification where+  parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 3 }++data SNSSubscription = SNSSubscription+  { snsToken :: Text+  , snsSubscribeURL :: Text+  }+  deriving stock (Show, Eq, Generic)++instance FromJSON SNSSubscription where+  parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 3 }
+ library/Amazon/SNS/Verify/Prelude.hs view
@@ -0,0 +1,23 @@+module Amazon.SNS.Verify.Prelude+  ( module X+  , module Amazon.SNS.Verify.Prelude+  ) where++import Prelude as X++import Control.Exception as X (Exception)+import qualified Control.Exception+import Control.Monad as X (join, (<=<))+import Control.Monad.IO.Class as X (MonadIO, liftIO)+import Data.ByteString as X (ByteString)+import Data.Text as X (Text)+import Data.Traversable as X (for)++throwIO :: (MonadIO m, Exception e) => e -> m a+throwIO = liftIO . Control.Exception.throwIO++unTry :: (MonadIO m, Exception e) => (a -> e) -> Either a b -> m b+unTry e = either (throwIO . e) pure++fromMaybeM :: Monad m => m a -> Maybe a -> m a+fromMaybeM f = maybe f pure
+ library/Amazon/SNS/Verify/Validate.hs view
@@ -0,0 +1,128 @@+module Amazon.SNS.Verify.Validate+  ( validateSnsMessage+  , handleSubscription+  , SNSNotificationValidationError(..)+  , ValidSNSMessage(..)+  ) where++import Amazon.SNS.Verify.Prelude++import Amazon.SNS.Verify.Payload+import Control.Error (ExceptT, catMaybes, headMay, runExceptT, throwE)+import Control.Monad (when)+import Data.ByteArray.Encoding (Base(Base64), convertFromBase)+import Data.PEM (pemContent, pemParseLBS)+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8)+import Data.X509+  ( HashALG(..)+  , PubKeyALG(..)+  , SignatureALG(..)+  , SignedCertificate+  , certPubKey+  , decodeSignedCertificate+  , getCertificate+  )+import Data.X509.Validation+  (SignatureFailure, SignatureVerification(..), verifySignature)+import Network.HTTP.Simple+  (getResponseBody, getResponseStatusCode, httpLbs, parseRequest_)++data ValidSNSMessage+  = SNSMessage Text+  | SNSSubscribe SNSSubscription+  | SNSUnsubscribe SNSSubscription+  deriving stock (Show, Eq)++-- | Validate SNS notification+--+-- SNS messages are validated through their signature. The algorithm is detailed+-- in the documentation below.+--+-- <https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html>+--+validateSnsMessage+  :: MonadIO m+  => SNSPayload+  -> m (Either SNSNotificationValidationError ValidSNSMessage)+validateSnsMessage payload@SNSPayload {..} = runExceptT $ do+  signature <- unTry BadSignature $ convertFromBase Base64 $ encodeUtf8+    snsSignature+  signedCert <- retrieveCertificate payload+  let+    valid = verifySignature+      (SignatureALG HashSHA1 PubKeyALG_RSA)+      (certPubKey $ getCertificate signedCert)+      (unsignedSignature payload)+      signature+  case valid of+    SignaturePass -> pure $ case snsTypePayload of+      Notification{} -> SNSMessage snsMessage+      SubscriptionConfirmation x -> SNSSubscribe x+      UnsubscribeConfirmation x -> SNSUnsubscribe x+    SignatureFailed e -> throwE $ InvalidPayload e++retrieveCertificate+  :: MonadIO m+  => SNSPayload+  -> ExceptT SNSNotificationValidationError m SignedCertificate+retrieveCertificate SNSPayload {..} = do+  response <- httpLbs $ parseRequest_ $ T.unpack snsSigningCertURL+  pems <- unTry BadPem $ pemParseLBS $ getResponseBody response+  cert <-+    fromMaybeM (throwE $ BadPem "Empty List") $ pemContent <$> headMay pems+  unTry BadCert $ decodeSignedCertificate cert++unsignedSignature :: SNSPayload -> ByteString+unsignedSignature SNSPayload {..} =+  encodeUtf8 $ mconcat $ (<> "\n") <$> catMaybes+    [ Just "Message"+    , Just snsMessage+    , Just "MessageId"+    , Just snsMessageId+    , "SubscrieURL" <$ mSubscribeUrl+    , mSubscribeUrl+    , "Subject" <$ mSubject+    , mSubject+    , Just "Timestamp"+    , Just snsTimestamp+    , "Token" <$ mToken+    , mToken+    , Just "TopicArn"+    , Just snsTopicArn+    , Just "Type"+    , Just snsType+    ]+ where+  (mSubject, mToken, mSubscribeUrl) = case snsTypePayload of+    Notification x -> (snsSubject x, Nothing, Nothing)+    SubscriptionConfirmation x ->+      (Nothing, Just $ snsToken x, Just $ snsSubscribeURL x)+    UnsubscribeConfirmation x ->+      (Nothing, Just $ snsToken x, Just $ snsSubscribeURL x)++handleSubscription+  :: MonadIO m+  => ValidSNSMessage+  -> m (Either SNSNotificationValidationError Text)+handleSubscription = runExceptT . \case+  SNSMessage t -> pure t+  SNSSubscribe SNSSubscription {..} -> do+    response <- httpLbs $ parseRequest_ $ T.unpack snsSubscribeURL+    when (getResponseStatusCode response >= 300) $ do+      throwE $ BadSubscription ()+    throwE SubscribeMessageResponded+  SNSUnsubscribe{} -> throwE UnsubscribeMessage++data SNSNotificationValidationError+  = BadPem String+  | BadSignature String+  | BadCert String+  | BadJSONParse String+  | BadSubscription ()+  | InvalidPayload SignatureFailure+  | MissingMessageTypeHeader+  | UnsubscribeMessage+  | SubscribeMessageResponded+  deriving stock (Show, Eq)+  deriving anyclass Exception
+ tests/Amazon/SNS/Verify/TestPrelude.hs view
@@ -0,0 +1,29 @@+module Amazon.SNS.Verify.TestPrelude+  ( module X+  , module Amazon.SNS.Verify.TestPrelude+  ) where++import Prelude as X++import Control.Concurrent.Async+import Control.Concurrent.MVar+import Data.Text as X (Text)+import Network.HTTP.Types.Status+import Network.Wai+import Network.Wai.Handler.Warp+import Test.Hspec as X++useCertServer :: IO () -> IO ()+useCertServer action = do+  (setReady, whenReady) <- initReadyState+  race_ (whenReady action)+    $ runSettings (setBeforeMainLoop setReady . setPort 3000 $ defaultSettings)+    $ \_req send -> send $ responseFile+        ok200+        [("Content-Type", "text/plain")]+        "./tests/cert.pem"+        Nothing+ where+  initReadyState = do+    ready <- newEmptyMVar+    return (putMVar ready (), withMVar ready . const)
+ tests/Amazon/SNS/Verify/ValidateSpec.hs view
@@ -0,0 +1,123 @@+module Amazon.SNS.Verify.ValidateSpec+  ( spec+  ) where++import Amazon.SNS.Verify.TestPrelude++import Amazon.SNS.Verify.Payload+import Amazon.SNS.Verify.Validate+import Data.X509.Validation (SignatureFailure(..))++spec :: Spec+spec = around_ useCertServer $ do+  describe "validateSnsMessage" $ do+    it "successfully validates an SNS notification" $ do+      let message = "Some message"+      x <- validateSnsMessage $ SNSPayload+        { snsMessage = message+        , snsMessageId = "78d4d7a0-a3eb-5c4d-834f-8d5fa9813ab6"+        , snsTimestamp = "2022-05-18T14:52:26.952Z"+        , snsTopicArn = "arn:aws:sns:us-west-2:123456789012:MyTopic"+        , snsType = "Notification"+        , snsSignatureVersion = "1"+        , snsSignature =+          "Dg24trcOUiLjclt5JwyJS0JEOnEEbi6P30XS6KBxMCwzZ08a04UwjaFTW9Ae8xurhBS5YESz1fY28vTwvEmxh/20WmB3bWIDOMp9v5RI8XSZOvpMm+hdQJ43VqGhEDyAvRU6iCDLihDlZNc/sBCwl9X0H4kh/8vIElRif9gFBbYI94ZHGgqEV+Zc3gVKo9Udrl/MxNvMVadsO/+/oPVUeWibQr3xfGK95oc/ocuNAgi0MOxZmLVnibHu36KOTSvy2qSLonnRRFcbaauYZJ4js7oTq+1ujXNO72oPLaeG3pVJ2grqMc5z8tKQxFnSTE3es7wQarU/CLrbO8j0isbnWw=="+        , snsSigningCertURL = cert+        , snsTypePayload = Notification $ SNSNotification+          { snsSubject =+            Just+              "SynthesisTaskNotification { TaskId: 680a1f1b-f3ae-4474-aa8f-3b6dfe52e656, Status: COMPLETED }"+          }+        }+      x `shouldBe` Right (SNSMessage message)++    it "fails to validate a currupt SNS notification" $ do+      let+        go = validateSnsMessage $ SNSPayload+          { snsMessage = "Some message"+          , snsMessageId = "corrupt"+          , snsTimestamp = "2022-05-18T14:52:26.952Z"+          , snsTopicArn = "arn:aws:sns:us-west-2:123456789012:MyTopic"+          , snsType = "Notification"+          , snsSignatureVersion = "1"+          , snsSignature =+            "Dg24trcOUiLjclt5JwyJS0JEOnEEbi6P30XS6KBxMCwzZ08a04UwjaFTW9Ae8xurhBS5YESz1fY28vTwvEmxh/20WmB3bWIDOMp9v5RI8XSZOvpMm+hdQJ43VqGhEDyAvRU6iCDLihDlZNc/sBCwl9X0H4kh/8vIElRif9gFBbYI94ZHGgqEV+Zc3gVKo9Udrl/MxNvMVadsO/+/oPVUeWibQr3xfGK95oc/ocuNAgi0MOxZmLVnibHu36KOTSvy2qSLonnRRFcbaauYZJ4js7oTq+1ujXNO72oPLaeG3pVJ2grqMc5z8tKQxFnSTE3es7wQarU/CLrbO8j0isbnWw=="+          , snsSigningCertURL = cert+          , snsTypePayload = Notification $ SNSNotification+            { snsSubject =+              Just+                "SynthesisTaskNotification { TaskId: 680a1f1b-f3ae-4474-aa8f-3b6dfe52e656, Status: COMPLETED }"+            }+          }+      go `shouldReturn` Left (InvalidPayload SignatureInvalid)++    it "fails to validate a bad PEM" $ do+      let+        go = validateSnsMessage $ SNSPayload+          { snsMessage = "Some message"+          , snsMessageId = "corrupt"+          , snsTimestamp = "2022-05-18T14:52:26.952Z"+          , snsTopicArn = "arn:aws:sns:us-west-2:123456789012:MyTopic"+          , snsType = "Notification"+          , snsSignatureVersion = "1"+          , snsSignature =+            "Dg24trcOUiLjclt5JwyJS0JEOnEEbi6P30XS6KBxMCwzZ08a04UwjaFTW9Ae8xurhBS5YESz1fY28vTwvEmxh/20WmB3bWIDOMp9v5RI8XSZOvpMm+hdQJ43VqGhEDyAvRU6iCDLihDlZNc/sBCwl9X0H4kh/8vIElRif9gFBbYI94ZHGgqEV+Zc3gVKo9Udrl/MxNvMVadsO/+/oPVUeWibQr3xfGK95oc/ocuNAgi0MOxZmLVnibHu36KOTSvy2qSLonnRRFcbaauYZJ4js7oTq+1ujXNO72oPLaeG3pVJ2grqMc5z8tKQxFnSTE3es7wQarU/CLrbO8j0isbnWw=="+          , snsSigningCertURL = "https://freckle.com/404.pem"+          , snsTypePayload = Notification $ SNSNotification+            { snsSubject =+              Just+                "SynthesisTaskNotification { TaskId: 680a1f1b-f3ae-4474-aa8f-3b6dfe52e656, Status: COMPLETED }"+            }+          }+      go `shouldReturn` Left (BadPem "Empty List")++    it "successfully validates an SNS subscription" $ do+      -- pendingWith "need valid subscription payload"+      let+        message+          = "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message."+        subscription = SNSSubscription+          { snsToken = "2336412f37..."+          , snsSubscribeURL =+            "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37..."+          }+      x <- validateSnsMessage $ SNSPayload+        { snsMessage = message+        , snsMessageId = "165545c9-2a5c-472c-8df2-7ff2be2b3b1b"+        , snsTimestamp = "2012-04-26T20:45:04.751Z"+        , snsTopicArn = "arn:aws:sns:us-west-2:123456789012:MyTopic"+        , snsType = "SubscriptionConfirmation"+        , snsSignatureVersion = "1"+        , snsSignature =+          "ZsPD3cDAHPU0mseSCKSQX8Ka2vT8lmaTHN6GbfCwmtfGFblWveBDUMWbSvhvDL4A6RujW8UfEpCytC2vwbMM2GUreTRpelK06t19nxeTHAnC5+JYBUpmlgauT92g3sLGYDWIUxAbD5u7ZY5QDfWaOpo8OD+xFy0sAB51SSDjTsXbt+gh/S6FbZQ1l/sjyBlDeJwxM/qpLn4xfQKO/Ev6lpV3ioBT8/Q7f1o4rzloYXxu1QnpDUx6L5uZB23lGEI4dcjIixi58WFwkz0SHgR9MWYIKJa2vZO2P8jFG63FGOhNaHbp3e/J6Tl6XnJnc+vriGle2dwhQjuWMDSdi92+kg=="+        , snsSigningCertURL = cert+        , snsTypePayload = SubscriptionConfirmation subscription+        }+      x `shouldBe` Right (SNSSubscribe subscription)++    it "successfully validates an SNS unsubscribe" $ do+      -- pendingWith "need valid subscription payload"+      let+        message+          = "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message."+        subscription = SNSSubscription+          { snsToken = "2336412f37..."+          , snsSubscribeURL =+            "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37..."+          }+      x <- validateSnsMessage $ SNSPayload+        { snsMessage = message+        , snsMessageId = "165545c9-2a5c-472c-8df2-7ff2be2b3b1b"+        , snsTimestamp = "2012-04-26T20:45:04.751Z"+        , snsTopicArn = "arn:aws:sns:us-west-2:123456789012:MyTopic"+        , snsType = "UnsubscribeConfirmation"+        , snsSignatureVersion = "1"+        , snsSignature =+          "DqsnMTKKAW65yGEjqGGmHzw660tq9lB0yK3GVznVCINDnzrrq+mvScrHSJQ17+tT+u1aSL8nFvyzC4xdn4j3b1TNSXJdqj1ivvMVpVDbvl8Wvf/+JQ/ac6MPdQMkq0RitX5pzod3bIekY9ZXejLoEP73Q5zcLbTGRnC7G4li5W5ZG9i81zfbM1PgqmkUQnPC49Cqm/KMvBok8PfVBzWfEFLcnSsbm7JJf6y5Y7j+onDLTlbjwdRhej+MUJrrtsXbkARUg6EZAFcbhULj/3EYhvwdDClL4h0OxxhwWRnofTONIZhLB7bAkrbgsikkyhZ49zG8PjD6+TLX3YPaNB5eFw=="+        , snsSigningCertURL = cert+        , snsTypePayload = UnsubscribeConfirmation subscription+        }+      x `shouldBe` Right (SNSUnsubscribe subscription)++cert :: Text+cert = "http://localhost:3000"
+ tests/Amazon/SNS/VerifySpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE QuasiQuotes #-}++module Amazon.SNS.VerifySpec+  ( spec+  ) where++import Amazon.SNS.Verify.TestPrelude++import Amazon.SNS.Verify+import Data.Aeson.QQ++spec :: Spec+spec = around_ useCertServer $ do+  describe "verifySNSMessage" $ do+    it "successfully validates an SNS notification" $ do+      let+        payload = [aesonQQ|+          { Message: "Some message"+          , MessageId: "78d4d7a0-a3eb-5c4d-834f-8d5fa9813ab6"+          , Timestamp: "2022-05-18T14:52:26.952Z"+          , TopicArn: "arn:aws:sns:us-west-2:123456789012:MyTopic"+          , Type: "Notification"+          , Subject: "SynthesisTaskNotification { TaskId: 680a1f1b-f3ae-4474-aa8f-3b6dfe52e656, Status: COMPLETED }"+          , SignatureVersion: "1"+          , Signature: "Dg24trcOUiLjclt5JwyJS0JEOnEEbi6P30XS6KBxMCwzZ08a04UwjaFTW9Ae8xurhBS5YESz1fY28vTwvEmxh/20WmB3bWIDOMp9v5RI8XSZOvpMm+hdQJ43VqGhEDyAvRU6iCDLihDlZNc/sBCwl9X0H4kh/8vIElRif9gFBbYI94ZHGgqEV+Zc3gVKo9Udrl/MxNvMVadsO/+/oPVUeWibQr3xfGK95oc/ocuNAgi0MOxZmLVnibHu36KOTSvy2qSLonnRRFcbaauYZJ4js7oTq+1ujXNO72oPLaeG3pVJ2grqMc5z8tKQxFnSTE3es7wQarU/CLrbO8j0isbnWw=="+          , SigningCertURL: "http://localhost:3000"+          }+          |]+      x <- verifySNSMessage payload++      x `shouldBe` "Some message"++    it "successfully confirms a subscription" $ do+      let+        payload = [aesonQQ|+          { Message: "Some message"+          , MessageId: "78d4d7a0-a3eb-5c4d-834f-8d5fa9813ab6"+          , Timestamp: "2022-05-18T14:52:26.952Z"+          , Token: "test"+          , TopicArn: "arn:aws:sns:us-west-2:123456789012:MyTopic"+          , Type: "SubscriptionConfirmation"+          , SignatureVersion: "1"+          , SubscribeURL: "http://localhost:3000"++          , Signature: "fHExOb2eyGY3mCISjk72nTLRomG9kB+I8c01Jj8iAEUWG2De3QrdRciMXrlP/zEJEbCVDN1lzO91rMu02ng0Akj1s3tN8lRK1QgCRt/35ob9QG9j2xXMowbdEmibj03rILQAriXk1H+pvftBvr9imz+eZID8TZfpjKkVJtn5pvs7LjpOQaTLgZmDIVtsz+04sTQscaETy00kLbqmfknqd6zGmvB7ub/HDhjF3xojBLE+1HCUCgQoS6FAEs/ivbcTdXpqs8L5trStC1So1w/xlTkaGnDmcLpVtj7qk2HBnggJhw4ay8dF4LRpipc5w254dghIeY0YlwflW4l1aZHb3w=="+          , SigningCertURL: "http://localhost:3000"+          }+          |]++      verifySNSMessage payload+        `shouldThrow` (\case+                        SubscribeMessageResponded -> True+                        _ -> False+                      )
+ tests/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Main -Wno-missing-export-lists #-}
+ tests/cert.pem view
@@ -0,0 +1,21 @@+-----BEGIN CERTIFICATE-----+MIIDazCCAlOgAwIBAgIUR/7v0Z57wq8BaU90+7lqR+MByIwwDQYJKoZIhvcNAQEL+BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA1MzEyMjUxMzhaFw0zMjA1+MjgyMjUxMzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw+HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB+AQUAA4IBDwAwggEKAoIBAQCylpwWwMURNfanuv4tIYMJ+1pypf6Nk9dCbYLpwFPt+SYzeeU+bNPB5y1a5fCaHNT7foIU/WuiJagyarqa0PLotAjWViMdrfGfy3EgUkLQU+SOEozOLV1WMXGoBh1w9vGI8RaRWiD/3FY970J/JCP4aiXFb22o12JBeUihgANnSB+qUhjFi2HdmosR40CSUu/X5gQVGyJ9DEdDZue/hoqsZPQJoqWxtEoQ3c/KPmvhKOW+N1XMUUAxXXlAUVx6T7+IJ9L2+aRGR6HtKhyMXYJL0iGpR84qC0MxZnMg1qwcs0a7+aru4ung7SRyBHWcfAQaWhszqqh9Aljlq5oZDUgscSpJPAgMBAAGjUzBRMB0GA1Ud+DgQWBBSUaXOnX7TA8jpLIb3GmrevUHJA7zAfBgNVHSMEGDAWgBSUaXOnX7TA8jpL+Ib3GmrevUHJA7zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAD+IVRm5kOFbmQwM3NcuCVhkxb+Tarx/ySoe6cT1+TtBNBKhg2inA6BxrZH8zxOaQ3i+6TpXLNZPxngFXNWBV3pQSL4J9TJClSNVcsVwQco+oUUDobzqSgnAi5h65AWmki63+HHIZK9M7kpMmK4rB4JBpbyKdCdr/4r2zNrla+he+RwROkTSk2T5Cea4fTEfKgre5+RoODmS5aytJZNJjB1Tn1Zges+mPzrLJ12vczEI/tgJb5q57hZ9OKFTCfhm169flA+sl56EFRnvjLykfZ8HibqA2dbAwTGR/Yy0JpOCBmAiaGf2Wa8xf/ZXybdtyCHa5eH+tch7wpVBtacV6tK5GmO4+-----END CERTIFICATE-----
+ tests/key.pem view
@@ -0,0 +1,28 @@+-----BEGIN PRIVATE KEY-----+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCylpwWwMURNfan+uv4tIYMJ+1pypf6Nk9dCbYLpwFPtSYzeeU+bNPB5y1a5fCaHNT7foIU/WuiJagya+rqa0PLotAjWViMdrfGfy3EgUkLQUSOEozOLV1WMXGoBh1w9vGI8RaRWiD/3FY970+J/JCP4aiXFb22o12JBeUihgANnSBqUhjFi2HdmosR40CSUu/X5gQVGyJ9DEdDZue+/hoqsZPQJoqWxtEoQ3c/KPmvhKOWN1XMUUAxXXlAUVx6T7+IJ9L2+aRGR6HtKhyM+XYJL0iGpR84qC0MxZnMg1qwcs0a7aru4ung7SRyBHWcfAQaWhszqqh9Aljlq5oZD+UgscSpJPAgMBAAECggEBAJCyjw2wavui6ZLFY6RVfsLbsVErRbCYlNTxvhx/BIFC+Eq0/RUzP+/fy1PW8rcrnTWYLvWqz0a1lC4lY8SEgAwjsvVXHWSyxw9d2u8BN7lpx+Il3YhAIVvw+9HUAo7AxrrZ/BsRF6O3dvj/nrU9JxfjiW8K9aLTwHykNEEGY7uPJE+Ri/IvQtwb8zxe2qi2jx4WpBoA2j0W5IhXUYy7IGeeiK1oIQX8yVrbX6i8IzW86gO+9I5zjbBCy3IPzG8m2JkmFVIqHR7+isDt/NHRdcT5o/jXRMjIJlJMsoXoAv34pt6d+rBq4+oqU7rqBLGMCV0Gkb9Npa0So0FRL2gnfVRTtlOECgYEA7hZL0jqeTGHh2Ag5+knFo/9XZBlbJpLhSkpTonjMJKWoW0VcQh8CFUzRXeAYJk4PfQ15wHGQm2p/SfDZB+J7dvlVf7FyJtVaIvYYW4E+rDGN+NvlUFx2uTvxErtIWNLKl+4LX1x2wm8amW4kax+LQO5ZmB2zXiuLJBhQEhXwimYcPMCgYEAwAZUnx9YYWAsPN+I01KKWyuj0fTNOpPC+u46RC6Ii+A5RlV8ksYPT4Sd0HcAzOeKLdj0DKCQ426hn5XVZ8DIS06b7FWXnP6oH+S6jf50LXzKDKLP1l/J582GtBobAzcUv28z/WMdy7QnfMX0wj1QDNRxtn4C+ZHVEt+K82dPi+kEDUCgYB48vCvqsGyEeOWSI9euqhZlzx6AOrM+CpSdr05aLnVxSQ0UqN++IypX23Y37fE9dDeeVMJlWed3Fsvp9/HfiEtyNdDDruSWJR8mzrR1wcQ8xYyPTUfz+R841uTZXraTLNIvVAfsKxQF/sOSKrNjjAkpUHNsdhJoO34XYDygt5c2gSwKBgBC6+PHLkGyHYa+IdouCtxwmsIoguqBYbOxJ1l+MIi71KGgdGE383OaTad/4DeTLorjdt+lzHO/fj8kZhmgaAv7H3thN/XUa61FA+eNWsC4qObuiDzLgg0kjGUHlKEc+bBtFB6+ViFf1hwzLWV7z39Fo5w3IfRgkRkBskqnjbQWGFelAoGBAKafavC9fgeDZDKPX1aP+kSS458nC+uigVbeuxeQPVPVVKXt1jGA/xF3z+ZCx/JBMX+I06pkpRkNMGufx6qWi+E391Zm2CowQ4uQWELRIyE2tb+aRS/WnfIZoJsvBvz6DfHxURJjr80XmB3AOQWYF++KAH5JbFlG4GXUIb3WELjdjdj+-----END PRIVATE KEY-----