Compare commits

...

5 Commits

Author SHA1 Message Date
Tom Alexander b91f63884a
wip 2024-07-20 12:22:10 -04:00
Tom Alexander 4555bb6894
wip 2024-07-20 12:14:00 -04:00
Tom Alexander 8290296926
wip 2024-07-20 12:11:50 -04:00
Tom Alexander a6000833b2
wip 2024-07-20 12:10:32 -04:00
Tom Alexander 7e44e5da4a
Add signature verification middleware. 2024-07-15 22:23:39 -04:00
5 changed files with 124 additions and 24 deletions

14
Cargo.lock generated
View File

@ -339,6 +339,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -567,6 +568,15 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
@ -1890,10 +1900,14 @@ name = "webhook_bridge"
version = "0.0.1"
dependencies = [
"axum",
"base64 0.22.1",
"hmac",
"http-body-util",
"k8s-openapi",
"kube",
"serde",
"serde_json",
"sha2",
"tokio",
"tower-http",
"tracing",

View File

@ -20,12 +20,16 @@ include = [
[dependencies]
# default form, http1, json, matched-path, original-uri, query, tokio, tower-log, tracing
axum = { version = "0.7.5", default-features = false, features = ["tokio", "http1", "http2", "json"] }
base64 = "0.22.1"
hmac = "0.12.1"
http-body-util = "0.1.2"
k8s-openapi = { version = "0.22.0", default-features = false, features = ["v1_30"] }
# default client, config, rustls-tls
kube = { version = "0.92.1", default-features = false, features = ["client", "config", "rustls-tls", "derive", "runtime"] }
serde = { version = "1.0.204", features = ["derive"] }
# default std
serde_json = { version = "1.0.120", default-features = false, features = ["std"] }
sha2 = "0.10.8"
tokio = { version = "1.38.0", default-features = false, features = ["macros", "process", "rt-multi-thread", "signal"] }
tower-http = { version = "0.5.2", default-features = false, features = ["trace", "timeout"] }
# default attributes, std, tracing-attributes

View File

@ -2,6 +2,7 @@
use std::time::Duration;
use axum::http::StatusCode;
use axum::middleware;
use axum::routing::get;
use axum::routing::post;
use axum::Json;
@ -15,6 +16,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use self::webhook::hook;
use self::webhook::verify_signature;
mod hook_push;
mod webhook;
@ -35,15 +37,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.expect("Set KUBECONFIG to a valid kubernetes config.");
let app = Router::new()
.route("/health", get(health))
.route("/hook", post(hook))
.layer(middleware::from_fn(verify_signature))
.route("/health", get(health))
.layer((
TraceLayer::new_for_http(),
// Add a timeout layer so graceful shutdown can't wait forever.
TimeoutLayer::new(Duration::from_secs(600)),
));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
let listener = tokio::net::TcpListener::bind("0.0.0.0:9988").await?;
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())

View File

@ -1,17 +1,29 @@
use std::future::Future;
use axum::async_trait;
use axum::body::Body;
use axum::body::Bytes;
use axum::extract::FromRequest;
use axum::extract::Request;
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum::response::Response;
use axum::Json;
use axum::RequestExt;
use base64::{engine::general_purpose, Engine as _};
use hmac::Hmac;
use hmac::Mac;
use http_body_util::BodyExt;
use serde::Serialize;
use sha2::Sha256;
use tracing::debug;
use crate::hook_push::HookPush;
type HmacSha256 = Hmac<Sha256>;
pub(crate) async fn hook(
_headers: HeaderMap,
payload: HookRequest,
@ -72,3 +84,68 @@ pub(crate) struct HookResponse {
ok: bool,
message: Option<String>,
}
pub(crate) async fn verify_signature(
request: Request,
next: Next,
) -> Result<impl IntoResponse, Response> {
let signature = request
.headers()
.get("X-Gitea-Signature")
.ok_or(StatusCode::BAD_REQUEST.into_response())?;
let signature = signature
.to_str()
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
let signature = hex_to_bytes(signature).ok_or(StatusCode::BAD_REQUEST.into_response())?;
let secret = std::env::var("WEBHOOK_BRIDGE_HMAC_SECRET")
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;
let request =
inspect_request_body(request, move |body| check_hash(body, secret, signature)).await?;
Ok(next.run(request).await)
}
async fn inspect_request_body<F, Fut>(request: Request, inspector: F) -> Result<Request, Response>
where
F: FnOnce(Bytes) -> Fut,
Fut: Future<Output = Result<Bytes, Response>>,
{
let (parts, body) = request.into_parts();
let bytes = body
.collect()
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?
.to_bytes();
let bytes = inspector(bytes).await?;
Ok(Request::from_parts(parts, Body::from(bytes)))
}
async fn check_hash(body: Bytes, secret: String, signature: Vec<u8>) -> Result<Bytes, Response> {
tracing::info!("Checking signature {:02x?}", signature.as_slice());
tracing::info!("Using secret {:?}", secret);
tracing::info!("and body {}", general_purpose::STANDARD.encode(&body));
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?;
mac.update(&body);
mac.verify_slice(&signature)
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()).into_response())?;
Ok(body)
}
fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 == 0 {
(0..s.len())
.step_by(2)
.map(|i| {
s.get(i..i + 2)
.and_then(|sub| u8::from_str_radix(sub, 16).ok())
})
.collect()
} else {
None
}
}

View File

