How to Verify AWS SNS Notifications in Haskell
by @eborden on June 03, 2022
AWS SNS topics are a standard method for offloading async work in the AWS ecosystem. SDKs for Java, PHP, etc. exist. These allow consumers to verify the authenticity of SNS notifications when sending them over HTTPS. While the venerable Amazonka exists, it does not handle this use case. However, we can!
The Notifications
An SNS notification is a simple JSON payload. It includes a signature and a URL to an X509 public key, along with other details.
{
"Signature": "Nzk0MzdGNUVEREExM0Y5QzA2NjlCOTc4REQ3QTkwNjZERDIwNTlGMQ==",
"SigningCertURL": "https://example.com/cert.pem",
...
}
Parsing these messages is an easy task, but machinery for verifying their X509 signatures requires a bit more of a dive.
Can I X509?
We are in luck. The Haskell ecosystem has all the libraries we need to verify these signatures.
Getting the Certificate
⚠️ Partial functions,
fromRight
andhead
, used here for brevity.
We need to fetch the certificate from our JSON payload. First a few imports from our friendly ecosystem.
import Network.HTTP.Simple (getResponseBody, httpLbs, parseRequest_)
import Data.PEM (pemContent, pemParseLBS)
import Data.X509 (decodeSignedCertificate, SignedCertificate)
Next we leverage those libraries to fetch the PEM,
response <- httpLbs $ parseRequest_ snsSigningCertURL
parse its contents,
let pems = fromRight $ pemParseLBS $ getResponseBody response
extract a certificate,
let cert = pemContent $ head pems
and decode it into a SignedCertificate
.
decodeSignedCertificate cert
Pulling it all together:
retrieveCertificate :: MonadIO m => SNSPayload -> m SignedCertificate
retrieveCertificate SNSPayload {snsSigningCertURL} = do
response <- httpLbs $ parseRequest_ snsSigningCertURL
let
pems = fromRight $ pemParseLBS $ getResponseBody response
cert = pemContent $ head pems
pure $ fromRight $ decodeSignedCertificate cert
Validating The Signature
Once we have a SignedCertificate
, we can calculate the unsigned signature and
verify it against the signature we recieved from AWS.
Again, a few imports we’ll need.
import Data.ByteArray.Encoding (Base(Base64), convertFromBase)
import Data.PEM (pemContent, pemParseLBS)
import Data.Text.Encoding (encodeUtf8)
import Data.X509
( HashALG(HashSHA1)
, PubKeyALG(PubKeyALG_RSA)
, SignatureALG(SignatureALG)
, certPubKey
, getCertificate
)
import Data.X509.Validation (SignatureVerification, verifySignature)
We must convert the signature from base64
encoding,
let signature = fromRight $ convertFromBase Base64 $ encodeUtf8 snsSignature
and extract a public key from our signed certificate.
let publicKey = certPubKey $ getCertificate signedCert
Then we can compare the signed and unsigned signatures using the RSA
encryption and SHA1
hashing AWS uses to produce the public and private keys.
let algorithm = SignatureALG HashSHA1 PubKeyALG_RSA
in verifySignature
algorithm
publicKey
(unsignedSignature payload)
signature
Bringing it all together, we’ve leveraged the robust Haskell ecosystem to do a huge amount of persnickity work for us.
validateSnsMessage :: MonadIO m => SNSPayload -> m SignatureVerification
validateSnsMessage payload@SNSPayload {..} = do
signedCert <- retrieveCertificate payload
let
signature = fromRight $ convertFromBase Base64 $ encodeUtf8 snsSignature
publicKey = certPubKey $ getCertificate signedCert
algorithm = SignatureALG HashSHA1 PubKeyALG_RSA
pure $ verifySignature
algorithm
publicKey
(unsignedSignature payload)
signature
unsignedSignature :: SNSPayload -> ByteString
unsignedSignature SNSPayload{..} =
encodeUtf8 $ mconcat [snsMessage {- etc -}]
But There’s More
The better news is you don’t have to implement this yourself. We have open
sourced our AWS SNS handling in the aws-sns-verify
package. This package includes parsing, verification, and
subscription handling. Everything you need to quickly setup an SNS webhook.
Enjoy!
import Amazon.SNS.Verify (verifySNSMessage)
handler :: Handler ()
handler = do
message <- verifySNSMessage =<< requireInsecureJsonBody
logDebugN message