dynk8s-provisioner/src/sns/sig.rs

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();
}
}