メール配信プラットフォームであるSendGridは、メールの到達状況などを、呼び出し元のアプリケーションにWebHookで伝えることができる。
アプリケーションがSendGridからリクエストを受け取るには、当然インターネットからアクセスできるようにする必要があるが、何の考えもなしに開放してはセキュリティ的に不安だ。しかしご安心を、SendGridにはWebHookにデジタル署名をつける機能がある。本稿ではその使い方を紹介する。
まず、Mail Settings
から、Signed Event Webhook Requests
という項目に入る。画面のボタンをクリックすると、Verification Keyが生成され、電子署名が有効になる。これは我々(HERP)のシステムで実際に運用している鍵である。
この設定をすると、以後のWebHookには、X-Twilio-Email-Event-Webhook-Signature
とX-Twilio-Email-Event-Webhook-Timestamp
の二つのHTTPヘッダーが付与される。アプリケーション側はこれらを使って検証すればよい。
まず、電子署名に使うアルゴリズム、ハッシュ関数、メッセージ、署名、公開鍵を整理する。
- アルゴリズム: ECDSA(有限体上の楕円曲線がなす群構造を用いた電子署名アルゴリズム)
- ハッシュ関数: SHA256
- メッセージ:
X-Twilio-Email-Event-Webhook-Timestamp
の値と、リクエストボディを結合したもの - 署名:
X-Twilio-Email-Event-Webhook-Signature
- 公開鍵:
Verification Key
- 秘密鍵: SendGridが保有する
関係を整理したところで、公式のドキュメントを参考に実装を進めていく。docs.sendgrid.com
まずは必要なモジュールをインポートしていく。ずらずらと並べられた文は、もはやHaskellの風物詩である。
import "asn1-encoding" Data.ASN1.BinaryEncoding (BER(..)) import "asn1-encoding" Data.ASN1.Encoding (decodeASN1) import "asn1-types" Data.ASN1.Types (ASN1(..), fromASN1) import "base" Data.Proxy import "base" Data.Bifunctor (bimap, first) import "base64" Data.ByteString.Base64 (decodeBase64) import "bytestring" Data.ByteString qualified as B import "bytestring" Data.ByteString.Lazy qualified as BL import "cryptonite" Crypto.ECC (Curve_P256R1) import "cryptonite" Crypto.Error (CryptoFailable(..)) import "cryptonite" Crypto.Hash.Algorithms (SHA256(..)) import "cryptonite" Crypto.PubKey.ECC.Types (CurveName(..)) import "cryptonite" Crypto.PubKey.ECDSA qualified as ECDSA import "x509" Data.X509 (PubKey(..), PubKeyEC(..), SerializedPoint(..))
公開鍵がBase64とASN.1でエンコードされていることはすぐにわかるが、その中身のフォーマットがドキュメントに書かれていなかったため、少々迷いが生じた。実験してみたところx509パッケージのPubKey
としてデコードできることがわかった。
type PublicKey = ECDSA.PublicKey Curve_P256R1 curve :: Proxy Curve_P256R1 curve = Proxy parsePublicKey :: B.ByteString -> Either String PublicKey parsePublicKey b64 = do -- base64としてデコード raw <- first show $ decodeBase64 b64 -- ASN.1としてデコード asn1 <- first show $ decodeASN1 BER $ BL.fromStrict raw -- 楕円曲線のパラメータのプリセットと、公開鍵を取り出す pubkey <- bimap show fst $ fromASN1 asn1 -- secp256r1と仮定する SerializedPoint point <- case pubkey of PubKeyEC (PubKeyEC_Named SEC_p256r1 bs) -> Right bs _ -> Left $ "Unsupported public key: " <> show pubkey -- 座標をデコード case ECDSA.decodePublic curve point of CryptoPassed a -> Right a CryptoFailed err -> Left $ show err
ここまで来れば、あとは関数を呼び出すだけなので簡単だ。暗号や電子署名は、実装を間違えればただのランダムな文字列になってしまうため、ライブラリのありがたみを感じやすい。
verify :: PublicKey -> B.ByteString -> B.ByteString -> B.ByteString -> Either String () verify pubKey timestamp payload signatureBase64 = do -- 署名をデコードする signature <- either (const $ Left "Malformed signature") pure $ decodeBase64 signatureBase64 -- ECDSAの署名(r, s)を取り出す point <- case decodeASN1 BER $ BL.fromStrict signature of Right [_, IntVal r, IntVal s, _] -> pure (r, s) Right asn1 -> Left $ "failed to decode the signature: " <> show asn1 Left err -> Left $ show err case ECDSA.signatureFromIntegers curve point of CryptoPassed sig | ECDSA.verify -- 検証する curve SHA256 pubKey sig $ timestamp <> payload -> pure () | otherwise -> Left "Verification failed" CryptoFailed _ -> Left "ECDSA.signatureFromIntegers failed"
WebHookの設定画面で「Test Your Integration」をクリックすると送られてくるデータを使って、実際に検証してみる。
_test_verify :: Either String () _test_verify = do pub <- parsePublicKey "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERYcga9cTuvv0EbOFM0PO/KJjCgqYwtGar22uUyPQPwUbm+OtKXGNGIaHBvkgXBCbTxG4XQ4ddfDPgfMAcguUtg==" verify pub "1655455728" "[{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"processed\",\"category\":[\"cat facts\"],\"sg_event_id\":\"cPeOfuc93o3vAEftXKt_zA==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"deferred\",\"category\":[\"cat facts\"],\"sg_event_id\":\"zFYYFn__N8lr3be4TqKnVw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"response\":\"400 try again later\",\"attempt\":\"5\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"delivered\",\"category\":[\"cat facts\"],\"sg_event_id\":\"rj8WvpmTWgE0z2MSd41KKg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"response\":\"250 OK\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"open\",\"category\":[\"cat facts\"],\"sg_event_id\":\"uz1AaVpvYihASovU-M-Jrg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"click\",\"category\":[\"cat facts\"],\"sg_event_id\":\"O2bZwfxc-xm-9zmeVZX8HA==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"bounce\",\"category\":[\"cat facts\"],\"sg_event_id\":\"vMLN27M1-TVv5XADvpW3Nw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"reason\":\"500 unknown recipient\",\"status\":\"5.0.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"dropped\",\"category\":[\"cat facts\"],\"sg_event_id\":\"u1u9xdYlTkoDkUZFZ_j_tg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"reason\":\"Bounced Address\",\"status\":\"5.0.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"spamreport\",\"category\":[\"cat facts\"],\"sg_event_id\":\"CjkStOi15hPQfWO58rH9dw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"unsubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"QrCQiwv2pfkejKE5Zi1D0g==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"group_unsubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"l67dvY6MIupGpuk1r9vIXw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\",\"asm_group_id\":10},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"group_resubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"fo7PLpVCoCA9WDHovGQyyw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\",\"asm_group_id\":10}]\r\n" "MEUCIQCBYJiC1zzZeM61EbekWSGMFgpRSzaQSA4zwV3vlMgf/wIgSrMZIIYTnx4dkqDK92re4WYhcM3xEKbLIKfmcu7Et0o=" _test_verify_fail :: Either String () _test_verify_fail = do pub <- parsePublicKey "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERYcga9cTuvv0EbOFM0PO/KJjCgqYwtGar22uUyPQPwUbm+OtKXGNGIaHBvkgXBCbTxG4XQ4ddfDPgfMAcguUtg==" verify pub "1655455729" "[{\"email\":\"bob@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"processed\",\"category\":[\"cat facts\"],\"sg_event_id\":\"cPeOfuc93o3vAEftXKt_zA==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"deferred\",\"category\":[\"cat facts\"],\"sg_event_id\":\"zFYYFn__N8lr3be4TqKnVw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"response\":\"400 try again later\",\"attempt\":\"5\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"delivered\",\"category\":[\"cat facts\"],\"sg_event_id\":\"rj8WvpmTWgE0z2MSd41KKg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"response\":\"250 OK\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"open\",\"category\":[\"cat facts\"],\"sg_event_id\":\"uz1AaVpvYihASovU-M-Jrg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"click\",\"category\":[\"cat facts\"],\"sg_event_id\":\"O2bZwfxc-xm-9zmeVZX8HA==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"bounce\",\"category\":[\"cat facts\"],\"sg_event_id\":\"vMLN27M1-TVv5XADvpW3Nw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"reason\":\"500 unknown recipient\",\"status\":\"5.0.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"dropped\",\"category\":[\"cat facts\"],\"sg_event_id\":\"u1u9xdYlTkoDkUZFZ_j_tg==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"reason\":\"Bounced Address\",\"status\":\"5.0.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"spamreport\",\"category\":[\"cat facts\"],\"sg_event_id\":\"CjkStOi15hPQfWO58rH9dw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"unsubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"QrCQiwv2pfkejKE5Zi1D0g==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\"},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"group_unsubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"l67dvY6MIupGpuk1r9vIXw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\",\"asm_group_id\":10},\r\n{\"email\":\"example@test.com\",\"timestamp\":1655455223,\"smtp-id\":\"\\u003c14c5d75ce93.dfd.64b469@ismtpd-555\\u003e\",\"event\":\"group_resubscribe\",\"category\":[\"cat facts\"],\"sg_event_id\":\"fo7PLpVCoCA9WDHovGQyyw==\",\"sg_message_id\":\"14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0\",\"useragent\":\"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)\",\"ip\":\"255.255.255.255\",\"url\":\"http://www.sendgrid.com/\",\"asm_group_id\":10}]\r\n" "MEUCIQCBYJiC1zzZeM61EbekWSGMFgpRSzaQSA4zwV3vlMgf/wIgSrMZIIYTnx4dkqDK92re4WYhcM3xEKbLIKfmcu7Et0o="
ghci> _test_verify Right () ghci> _test_verify_fail Left "Verification failed"
SendGridから送られてきたデータは通り、改ざんしたデータは弾けることを確認できた。
まとめ
インターネットサバンナの住人として、不正な情報に対する警戒は怠ってはいけない。SendGridは「リクエストの内容とタイムスタンプをECDSAで署名する」というシンプルな仕組みがあるおかげで、比較的少ないコードで検証機能を実装できた。
PR枠
WebHookの門はECDSAによって閉ざされているが、採用においてHERPの門は開かれている。HERPは頭が良くて物事を成し遂げられる人を募集しています。