@ -9,14 +9,14 @@ function main() {
payload=$(cat <<EOF
{
"ref": "refs/heads/main",
"before": "25c06cbffd1cd2b372e790732bc8566e575a7f01",
"after": "8fb5a83e8672cf9c317087b70cef329cb604eeed",
"compare_url": "https://code.fizz.buzz/talexander/webhook_bridge/compare/25c06cbffd1cd2b372e790732bc8566e575a7f01...8fb5a83e8672cf9c317087b70cef329cb604eeed",
"before": "2a544017179b6bd125deacb7919566e8b62e73ea",
"after": "7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"compare_url": "https://code.fizz.buzz/talexander/webhook_bridge/compare/2a544017179b6bd125deacb7919566e8b62e73ea...7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"commits": [
{
"id": "8fb5a83e8672cf9c317087b70cef329cb604eeed",
"message": "Access log.\n",
"url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/8fb5a83e8672cf9c317087b70cef329cb604eeed",
"id": "7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"message": "Add signature verification middleware.\n",
"url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"author": {
"name": "Tom Alexander",
"email": "tom@fizz.buzz",
@ -28,21 +28,22 @@ function main() {
"username": ""
},
"verification": null,
"timestamp": "2024-07-14T15:50:13-04:00",
"timestamp": "2024-07-15T21:02:30-04:00",
"added": [],
"removed": [],
"modified": [
"Cargo.lock",
"Cargo.toml",
"src/main.rs"
"src/main.rs",
"src/webhook.rs"
]
}
],
"total_commits": 1,
"head_commit": {
"id": "8fb5a83e8672cf9c317087b70cef329cb604eeed",
"message": "Access log.\n",
"url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/8fb5a83e8672cf9c317087b70cef329cb604eeed",
"id": "7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"message": "Add signature verification middleware.\n",
"url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/7e44e5da4af3fe7e3db39f43d4d7be5b7e92170d",
"author": {
"name": "Tom Alexander",
"email": "tom@fizz.buzz",
@ -54,13 +55,14 @@ function main() {
"username": ""
},
"verification": null,
"timestamp": "2024-07-14T15:50:13-04:00",
"timestamp": "2024-07-15T21:02:30-04:00",
"added": [],
"removed": [],
"modified": [
"Cargo.lock",
"Cargo.toml",
"src/main.rs"
"src/main.rs",
"src/webhook.rs"
]
},
"repository": {
@ -97,7 +99,7 @@ function main() {
"template": false,
"parent": null,
"mirror": false,
"size": 45,
"size": 103,
"language": "",
"languages_url": "https://code.fizz.buzz/api/v1/repos/talexander/webhook_bridge/languages",
"html_url": "https://code.fizz.buzz/talexander/webhook_bridge",
@ -116,7 +118,7 @@ function main() {
"default_branch": "main",
"archived": false,
"created_at": "2024-07-14T18:48:52Z",
"updated_at": "2024-07-14T19:43:45Z",
"updated_at": "2024-07-15T03:11:13Z",
"archived_at": "1970-01-01T00:00:00Z",
"permissions": {
"admin": true,
@ -204,19 +206,19 @@ curl -v \
--http2-prior-knowledge \
-X POST \
-H 'Content-Type: application/json' \
-H 'X-GitHub-Delivery: 2187f277-1104-4011-845b-a88a5a98731f' \
-H 'X-GitHub-Delivery: 04036b0e-6660-4761-896e-e2b6a7631a22' \
-H 'X-GitHub-Event: push' \
-H 'X-GitHub-Event-Type: push' \
-H 'X-Gitea-Delivery: 2187f277-1104-4011-845b-a88a5a98731f' \
-H 'X-Gitea-Delivery: 04036b0e-6660-4761-896e-e2b6a7631a22' \
-H 'X-Gitea-Event: push' \
-H 'X-Gitea-Event-Type: push' \
-H 'X-Gitea-Signature: 5f5ca80269a5cebd41c3ef2de547b86c40b35418fbefc9f59459602435d2d9ea' \
-H 'X-Gogs-Delivery: 2187f277-1104-4011-845b-a88a5a98731f' \
-H 'X-Gitea-Signature: ea67493aae634655aed55474e6df6d075f7d5f48c35542c9d5fa5c56f5ed9e79' \
-H 'X-Gogs-Delivery: 04036b0e-6660-4761-896e-e2b6a7631a22' \
-H 'X-Gogs-Event: push' \
-H 'X-Gogs-Event-Type: push' \
-H 'X-Gogs-Signature: 5f5ca80269a5cebd41c3ef2de547b86c40b35418fbefc9f59459602435d2d9ea' \
-H 'X-Hub-Signature: sha1=6e13417e9a9ce53fc08a684635350aa2c60ce7bc' \
-H 'X-Hub-Signature-256: sha256=5f5ca80269a5cebd41c3ef2de547b86c40b35418fbefc9f59459602435d2d9ea' \
-H 'X-Gogs-Signature: ea67493aae634655aed55474e6df6d075f7d5f48c35542c9d5fa5c56f5ed9e79' \
-H 'X-Hub-Signature: sha1=291432b9e22fe0dbdb8c809e4f5c5557fbdd48d8' \
-H 'X-Hub-Signature-256: sha256=ea67493aae634655aed55474e6df6d075f7d5f48c35542c9d5fa5c56f5ed9e79' \
-d @- \
'http://127.0.0.1:8080/hook' <<<"$payload"
}