From d16c4f89258508a4cb273b6dcced8f30e56faf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Thu, 27 Jun 2024 10:58:11 +0200 Subject: [PATCH] Add webhook secret fallback for key rotation (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio CastaƱo Arteaga --- charts/gitvote/Chart.yaml | 2 +- charts/gitvote/README.md | 1 + charts/gitvote/templates/gitvote_secret.yaml | 1 + charts/gitvote/values.yaml | 2 ++ src/handlers.rs | 34 ++++++++++++++++++-- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/charts/gitvote/Chart.yaml b/charts/gitvote/Chart.yaml index ae4a454..a21291e 100644 --- a/charts/gitvote/Chart.yaml +++ b/charts/gitvote/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: gitvote description: GitVote is a GitHub application that allows holding a vote on issues and pull requests type: application -version: 1.2.1-0 +version: 1.2.1-1 appVersion: 1.2.0 kubeVersion: ">= 1.19.0-0" home: https://gitvote.dev diff --git a/charts/gitvote/README.md b/charts/gitvote/README.md index 75ac1b4..9768a55 100644 --- a/charts/gitvote/README.md +++ b/charts/gitvote/README.md @@ -48,6 +48,7 @@ gitvote: ... -----END RSA PRIVATE KEY----- webhookSecret: "your-webhook-secret" + webhookSecretFallback: "old-webhook-secret" # Handy for webhook secret rotation ``` To install the chart with the release name `my-gitvote` run: diff --git a/charts/gitvote/templates/gitvote_secret.yaml b/charts/gitvote/templates/gitvote_secret.yaml index 91b6d57..67844b0 100644 --- a/charts/gitvote/templates/gitvote_secret.yaml +++ b/charts/gitvote/templates/gitvote_secret.yaml @@ -18,3 +18,4 @@ stringData: appID: {{ .Values.gitvote.github.appID }} appPrivateKey: {{ .Values.gitvote.github.appPrivateKey | quote }} webhookSecret: {{ .Values.gitvote.github.webhookSecret | quote }} + webhookSecretFallback: {{ .Values.gitvote.github.webhookSecretFallback | quote }} diff --git a/charts/gitvote/values.yaml b/charts/gitvote/values.yaml index f042d48..7943070 100644 --- a/charts/gitvote/values.yaml +++ b/charts/gitvote/values.yaml @@ -54,6 +54,8 @@ gitvote: appPrivateKey: null # GitHub application webhook secret webhookSecret: null + # GitHub application webhook secret fallback (handy for webhook secret rotation) + webhookSecretFallback: null # Ingress configuration ingress: diff --git a/src/handlers.rs b/src/handlers.rs index 309215c..2edc9c8 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -8,7 +8,7 @@ use axum::{ routing::{get, post}, Router, }; -use config::Config; +use config::{Config, ConfigError}; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::sync::Arc; @@ -29,6 +29,7 @@ struct RouterState { gh: DynGH, cmds_tx: async_channel::Sender, webhook_secret: String, + webhook_secret_fallback: Option, } /// Setup HTTP server router. @@ -40,6 +41,11 @@ pub(crate) fn setup_router( ) -> Result { // Setup webhook secret let webhook_secret = cfg.get_string("github.webhookSecret")?; + let webhook_secret_fallback = match cfg.get_string("github.webhookSecretFallback") { + Ok(secret) => Some(secret), + Err(ConfigError::NotFound(_)) => None, + Err(err) => return Err(err.into()), + }; // Setup router let router = Router::new() @@ -51,6 +57,7 @@ pub(crate) fn setup_router( gh, cmds_tx, webhook_secret, + webhook_secret_fallback, }); Ok(router) @@ -70,13 +77,17 @@ async fn event( State(gh): State, State(cmds_tx): State>, State(webhook_secret): State, + State(webhook_secret_fallback): State>, headers: HeaderMap, body: Bytes, ) -> impl IntoResponse { // Verify payload signature + let webhook_secret = webhook_secret.as_bytes(); + let webhook_secret_fallback = webhook_secret_fallback.as_ref().map(String::as_bytes); if verify_signature( headers.get(GITHUB_SIGNATURE_HEADER), - webhook_secret.as_bytes(), + webhook_secret, + webhook_secret_fallback, &body[..], ) .is_err() @@ -117,14 +128,31 @@ async fn event( } /// Verify that the signature provided is valid. -fn verify_signature(signature: Option<&HeaderValue>, secret: &[u8], body: &[u8]) -> Result<()> { +fn verify_signature( + signature: Option<&HeaderValue>, + secret: &[u8], + secret_fallback: Option<&[u8]>, + body: &[u8], +) -> Result<()> { if let Some(signature) = signature .and_then(|s| s.to_str().ok()) .and_then(|s| s.strip_prefix("sha256=")) .and_then(|s| hex::decode(s).ok()) { + // Try primary secret let mut mac = Hmac::::new_from_slice(secret)?; mac.update(body); + let result = mac.verify_slice(&signature[..]); + if result.is_ok() { + return Ok(()); + } + if secret_fallback.is_none() { + return result.map_err(Error::new); + } + + // Try fallback secret (if available) + let mut mac = Hmac::::new_from_slice(secret_fallback.expect("secret should be set"))?; + mac.update(body); mac.verify_slice(&signature[..]).map_err(Error::new) } else { Err(format_err!("no valid signature found"))