sns: Begin work on Amazon SNS message handling

In order to prevent arbitrary clients from using the provisioner to
retrieve WireGuard keys and Kubernetes bootstrap tokens, access to those
resources *must* be restricted to the EC2 machines created by the
Kubernetes Cloud Autoscaler.  The key to the authentication process will
be SNS notifications from AWS to indicate when new EC2 instances are
created; everything that the provisioner does will be associated with an
instance it discovered through an SNS notification.

SNS messages are signed using PKCS#1 v1.5 RSA-SHA1, with a public key
distributed in an X.509 certificate.  To ensure that messages received
are indeed from AWS, the provisioner will need to verify those
signatures.  Messages with missing or invalid signatures will be
considered unsafe and ignored.

The `model::sns` module includes the data structures that represent SNS
messages.  The `sns::sig` module includes the primitive operations for
implementing signature verification.
master
Dustin 2022-09-01 18:22:22 -05:00
parent 90e5bd65ca
commit 196a43c49c
7 changed files with 1132 additions and 0 deletions

634
Cargo.lock generated
View File

@ -2,6 +2,640 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "asn1-rs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64ct"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474"
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "const-oid"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
[[package]]
name = "cpufeatures"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813"
dependencies = [
"libc",
]
[[package]]
name = "crypto-bigint"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "der"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c"
dependencies = [
"const-oid",
"crypto-bigint",
"pem-rfc7468",
]
[[package]]
name = "der-parser"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dynk8s-provisioner"
version = "0.1.0"
dependencies = [
"base64",
"log",
"rsa",
"serde",
"serde_json",
"sha1",
"x509-parser",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin",
]
[[package]]
name = "libc"
version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]]
name = "libm"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "oid-registry"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4bda43fd1b844cbc6e6e54b5444e2b1bc7838bce59ad205902cccbb26d6761"
dependencies = [
"asn1-rs",
]
[[package]]
name = "pem-rfc7468"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30"
dependencies = [
"base64ct",
]
[[package]]
name = "pkcs1"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320"
dependencies = [
"der",
"pkcs8",
"zeroize",
]
[[package]]
name = "pkcs8"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0"
dependencies = [
"der",
"spki",
"zeroize",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro2"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rsa"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b"
dependencies = [
"byteorder",
"digest",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"smallvec",
"subtle",
"zeroize",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "serde"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cf2781a4ca844dd4f9b608a1791eea19830df0ad3cdd9988cd05f1c66ccb63a"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "smallvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spki"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
dependencies = [
"itoa",
"libc",
"num_threads",
"time-macros",
]
[[package]]
name = "time-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-ident"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-xid"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "x509-parser"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8"
dependencies = [
"asn1-rs",
"base64",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"rusticata-macros",
"thiserror",
"time",
]
[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"

View File

@ -4,3 +4,10 @@ version = "0.1.0"
edition = "2021"
[dependencies]
base64 = "0.13.0"
log = "0.4.17"
rsa = "0.6.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"
sha1 = "0.10.2"
x509-parser = "0.14.0"

View File

@ -1,3 +1,6 @@
mod model;
mod sns;
#[doc(hidden)]
fn main() {
println!("Hello, world!");

2
src/model/mod.rs Normal file
View File

@ -0,0 +1,2 @@
//! The dynk8s provisioner data model
pub mod sns;

78
src/model/sns.rs Normal file
View File

@ -0,0 +1,78 @@
//! Amazon SNS message types
//!
//! These message types are defined in the [Parsing message formats][0] page of
//! the AWS Documentation.
//!
//! [0]: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html
use serde::Deserialize;
/// SNS Subscription confirmation message
///
/// See also: [HTTP/HTTPS subscription confirmation JSON format][0]
///
/// [0]: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html#http-subscription-confirmation-json
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SubscriptionConfirmationMessage {
#[serde(rename = "Type")]
pub message_type: String,
#[serde(rename = "MessageId")]
pub message_id: String,
pub token: String,
pub topic_arn: String,
pub message: String,
#[serde(rename = "SubscribeURL")]
pub subscribe_url: String,
pub timestamp: String,
pub signature_version: String,
pub signature: String,
#[serde(rename = "SigningCertURL")]
pub signing_cert_url: String,
}
/// SNS Notification message
///
/// See also: [HTTP/HTTPS notification JSON format][0]
///
/// [0]: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html#http-notification-json
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NotificationMessage {
#[serde(rename = "Type")]
pub message_type: String,
#[serde(rename = "MessageId")]
pub message_id: String,
pub topic_arn: String,
pub message: String,
#[serde(default)]
pub subject: Option<String>,
pub timestamp: String,
pub signature_version: String,
pub signature: String,
#[serde(rename = "SigningCertURL")]
pub signing_cert_url: String,
}
/// SNS Unsubscribe confirmation message
///
/// See also: [HTTP/HTTPS unsubscribe confirmation JSON format][0]
///
/// [0]: https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html#http-unsubscribe-confirmation-json
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct UnsubscribeConfirmationMessage {
#[serde(rename = "Type")]
pub message_type: String,
#[serde(rename = "MessageId")]
pub message_id: String,
pub token: String,
pub topic_arn: String,
pub message: String,
#[serde(rename = "SubscribeURL")]
pub subscribe_url: String,
pub timestamp: String,
pub signature_version: String,
pub signature: String,
#[serde(rename = "SigningCertURL")]
pub signing_cert_url: String,
}

2
src/sns/mod.rs Normal file
View File

@ -0,0 +1,2 @@
//! Amazon Simple Notification Service (SNS) utilities
pub mod sig;

406
src/sns/sig.rs Normal file
View File

@ -0,0 +1,406 @@
//! 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(&self.message_type);
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(&self.message_type);
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(&self.message_type);
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();
}
}