diff --git a/Cargo.lock b/Cargo.lock index 9e89bef..6713fd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.0.1" @@ -128,6 +137,34 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "dirs" version = "3.0.2" @@ -212,6 +249,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -269,6 +316,22 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "http" version = "0.2.4" @@ -436,7 +499,7 @@ dependencies = [ [[package]] name = "multihook" -version = "0.1.3" +version = "0.1.4" dependencies = [ "chrono", "colored", @@ -444,6 +507,8 @@ dependencies = [ "dirs", "fern", "glob", + "hex", + "hmac", "hyper", "jsonpath", "lazy_static", @@ -451,6 +516,7 @@ dependencies = [ "regex", "serde 1.0.127", "serde_json", + "sha2", "thiserror", "tokio", "toml", @@ -529,6 +595,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "pest" version = "1.0.6" @@ -685,6 +757,19 @@ dependencies = [ "serde 1.0.127", ] +[[package]] +name = "sha2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -716,6 +801,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "0.11.11" @@ -862,6 +953,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + [[package]] name = "unicode-xid" version = "0.0.4" diff --git a/Cargo.toml b/Cargo.toml index eea9b0f..77173d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "A webhook server" authors = ["trivernis "] license = "GPL-3.0" readme = "README.md" -version = "0.1.3" +version = "0.1.4" edition = "2018" repository = "https://github.com/Trivernis/multihook.git" @@ -24,6 +24,9 @@ fern = "0.6.0" serde_json = "1.0.66" jsonpath = "0.1.1" regex = "1.5.4" +hmac = "0.11.0" +sha2 = "0.9.5" +hex = "0.4.3" [dependencies.serde] version = "1.0.127" diff --git a/README.md b/README.md index 517771b..e4b2955 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ allow_parallel = true [endpoints.error] path = "error" action = "echo '{{$.books.*.title}}'" +# Validate secrets according to different parsing rules +# Currently only GitHub secrets are supported +secret = { value = "my secret", type = "GitHub"} [endpoints.testscript] path = "script" diff --git a/src/main.rs b/src/main.rs index ff92c65..529c771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use utils::settings::get_settings; use crate::server::HookServer; +mod secret_validation; mod server; pub(crate) mod utils; diff --git a/src/secret_validation/github.rs b/src/secret_validation/github.rs new file mode 100644 index 0000000..37efd9e --- /dev/null +++ b/src/secret_validation/github.rs @@ -0,0 +1,28 @@ +use crate::secret_validation::SecretValidator; +use hmac::{Hmac, Mac, NewMac}; +use hyper::HeaderMap; +use sha2::Sha256; + +pub struct GithubSecretValidator; + +static X_HUB_SIGNATURE_256_HEADER: &str = "X-Hub-Signature-256"; + +impl SecretValidator for GithubSecretValidator { + fn validate(&self, headers: &HeaderMap, body: &[u8], secret: &[u8]) -> bool { + log::debug!("Validating GitHub Secret"); + if let Some(github_sum) = headers.get(X_HUB_SIGNATURE_256_HEADER) { + let mut mac = Hmac::::new_from_slice(secret).unwrap(); + mac.update(body); + + let decoded_secret = if let Ok(decoded) = hex::decode(github_sum) { + decoded + } else { + return false; + }; + mac.verify(&decoded_secret).is_ok() + } else { + log::debug!("Missing Signature Header"); + false + } + } +} diff --git a/src/secret_validation/mod.rs b/src/secret_validation/mod.rs new file mode 100644 index 0000000..93e7933 --- /dev/null +++ b/src/secret_validation/mod.rs @@ -0,0 +1,22 @@ +mod github; + +use crate::secret_validation::github::GithubSecretValidator; +use hyper::HeaderMap; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum SecretFormat { + GitHub, +} + +impl SecretFormat { + pub fn validator(&self) -> impl SecretValidator { + match self { + SecretFormat::GitHub => GithubSecretValidator, + } + } +} + +pub trait SecretValidator { + fn validate(&self, headers: &HeaderMap, body: &[u8], secret: &[u8]) -> bool; +} diff --git a/src/server/action.rs b/src/server/action.rs index 4ddec41..36efbcb 100644 --- a/src/server/action.rs +++ b/src/server/action.rs @@ -1,6 +1,8 @@ +use crate::secret_validation::SecretValidator; use crate::server::command_template::CommandTemplate; -use crate::utils::error::MultihookResult; -use crate::utils::settings::EndpointSettings; +use crate::utils::error::{MultihookError, MultihookResult}; +use crate::utils::settings::{EndpointSettings, SecretSettings}; +use hyper::http::request::Parts; use hyper::{Body, Request}; use serde_json::Value; use std::fs::read_to_string; @@ -17,10 +19,16 @@ pub struct HookAction { command: CommandTemplate, parallel_lock: Arc, run_detached: bool, + secret: Option, } impl HookAction { - pub fn new(command: S, parallel: bool, detached: bool) -> Self { + pub fn new( + command: S, + parallel: bool, + detached: bool, + secret: Option, + ) -> Self { let parallel_lock = if parallel { Semaphore::new(MAX_CONCURRENCY) } else { @@ -30,12 +38,17 @@ impl HookAction { command: CommandTemplate::new(command), parallel_lock: Arc::new(parallel_lock), run_detached: detached, + secret, } } pub async fn execute(&self, req: Request) -> MultihookResult<()> { - let body = hyper::body::to_bytes(req.into_body()).await?.to_vec(); + let (parts, body) = req.into_parts(); + let body = hyper::body::to_bytes(body).await?.to_vec(); + + self.validate_secret(&parts, &body)?; let body = String::from_utf8(body)?; + if self.run_detached { tokio::spawn({ let action = self.clone(); @@ -52,6 +65,16 @@ impl HookAction { } } + fn validate_secret(&self, parts: &Parts, body: &Vec) -> MultihookResult<()> { + if let Some(secret) = &self.secret { + let validator = secret.format.validator(); + if !validator.validate(&parts.headers, &body, &secret.value.as_bytes()) { + return Err(MultihookError::InvalidSecret); + } + } + Ok(()) + } + async fn execute_command(&self, body: &str) -> MultihookResult<()> { let json_body: Value = serde_json::from_str(body).unwrap_or_default(); let command = self.command.evaluate(&json_body); @@ -85,6 +108,11 @@ impl From<&EndpointSettings> for HookAction { let action = endpoint.action.clone(); let path = PathBuf::from(&action); let contents = read_to_string(path).unwrap_or(action); - Self::new(contents, endpoint.allow_parallel, endpoint.run_detached) + Self::new( + contents, + endpoint.allow_parallel, + endpoint.run_detached, + endpoint.secret.clone(), + ) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 809cb9c..89dac9e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,10 +1,11 @@ use std::sync::Arc; +use hyper::{Body, Method, Response}; + use action::HookAction; use crate::server::http::{HTTPCallback, HTTPServer}; use crate::utils::error::MultihookResult; -use hyper::{Body, Method, Response}; pub mod action; pub mod command_template; diff --git a/src/utils/error.rs b/src/utils/error.rs index 2c4b60a..c78e5f6 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -22,4 +22,7 @@ pub enum MultihookError { #[error(transparent)] Hyper(#[from] hyper::Error), + + #[error("Secret validation failed.")] + InvalidSecret, } diff --git a/src/utils/settings.rs b/src/utils/settings.rs index c2dbae8..40680d4 100644 --- a/src/utils/settings.rs +++ b/src/utils/settings.rs @@ -1,3 +1,4 @@ +use crate::secret_validation::SecretFormat; use crate::utils::error::MultihookResult; use config::File; use lazy_static::lazy_static; @@ -25,6 +26,13 @@ pub struct EndpointSettings { pub allow_parallel: bool, #[serde(default)] pub run_detached: bool, + pub secret: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SecretSettings { + pub value: String, + pub format: SecretFormat, } impl Default for Settings {