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 +8/−0
- LICENSE +21/−0
- README.md +31/−0
- aws-sns-verify.cabal +116/−0
- library/Amazon/SNS/Verify.hs +65/−0
- library/Amazon/SNS/Verify/Payload.hs +81/−0
- library/Amazon/SNS/Verify/Prelude.hs +23/−0
- library/Amazon/SNS/Verify/Validate.hs +128/−0
- tests/Amazon/SNS/Verify/TestPrelude.hs +29/−0
- tests/Amazon/SNS/Verify/ValidateSpec.hs +123/−0
- tests/Amazon/SNS/VerifySpec.hs +54/−0
- tests/Main.hs +1/−0
- tests/cert.pem +21/−0
- tests/key.pem +28/−0
+ 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-----