diff --git a/Cargo.lock b/Cargo.lock index 90130f1..e1d7d06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b984796..09a65f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/main.rs b/src/main.rs index b9ec0e0..a8ec5b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +mod model; +mod sns; + #[doc(hidden)] fn main() { println!("Hello, world!"); diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..0f400a7 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,2 @@ +//! The dynk8s provisioner data model +pub mod sns; diff --git a/src/model/sns.rs b/src/model/sns.rs new file mode 100644 index 0000000..552fe9b --- /dev/null +++ b/src/model/sns.rs @@ -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, + 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, +} diff --git a/src/sns/mod.rs b/src/sns/mod.rs new file mode 100644 index 0000000..719b492 --- /dev/null +++ b/src/sns/mod.rs @@ -0,0 +1,2 @@ +//! Amazon Simple Notification Service (SNS) utilities +pub mod sig; diff --git a/src/sns/sig.rs b/src/sns/sig.rs new file mode 100644 index 0000000..0624cb4 --- /dev/null +++ b/src/sns/sig.rs @@ -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), + /// 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(&self.message_type); + 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(&self.message_type); + 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(&self.message_type); + 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(); + } +}