Skip to content

Commit

Permalink
Add webhook secret fallback for key rotation
Browse files Browse the repository at this point in the history
Signed-off-by: Sergio Castaño Arteaga <[email protected]>
  • Loading branch information
tegioz committed Jun 27, 2024
1 parent 8470994 commit 774d014
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 4 deletions.
2 changes: 1 addition & 1 deletion charts/gitvote/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions charts/gitvote/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions charts/gitvote/templates/gitvote_secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
2 changes: 2 additions & 0 deletions charts/gitvote/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 31 additions & 3 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +29,7 @@ struct RouterState {
gh: DynGH,
cmds_tx: async_channel::Sender<Command>,
webhook_secret: String,
webhook_secret_fallback: Option<String>,
}

/// Setup HTTP server router.
Expand All @@ -40,6 +41,11 @@ pub(crate) fn setup_router(
) -> Result<Router> {
// 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()
Expand All @@ -51,6 +57,7 @@ pub(crate) fn setup_router(
gh,
cmds_tx,
webhook_secret,
webhook_secret_fallback,
});

Ok(router)
Expand All @@ -70,13 +77,17 @@ async fn event(
State(gh): State<DynGH>,
State(cmds_tx): State<async_channel::Sender<Command>>,
State(webhook_secret): State<String>,
State(webhook_secret_fallback): State<Option<String>>,
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()
Expand Down Expand Up @@ -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::<Sha256>::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::<Sha256>::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"))
Expand Down

0 comments on commit 774d014

Please sign in to comment.