407 lines
15 KiB
Rust
407 lines
15 KiB
Rust
//! 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<x509_parser::error::X509Error>),
|
|
/// 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<base64::DecodeError> for SignatureError {
|
|
fn from(e: base64::DecodeError) -> Self {
|
|
Self::Base64DecodeError(e)
|
|
}
|
|
}
|
|
|
|
impl From<rsa::errors::Error> for SignatureError {
|
|
fn from(e: rsa::errors::Error) -> Self {
|
|
Self::VerificationError(e)
|
|
}
|
|
}
|
|
|
|
impl From<rsa::pkcs8::spki::Error> for SignatureError {
|
|
fn from(e: rsa::pkcs8::spki::Error) -> Self {
|
|
Self::PkiError(e)
|
|
}
|
|
}
|
|
|
|
impl From<x509_parser::nom::Err<X509Error>> for SignatureError {
|
|
fn from(e: x509_parser::nom::Err<X509Error>) -> Self {
|
|
Self::X509Error(e)
|
|
}
|
|
}
|
|
|
|
impl From<PEMError> 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<Vec<u8>, 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<C: AsRef<[u8]>>(&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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<C: AsRef<[u8]>>(
|
|
cert: C,
|
|
) -> Result<RsaPublicKey, SignatureError> {
|
|
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<T, C, S>(
|
|
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();
|
|
}
|
|
}
|