@@ -193,3 +193,60 @@ def toggle_customer_status(self, params: ToggleCustomerStatusParams) -> ToggleCu
193193 query_params = {'customerId' : params ['customerId' ]}
194194 return self ._handle_request ('POST' , '/customer/disable' , params = None , query_params = query_params )
195195
196+ @staticmethod
197+ def verify_webhook_signature (payload : str , header : str , secret : str , tolerance_seconds : int = 300 ) -> bool :
198+ """
199+ Verifies a webhook payload signature received from Keymint.
200+ :param payload: The raw request body as a string.
201+ :param header: The value of the "Keymint-Signature" header.
202+ :param secret: The webhook endpoint's signing secret.
203+ :param tolerance_seconds: Time tolerance in seconds to prevent replay attacks. Defaults to 300 (5 minutes).
204+ :returns: True if the signature is valid, False otherwise.
205+ """
206+ import hmac
207+ import hashlib
208+ import time
209+
210+ if not header or not secret :
211+ return False
212+
213+ try :
214+ # Parse header (e.g. t=1719374021,v1=signature)
215+ timestamp_str = ""
216+ signature = ""
217+ parts = header .split ("," )
218+ for part in parts :
219+ kv = part .strip ().split ("=" , 1 )
220+ if len (kv ) == 2 :
221+ if kv [0 ] == "t" :
222+ timestamp_str = kv [1 ]
223+ elif kv [0 ] == "v1" :
224+ signature = kv [1 ]
225+
226+ if not timestamp_str or not signature :
227+ return False
228+
229+ # Check timestamp validity
230+ try :
231+ timestamp_int = int (timestamp_str )
232+ except ValueError :
233+ return False
234+
235+ now = int (time .time ())
236+ if abs (now - timestamp_int ) > tolerance_seconds :
237+ return False
238+
239+ # Verify HMAC signature
240+ signable_content = f"{ timestamp_str } .{ payload } " .encode ("utf-8" )
241+ expected_signature = hmac .new (
242+ secret .encode ("utf-8" ),
243+ signable_content ,
244+ hashlib .sha256
245+ ).hexdigest ()
246+
247+ # Constant-time comparison to prevent timing attacks
248+ return hmac .compare_digest (expected_signature , signature )
249+ except Exception :
250+ return False
251+
252+
0 commit comments