//! Amazon Simple Notification Service message signature verification //! //! Messages sent by Amazon SNS are signed to allow recipients to //! verify that they originated from Amazon and have not been spoofed. //! This verification process is particularly important for HTTP/HTTPS //! notifications, since these could easily be forged by untrusted //! third parties; the message format is simple and well documented. //! //! This module provides the [`SignatureVerifier`] trait as well as //! implementations of it for the two common SNS message types: //! subscription confirmation and notification. //! //! For additional information about the SNS message signature //! verification process, see [Verifying the signatures of Amazon SNS //! messages][0] in the AWS documentation. //! //! [0]: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html use std::fmt; use log::debug; use rsa::hash::Hash; use rsa::padding::PaddingScheme; use rsa::pkcs8::DecodePublicKey; use rsa::{PublicKey, RsaPublicKey}; use sha1::{Digest, Sha1}; use x509_parser::error::{PEMError, X509Error}; use x509_parser::pem::Pem; use crate::model::sns::*; /// Error type for message signature verification issues #[derive(Debug)] pub enum SignatureError { /// An error occurred while decoding a base64 string (e.g. the signature /// field of a message payload) Base64DecodeError(base64::DecodeError), /// The signature could not be verified using the provided RSA key VerificationError(rsa::errors::Error), /// The RSA public key could not be loaded PkiError(rsa::pkcs8::spki::Error), /// The X.509 certificate could not be parsed X509Error(x509_parser::nom::Err), /// The PEM structure could not be decoded PEMError(PEMError), /// No certificate was provided for signature verification NoCertificate, /// The message specified an unsupported signature version UnsupportedVersion(String), } impl fmt::Display for SignatureError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Base64DecodeError(e) => write!(f, "{}", e), Self::VerificationError(e) => write!(f, "{}", e), Self::PkiError(e) => write!(f, "{}", e), Self::X509Error(e) => write!(f, "{}", e), Self::PEMError(e) => write!(f, "{}", e), Self::NoCertificate => write!(f, "No certificate supplied"), Self::UnsupportedVersion(v) => { write!(f, "Unsupported signature version: {}", v) } } } } impl std::error::Error for SignatureError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Base64DecodeError(e) => Some(e), Self::VerificationError(e) => Some(e), Self::PkiError(e) => Some(e), Self::X509Error(e) => Some(e), Self::PEMError(e) => Some(e), _ => None, } } } impl From for SignatureError { fn from(e: base64::DecodeError) -> Self { Self::Base64DecodeError(e) } } impl From for SignatureError { fn from(e: rsa::errors::Error) -> Self { Self::VerificationError(e) } } impl From for SignatureError { fn from(e: rsa::pkcs8::spki::Error) -> Self { Self::PkiError(e) } } impl From> for SignatureError { fn from(e: x509_parser::nom::Err) -> Self { Self::X509Error(e) } } impl From for SignatureError { fn from(e: PEMError) -> Self { Self::PEMError(e) } } /// Trait for SNS message signature verification /// /// The provided [`SignatureVerifier::verify`] method verifies the message /// signature by using the data provided by the /// [`SignatureVerifier::as_plaintext`] and [`SignatureVerifier::signature`] /// methods. pub trait SignatureVerifier { /// Return the plaintext version of the message (string to sign) fn as_plaintext(&self) -> String; /// Return the message signature as an array of bytes fn signature(&self) -> Result, base64::DecodeError>; /// Return the signature version fn signature_version(&self) -> Option<&str>; /// Verify the message signature /// /// This method verifies the signature of the message using the public key /// included in the supplied certificate. The certificate is usually /// fetched from the URL included in the message body. fn verify>(&self, cert: C) -> Result<(), SignatureError> { let ver = self.signature_version(); match ver { Some("1") => { debug!("Verifying message using signature version 1"); verify_sig_rsa_sha1( &self.as_plaintext(), &self.signature()?, cert, ) } Some(v) => { return Err(SignatureError::UnsupportedVersion(v.into())) } None => { return Err(SignatureError::UnsupportedVersion( "No version specified".into(), )) } } } } impl SignatureVerifier for SubscriptionConfirmationMessage { fn as_plaintext(&self) -> String { let mut s = String::new(); s.push_str("Message\n"); s.push_str(&self.message); s.push('\n'); s.push_str("MessageId\n"); s.push_str(&self.message_id); s.push('\n'); s.push_str("SubscribeURL\n"); s.push_str(&self.subscribe_url); s.push('\n'); s.push_str("Timestamp\n"); s.push_str(&self.timestamp); s.push('\n'); s.push_str("Token\n"); s.push_str(&self.token); s.push('\n'); s.push_str("TopicArn\n"); s.push_str(&self.topic_arn); s.push('\n'); s.push_str("Type\n"); s.push_str("SubscriptionConfirmation"); s.push('\n'); s } fn signature(&self) -> Result, base64::DecodeError> { base64::decode(&self.signature) } fn signature_version(&self) -> Option<&str> { Some(&self.signature_version) } } impl SignatureVerifier for NotificationMessage { fn as_plaintext(&self) -> String { let mut s = String::new(); s.push_str("Message\n"); s.push_str(&self.message); s.push('\n'); s.push_str("MessageId\n"); s.push_str(&self.message_id); s.push('\n'); if let Some(subject) = &self.subject { s.push_str("Subject\n"); s.push_str(subject); s.push('\n'); } s.push_str("Timestamp\n"); s.push_str(&self.timestamp); s.push('\n'); s.push_str("TopicArn\n"); s.push_str(&self.topic_arn); s.push('\n'); s.push_str("Type\n"); s.push_str("Notification"); s.push('\n'); s } fn signature(&self) -> Result, base64::DecodeError> { base64::decode(&self.signature) } fn signature_version(&self) -> Option<&str> { Some(&self.signature_version) } } impl SignatureVerifier for UnsubscribeConfirmationMessage { fn as_plaintext(&self) -> String { let mut s = String::new(); s.push_str("Message\n"); s.push_str(&self.message); s.push('\n'); s.push_str("MessageId\n"); s.push_str(&self.message_id); s.push('\n'); s.push_str("SubscribeURL\n"); s.push_str(&self.subscribe_url); s.push('\n'); s.push_str("Timestamp\n"); s.push_str(&self.timestamp); s.push('\n'); s.push_str("Token\n"); s.push_str(&self.token); s.push('\n'); s.push_str("TopicArn\n"); s.push_str(&self.topic_arn); s.push('\n'); s.push_str("Type\n"); s.push_str("UnsubscribeConfirmation"); s.push('\n'); s } fn signature(&self) -> Result, base64::DecodeError> { base64::decode(&self.signature) } fn signature_version(&self) -> Option<&str> { Some(&self.signature_version) } } /// Extract the RSA public key from a PEM-encoded certificate fn key_from_cert_pem>( cert: C, ) -> Result { for pem in Pem::iter_from_buffer(cert.as_ref()) { let pem = pem?; let x509 = pem.parse_x509()?; return Ok(RsaPublicKey::from_public_key_der(x509.public_key().raw)?); } Err(SignatureError::NoCertificate) } /// Verify the signature of a byte array with RSA-SHA1, using the public key /// found in the provided PEM-encoded X.509 certificate fn verify_sig_rsa_sha1( txt: T, sig: S, cert: C, ) -> Result<(), SignatureError> where T: AsRef<[u8]>, S: AsRef<[u8]>, C: AsRef<[u8]>, { let key = key_from_cert_pem(cert.as_ref())?; let mut hasher = Sha1::new(); hasher.update(txt.as_ref()); let hashed = hasher.finalize(); let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA1)); Ok(key.verify(padding, &hashed, sig.as_ref())?) } #[cfg(test)] mod test { use super::*; use serde_json; const TEST_CERT: &str = r#"-----BEGIN CERTIFICATE----- MIIF2zCCBMOgAwIBAgIQCbogXPgKeqHy9lOwcjGKpTANBgkqhkiG9w0BAQsFADBG MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMjA2MjkwMDAwMDBaFw0yMzA2MDMy MzU5NTlaMBwxGjAYBgNVBAMTEXNucy5hbWF6b25hd3MuY29tMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5YzF4g9Y8VUo4F8DVUcQ2pylVpAiPNyyq2VY 5ybkw+jt7ZAKpmdnGKPdFKCfI0TuZvUBABJ6I8yz0Zw2b8oDNmF+W+9cRZ0+G2VU 9fakJa0jRrgJBnVecjFKoGDU9YwjDXTfT4LEGWFm8PFsvsyT3cm/4yxIY2Ds4GLm g9ymrXBKFR41qNaRCTKU1VQ+WDXLAHpW8EfIBjIqDg0dncYGu/u0Qx3W/BVy6BPl xMH7exn7wJA1GO6VnDPyyKQ2fwR5ks2omE+J3qRmMYAcQCfjSDAfLw3t4oIPKK1R nRCdK6pgoSFxphF9QlKXn1rmNprC+MbnVnRe0CEymqhGngiQiQIDAQABo4IC7TCC AukwHwYDVR0jBBgwFoAUWaRmBlKge5WSPKOUByeWdFv5PdAwHQYDVR0OBBYEFMJb o6szSbq+Fu1RMn9G6ujEaVEIMBwGA1UdEQQVMBOCEXNucy5hbWF6b25hd3MuY29t MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw PQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2NybC5zY2ExYi5hbWF6b250cnVzdC5j b20vc2NhMWItMS5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEE aTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5zY2ExYi5hbWF6b250cnVzdC5j b20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuc2NhMWIuYW1hem9udHJ1c3QuY29t L3NjYTFiLmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFr AWkAdgDoPtDaPvUGNTLnVyi8iWvJA9PL0RFr7Otp4Xd9bQa9bgAAAYGtmiE9AAAE AwBHMEUCIELrqLuolvf1v8v/E4niiCqlo8f2+H17RVBANsyL71BJAiEA0m/JNE5l 6hhBBedPEGpohM8s3ruGQh8lt09MZcsfT2MAdwA1zxkbv7FsV78PrUxtQsu7ticg JlHqP+Eq76gDwzvWTAAAAYGtmiEaAAAEAwBIMEYCIQDEqcEbX5emS67nngXRHOIg Zev/O4uqg1ZfgEvzaBqd7QIhAMmiWF8jFp71BVZMGwJxLifkyNT7lLEFH/SKBzXk aHFLAHYAs3N3B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAGBrZohUgAA BAMARzBFAiEAuUV+F7cNwWZFU2loPe7oVpVdM2dwxncLm9gJF7fv/9ICIF2xc324 XAWRWEVosNUKY4nOiwzhVAT69/cPdEoK3En2MA0GCSqGSIb3DQEBCwUAA4IBAQAi YYozz0Q1hAmQf1nDyqoGzsZQCpEjGh0CrhP8FktRzEthA67dwI8qsrTzmswsotne nTn0dmg5esNSFwCamxDIKguCK1Bty6F7lE1Ow6WDjmFRoNvateP59Pjh6NT3IdEN QPBy5CfaQ4nkRKmzgtZQ71y3GdqfhWskfcXSQIzwi9LpF684a+tbV4jNLghyiSYk 3W6jorBvXRqNXF68JsQJz27oUaJywJXJ0LtrLRJ640vhd41T0rY/RMhemvCWSDKF fM9rUbiv3laMUnh/Viea8UA//fYheXHkE0ZnjdKEdWV6jX0T2DqYu0ieso+Hy/aw Az407vZW083P5WFSIT26 -----END CERTIFICATE----- "#; const TEST_MSG: &str = r#"{ "Type" : "SubscriptionConfirmation", "MessageId" : "5506049a-38a4-4af5-b1e2-e1c4de84c009", "Token" : "2336412f37fb687f5d51e6e2425dacbba7f97a62baf67e3ffa0b3da7071d369eb88d52257631cc33d52b277c7f215a6160215838db9eb1332ba95cf0418bbb0cf7037c4ab353be5f46441cb837c0cc6877623cbde0238813d4a64fce01ef705d3b8f1c3bdc86aa2f183e960ec112c984", "TopicArn" : "arn:aws:sns:us-east-2:566967686773:dchtest1", "Message" : "You have chosen to subscribe to the topic arn:aws:sns:us-east-2:566967686773:dchtest1.\nTo confirm the subscription, visit the SubscribeURL included in this message.", "SubscribeURL" : "https://sns.us-east-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-2:566967686773:dchtest1&Token=2336412f37fb687f5d51e6e2425dacbba7f97a62baf67e3ffa0b3da7071d369eb88d52257631cc33d52b277c7f215a6160215838db9eb1332ba95cf0418bbb0cf7037c4ab353be5f46441cb837c0cc6877623cbde0238813d4a64fce01ef705d3b8f1c3bdc86aa2f183e960ec112c984", "Timestamp" : "2022-08-31T21:17:09.115Z", "SignatureVersion" : "1", "Signature" : "uPd8FxfVtenWUtcOF7iBNI6YQ6uHqGMlbc1U8/KesvnA9p2/3XEK6MJhaLcgOmLfpBhbljRFcEWPK2xBWsagX6uUk5d5mCQRkE/N+IfezLg/Q8vwTUw3rhVXZge7gl8NCCpqia1xQoSo8PbMkfAb9sw6YoytJopaPrRzvxHRTdUmyTVw2vrl8yxHD2OTRVYKpKv6Pg1Pf0VXdZq07xMRaqF2zTFK+LNYBJ74wrJRg1zLe6xfscwQytUpKf8vFHyPhP2QZWxi/mJ6YIowvXh0cElGyjox3jLxEoQ+K0jARrQSAhOBufHzd35BOAm0b7JES/YMYE58NxYHkXmoX3u1Cg==", "SigningCertURL" : "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-56e67fcb41f6fec09b0196692625d385.pem" } "#; #[test] fn test_subscriptionconfirmation_as_plaintext() { let msg: SubscriptionConfirmationMessage = serde_json::from_str(TEST_MSG).unwrap(); assert_eq!( msg.as_plaintext(), r#"Message You have chosen to subscribe to the topic arn:aws:sns:us-east-2:566967686773:dchtest1. To confirm the subscription, visit the SubscribeURL included in this message. MessageId 5506049a-38a4-4af5-b1e2-e1c4de84c009 SubscribeURL https://sns.us-east-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-2:566967686773:dchtest1&Token=2336412f37fb687f5d51e6e2425dacbba7f97a62baf67e3ffa0b3da7071d369eb88d52257631cc33d52b277c7f215a6160215838db9eb1332ba95cf0418bbb0cf7037c4ab353be5f46441cb837c0cc6877623cbde0238813d4a64fce01ef705d3b8f1c3bdc86aa2f183e960ec112c984 Timestamp 2022-08-31T21:17:09.115Z Token 2336412f37fb687f5d51e6e2425dacbba7f97a62baf67e3ffa0b3da7071d369eb88d52257631cc33d52b277c7f215a6160215838db9eb1332ba95cf0418bbb0cf7037c4ab353be5f46441cb837c0cc6877623cbde0238813d4a64fce01ef705d3b8f1c3bdc86aa2f183e960ec112c984 TopicArn arn:aws:sns:us-east-2:566967686773:dchtest1 Type SubscriptionConfirmation "# ); } #[test] fn test_key_from_cert() { key_from_cert_pem(TEST_CERT).unwrap(); } #[test] fn test_key_from_cert_empty() { match key_from_cert_pem("") { Err(SignatureError::NoCertificate) => (), _ => panic!("unexpected result"), } } #[test] fn test_verify_msg() { let msg: SubscriptionConfirmationMessage = serde_json::from_str(TEST_MSG).unwrap(); msg.verify(TEST_CERT).unwrap(); } #[test] fn test_verify_msg_invalid() { let mut msg: SubscriptionConfirmationMessage = serde_json::from_str(TEST_MSG).unwrap(); msg.topic_arn = msg.topic_arn.replace("1", "2"); msg.verify(TEST_CERT).unwrap_err(); } #[test] fn test_verify_msg_unsupported_version() { let mut msg: SubscriptionConfirmationMessage = serde_json::from_str(TEST_MSG).unwrap(); msg.signature_version = "2".into(); msg.verify(TEST_CERT).unwrap_err(); } }