github_webhook_extract/
lib.rs

1//! # Github Webhook
2//!
3//! Contains types for github webhooks
4//!
5//! ## Features
6//!
7//! * axum: enable the axum feature to get extractor implementations
8
9#[cfg(feature = "axum")]
10mod axum;
11mod types;
12
13use bytes::Bytes;
14use std::env;
15pub use types::*;
16
17use digest::CtOutput;
18use generic_array::GenericArray;
19use hmac::{Hmac, Mac};
20use sha1::Sha1;
21use sha2::Sha256;
22use thiserror::Error;
23use tracing::instrument;
24use uuid::Uuid;
25
26/// Verify and parse a github payload. Pass your parsed
27/// data from your web library for parsing and return errors.
28///
29/// ## fields:
30/// * `guid`: value of `X-GitHub-Delivery` header
31/// * `signature_sha1`: value of `X-Hub-Signature` header
32/// * `signature_sha256`: value of `X-Hub-Signature-256` header
33/// * `bytes`: raw body of the request
34/// * `json`: body of the request in json form
35#[instrument(skip_all)]
36pub fn verify(
37    guid: Uuid,
38    signature_sha1: Option<String>,
39    signature_sha256: Option<String>,
40    bytes: Bytes,
41    json: &str,
42) -> Result<GithubPayload, VerifyError> {
43    // verify signatures
44    match (&signature_sha1, &signature_sha256) {
45        (Some(sha1), None) => {
46            tracing::debug!("using sha1");
47            let token = env::var("GITHUB_TOKEN").map_err(|_| {
48                tracing::error!("secret github token is missing");
49                VerifyError::TokenMissing
50            })?;
51
52            let mut mac = Hmac::<Sha1>::new_from_slice(token.as_bytes()).map_err(|e| {
53                tracing::error!("error creating hmac: {:?}", e);
54                VerifyError::HmacCreation
55            })?;
56            mac.update(&bytes);
57            let result = mac.finalize();
58            let signature = hex::decode(sha1.split_once('=').ok_or(VerifyError::Sha1ParseError)?.1)
59                .map_err(|e| {
60                    tracing::debug!(?e);
61                    VerifyError::HexParseError
62                })?;
63
64            if result != CtOutput::new(*GenericArray::from_slice(&signature)) {
65                return Err(VerifyError::NotVerified);
66            }
67        }
68        (_, Some(sha256)) => {
69            tracing::debug!("using sha256");
70            let token = env::var("GITHUB_TOKEN").map_err(|_| {
71                tracing::error!("secret github token is missing");
72                VerifyError::TokenMissing
73            })?;
74
75            let mut mac = Hmac::<Sha256>::new_from_slice(token.as_bytes()).map_err(|e| {
76                tracing::error!("error creating hmac: {:?}", e);
77                VerifyError::HmacCreation
78            })?;
79            mac.update(&bytes);
80            let result = mac.finalize();
81            let signature = hex::decode(
82                sha256
83                    .split_once('=')
84                    .ok_or(VerifyError::Sha256ParseError)?
85                    .1,
86            )
87            .map_err(|e| {
88                tracing::debug!(?e);
89                VerifyError::HexParseError
90            })?;
91
92            if result != CtOutput::new(*GenericArray::from_slice(&signature)) {
93                return Err(VerifyError::NotVerified);
94            }
95        }
96        (None, None) => tracing::debug!("no signature verification"),
97    }
98
99    let deserializer = &mut serde_json::Deserializer::from_str(json);
100    let event: Event = serde_path_to_error::deserialize(deserializer).map_err(|e| {
101        tracing::warn!("failed to deserialize event: {}", e);
102        VerifyError::EventParseError
103    })?;
104
105    tracing::debug!("finished extracting github payload");
106    Ok(GithubPayload {
107        guid,
108        signature_sha1,
109        signature_sha256,
110        event,
111    })
112}
113
114/// Error verifying a github payload
115#[derive(Debug, Error)]
116pub enum VerifyError {
117    #[error("github token not found in environment")]
118    TokenMissing,
119    #[error("could not create hmac")]
120    HmacCreation,
121    #[error("could not parse sha1 header")]
122    Sha1ParseError,
123    #[error("could not parse sha256 header")]
124    Sha256ParseError,
125    #[error("could not parse hex signature")]
126    HexParseError,
127    #[error("payload not verified correctly")]
128    NotVerified,
129    #[error("could not parse event")]
130    EventParseError,
131}