rocket_webhook/webhooks/built_in/
stripe.rs

1use hmac::Hmac;
2use rocket::{Request, data::Outcome, http::Status, outcome::try_outcome, tokio::io::AsyncRead};
3use sha2::Sha256;
4use zeroize::Zeroizing;
5
6use crate::{
7    WebhookError,
8    webhooks::{Webhook, interface::hmac::WebhookHmac},
9};
10
11/// # Stripe webhook
12/// Looks for the `Stripe-Signature` header, splits it by `,` and then
13/// reads `t=<timestamp>` and `v1=<hex signature>` (multiple signatures supported).
14///
15/// Signature should be a digest of `<timestamp>.<body>`
16///
17/// [Stripe docs](https://docs.stripe.com/webhooks?verify=verify-manually#verify-manually)
18pub struct StripeWebhook {
19    secret_key: Zeroizing<Vec<u8>>,
20}
21
22impl StripeWebhook {
23    /// Instantiate with the secret key
24    pub fn with_secret(secret_key: impl Into<Vec<u8>>) -> Self {
25        Self {
26            secret_key: Zeroizing::new(secret_key.into()),
27        }
28    }
29}
30
31const SIG_HEADER: &str = "Stripe-Signature";
32
33impl Webhook for StripeWebhook {
34    async fn validate_body(
35        &self,
36        req: &Request<'_>,
37        body: impl AsyncRead + Unpin + Send + Sync,
38        time_bounds: (u32, u32),
39    ) -> Outcome<'_, Vec<u8>, WebhookError> {
40        self.validate_with_hmac(req, body, time_bounds).await
41    }
42}
43
44impl WebhookHmac for StripeWebhook {
45    type MAC = Hmac<Sha256>;
46
47    fn secret_key(&self) -> &[u8] {
48        &self.secret_key
49    }
50
51    fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError> {
52        let header = try_outcome!(self.get_header(req, SIG_HEADER, None));
53        let mut signatures = Vec::new();
54        for hex_sig in header.split(',').filter_map(|s| s.strip_prefix("v1=")) {
55            match hex::decode(hex_sig) {
56                Ok(bytes) => signatures.push(bytes),
57                Err(_) => {
58                    return Outcome::Error((
59                        Status::BadRequest,
60                        WebhookError::InvalidHeader(format!(
61                            "Signature in {SIG_HEADER} header was not valid hex: '{hex_sig}'"
62                        )),
63                    ));
64                }
65            };
66        }
67
68        Outcome::Success(signatures)
69    }
70
71    fn body_prefix(
72        &self,
73        req: &Request<'_>,
74        time_bounds: (u32, u32),
75    ) -> Outcome<'_, Option<Vec<u8>>, WebhookError> {
76        let sig_header = try_outcome!(self.get_header(req, SIG_HEADER, None));
77        let Some(timestamp) = sig_header
78            .split(',')
79            .find_map(|part| part.strip_prefix("t="))
80        else {
81            return Outcome::Error((
82                Status::BadRequest,
83                WebhookError::InvalidHeader(format!(
84                    "Did not find timestamp in header: '{sig_header}'"
85                )),
86            ));
87        };
88        try_outcome!(self.validate_timestamp(timestamp, time_bounds));
89
90        let prefix = [timestamp.as_bytes(), b"."].concat();
91        Outcome::Success(Some(prefix))
92    }
93}