From 13012757882de3f135e879c41fd634873d442a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Wed, 10 Jul 2024 13:19:04 +0200 Subject: [PATCH] Compare payload's hmac signature for forgejo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- Cargo.toml | 3 +++ src/error.rs | 23 +++++++++++++++++++++++ src/web.rs | 45 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c6deaee..4ab7052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" toml = "0.8" xmpp = "0.5" +hmac = "0.12" +sha2 = "0.10" +hex = "0.4" [patch.crates-io] forgejo-hooks = { path = "forgejo-hooks" } diff --git a/src/error.rs b/src/error.rs index b7ce3c0..645bb03 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,11 +18,18 @@ use std::error::Error as StdError; use std::io::Error as IoError; use std::str::Utf8Error; +use hex::FromHexError; +use hmac::digest::InvalidLength as HmacInvalidLength; + #[derive(Debug)] pub(crate) enum Error { MethodMismatch, InvalidToken, InvalidContentType, + InvalidSignature, + InvalidRequest, + Hex(FromHexError), + Hmac(HmacInvalidLength), Hyper(hyper::Error), Io(IoError), SerdeJson(serde_json::Error), @@ -39,6 +46,10 @@ impl std::fmt::Display for Error { Error::MethodMismatch => write!(fmt, "the method is invalid"), Error::InvalidToken => write!(fmt, "the token is invalid"), Error::InvalidContentType => write!(fmt, "the content-type is invalid"), + Error::InvalidSignature => write!(fmt, "the signature is invalid"), + Error::InvalidRequest => write!(fmt, "the request is invalid"), + Error::Hex(e) => write!(fmt, "hex error: {}", e), + Error::Hmac(e) => write!(fmt, "hmac error: {}", e), Error::Hyper(e) => write!(fmt, "hyper error: {}", e), Error::Io(e) => write!(fmt, "Io error: {}", e), Error::SerdeJson(e) => write!(fmt, "serde_json error: {}", e), @@ -49,6 +60,18 @@ impl std::fmt::Display for Error { } } +impl From for Error { + fn from(err: FromHexError) -> Error { + Error::Hex(err) + } +} + +impl From for Error { + fn from(err: HmacInvalidLength) -> Error { + Error::Hmac(err) + } +} + impl From for Error { fn from(err: hyper::Error) -> Error { Error::Hyper(err) diff --git a/src/web.rs b/src/web.rs index 19b0e05..05fd547 100644 --- a/src/web.rs +++ b/src/web.rs @@ -17,14 +17,19 @@ use crate::error::Error; use crate::webhook::{ForgejoHook, GitlabHook, Hook}; use std::convert::Infallible; +use std::io::Read; use std::sync::{Arc, Mutex}; use bytes::{Buf, Bytes}; +use hmac::{Hmac, Mac}; use http_body_util::{BodyExt, Full}; use hyper::{body::Incoming, header, Method, Request, Response}; -use log::{debug, error}; +use log::{debug, error, trace}; +use sha2::Sha256; use tokio::sync::mpsc::UnboundedSender; +type HmacSha256 = Hmac; + fn error_res(e: E) -> Result>, Infallible> { error!("error response: {:?}", e); @@ -43,7 +48,8 @@ async fn webhooks_inner(req: Request, token: &str) -> Result, token: &str) -> Result = vec![]; + let whole_body = req.collect().await?.aggregate(); + whole_body.reader().read_to_end(&mut payload)?; + let mut mac = HmacSha256::new_from_slice(token.as_bytes())?; + mac.update(&payload); + let result = mac.finalize().into_bytes(); + trace!("Payload calculated signature: {:?}", hex::encode(result)); + + if let Some(val) = headers.get("X-Forgejo-Signature") { + trace!("Payload advertized signature: {:?}", val); + let slice: Vec = hex::decode(val)?; + + if slice[..] != result[..] { + return Err(Error::InvalidSignature); + } + + let hook: ForgejoHook = serde_json::from_slice(&payload[..])?; + return Ok(Hook::Forgejo(hook)); + } else if let Some(val) = headers.get("X-Gitlab-Token") + && token != val { - return Err(Error::InvalidContentType); + if token != val { + return Err(Error::InvalidToken); + } + + let hook: GitlabHook = serde_json::from_slice(&payload[..])?; + return Ok(Hook::Gitlab(hook)); } - let whole_body = req.collect().await?.aggregate(); - let hook = serde_json::from_reader(whole_body.reader())?; - Ok(Hook::Gitlab(hook)) + // No match found for the payload + Err(Error::InvalidRequest) } pub async fn webhooks(