Add secret validation

Signed-off-by: trivernis <trivernis@protonmail.com>
main
trivernis 3 years ago
parent 40ee976942
commit 2b80515d84
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

99
Cargo.lock generated

@ -70,6 +70,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 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]] [[package]]
name = "bytes" name = "bytes"
version = "1.0.1" version = "1.0.1"
@ -128,6 +137,34 @@ dependencies = [
"yaml-rust", "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]] [[package]]
name = "dirs" name = "dirs"
version = "3.0.2" version = "3.0.2"
@ -212,6 +249,16 @@ dependencies = [
"pin-utils", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@ -269,6 +316,22 @@ dependencies = [
"libc", "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]] [[package]]
name = "http" name = "http"
version = "0.2.4" version = "0.2.4"
@ -436,7 +499,7 @@ dependencies = [
[[package]] [[package]]
name = "multihook" name = "multihook"
version = "0.1.3" version = "0.1.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"colored", "colored",
@ -444,6 +507,8 @@ dependencies = [
"dirs", "dirs",
"fern", "fern",
"glob", "glob",
"hex",
"hmac",
"hyper", "hyper",
"jsonpath", "jsonpath",
"lazy_static", "lazy_static",
@ -451,6 +516,7 @@ dependencies = [
"regex", "regex",
"serde 1.0.127", "serde 1.0.127",
"serde_json", "serde_json",
"sha2",
"thiserror", "thiserror",
"tokio", "tokio",
"toml", "toml",
@ -529,6 +595,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "pest" name = "pest"
version = "1.0.6" version = "1.0.6"
@ -685,6 +757,19 @@ dependencies = [
"serde 1.0.127", "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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.0" version = "1.4.0"
@ -716,6 +801,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "0.11.11" version = "0.11.11"
@ -862,6 +953,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "typenum"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.0.4" version = "0.0.4"

@ -4,7 +4,7 @@ description = "A webhook server"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
readme = "README.md" readme = "README.md"
version = "0.1.3" version = "0.1.4"
edition = "2018" edition = "2018"
repository = "https://github.com/Trivernis/multihook.git" repository = "https://github.com/Trivernis/multihook.git"
@ -24,6 +24,9 @@ fern = "0.6.0"
serde_json = "1.0.66" serde_json = "1.0.66"
jsonpath = "0.1.1" jsonpath = "0.1.1"
regex = "1.5.4" regex = "1.5.4"
hmac = "0.11.0"
sha2 = "0.9.5"
hex = "0.4.3"
[dependencies.serde] [dependencies.serde]
version = "1.0.127" version = "1.0.127"

@ -46,6 +46,9 @@ allow_parallel = true
[endpoints.error] [endpoints.error]
path = "error" path = "error"
action = "echo '{{$.books.*.title}}'" 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] [endpoints.testscript]
path = "script" path = "script"

@ -5,6 +5,7 @@ use utils::settings::get_settings;
use crate::server::HookServer; use crate::server::HookServer;
mod secret_validation;
mod server; mod server;
pub(crate) mod utils; pub(crate) mod utils;

@ -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::<Sha256>::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
}
}
}

@ -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;
}

@ -1,6 +1,8 @@
use crate::secret_validation::SecretValidator;
use crate::server::command_template::CommandTemplate; use crate::server::command_template::CommandTemplate;
use crate::utils::error::MultihookResult; use crate::utils::error::{MultihookError, MultihookResult};
use crate::utils::settings::EndpointSettings; use crate::utils::settings::{EndpointSettings, SecretSettings};
use hyper::http::request::Parts;
use hyper::{Body, Request}; use hyper::{Body, Request};
use serde_json::Value; use serde_json::Value;
use std::fs::read_to_string; use std::fs::read_to_string;
@ -17,10 +19,16 @@ pub struct HookAction {
command: CommandTemplate, command: CommandTemplate,
parallel_lock: Arc<Semaphore>, parallel_lock: Arc<Semaphore>,
run_detached: bool, run_detached: bool,
secret: Option<SecretSettings>,
} }
impl HookAction { impl HookAction {
pub fn new<S: ToString>(command: S, parallel: bool, detached: bool) -> Self { pub fn new<S: ToString>(
command: S,
parallel: bool,
detached: bool,
secret: Option<SecretSettings>,
) -> Self {
let parallel_lock = if parallel { let parallel_lock = if parallel {
Semaphore::new(MAX_CONCURRENCY) Semaphore::new(MAX_CONCURRENCY)
} else { } else {
@ -30,12 +38,17 @@ impl HookAction {
command: CommandTemplate::new(command), command: CommandTemplate::new(command),
parallel_lock: Arc::new(parallel_lock), parallel_lock: Arc::new(parallel_lock),
run_detached: detached, run_detached: detached,
secret,
} }
} }
pub async fn execute(&self, req: Request<Body>) -> MultihookResult<()> { pub async fn execute(&self, req: Request<Body>) -> 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)?; let body = String::from_utf8(body)?;
if self.run_detached { if self.run_detached {
tokio::spawn({ tokio::spawn({
let action = self.clone(); let action = self.clone();
@ -52,6 +65,16 @@ impl HookAction {
} }
} }
fn validate_secret(&self, parts: &Parts, body: &Vec<u8>) -> 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<()> { async fn execute_command(&self, body: &str) -> MultihookResult<()> {
let json_body: Value = serde_json::from_str(body).unwrap_or_default(); let json_body: Value = serde_json::from_str(body).unwrap_or_default();
let command = self.command.evaluate(&json_body); let command = self.command.evaluate(&json_body);
@ -85,6 +108,11 @@ impl From<&EndpointSettings> for HookAction {
let action = endpoint.action.clone(); let action = endpoint.action.clone();
let path = PathBuf::from(&action); let path = PathBuf::from(&action);
let contents = read_to_string(path).unwrap_or(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(),
)
} }
} }

@ -1,10 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use hyper::{Body, Method, Response};
use action::HookAction; use action::HookAction;
use crate::server::http::{HTTPCallback, HTTPServer}; use crate::server::http::{HTTPCallback, HTTPServer};
use crate::utils::error::MultihookResult; use crate::utils::error::MultihookResult;
use hyper::{Body, Method, Response};
pub mod action; pub mod action;
pub mod command_template; pub mod command_template;

@ -22,4 +22,7 @@ pub enum MultihookError {
#[error(transparent)] #[error(transparent)]
Hyper(#[from] hyper::Error), Hyper(#[from] hyper::Error),
#[error("Secret validation failed.")]
InvalidSecret,
} }

@ -1,3 +1,4 @@
use crate::secret_validation::SecretFormat;
use crate::utils::error::MultihookResult; use crate::utils::error::MultihookResult;
use config::File; use config::File;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -25,6 +26,13 @@ pub struct EndpointSettings {
pub allow_parallel: bool, pub allow_parallel: bool,
#[serde(default)] #[serde(default)]
pub run_detached: bool, pub run_detached: bool,
pub secret: Option<SecretSettings>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SecretSettings {
pub value: String,
pub format: SecretFormat,
} }
impl Default for Settings { impl Default for Settings {

Loading…
Cancel
Save