Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 1, 2025

HTTP shovels can now sign webhook requests with HMAC-SHA256, enabling endpoints to verify request authenticity. Compatible with GitHub/PagerDuty signature formats.

Implementation

  • HTTPDestination: Added optional signature_secret parameter, computes HMAC-SHA256 of body and adds X-LavinMQ-Signature-256: sha256=<hex> header
  • ShovelStore: Parses dest-signature-secret from configuration
  • Performance: Body only buffered when signature enabled; otherwise streamed as before

Configuration

{
  "src-uri": "amqp://localhost",
  "src-queue": "events",  
  "dest-uri": "https://example.com/webhook",
  "dest-signature-secret": "my-secret-key"
}

Verification Example (Python)

import hmac, hashlib

signature = request.headers.get('X-LavinMQ-Signature-256')[7:]  # Remove 'sha256='
body = request.get_data()
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(signature, expected)

Additional verification examples for Node.js and Go included in WEBHOOK_SIGNATURE_VERIFICATION.md.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • moment.github.io
    • Triggering command: curl --fail --retry 5 -sLo static/js/lib/luxon.js REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Webhook signature verification support</issue_title>
<issue_description>Webhook endpoint invoked by lavinmq should be able to verify that it in fact was their LavinMQ broker that made the request. See these api docs for reference:
https://developer.pagerduty.com/docs/verifying-webhook-signatures
https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Add webhook signature verification support Add HMAC-SHA256 signature verification for HTTP webhook shovels Nov 1, 2025
Copilot AI requested a review from baelter November 1, 2025 21:14
@baelter baelter marked this pull request as ready for review November 1, 2025 22:08
@baelter baelter requested a review from a team as a code owner November 1, 2025 22:08
@github-actions
Copy link

github-actions bot commented Nov 1, 2025


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


0 out of 2 committers have signed the CLA.
@copilot
@baelter
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

Comment on lines 365 to 366
# Note: This requires reading the entire body into memory to compute HMAC.
# For webhook use cases, messages are typically small event notifications.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of this there should probably be a (configurable?) max size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

if secret = @signature_secret
body = msg.body_io.gets_to_end
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Algorithm::SHA256, secret, body)
headers["X-LavinMQ-Signature-256"] = "sha256=#{signature}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it called "LavinMQ-Signature-256"? it's nothing "lavinmq" about it, and sha256 is already in the value, why not just X-HMAC-Signature?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, looking on other implementations of this I see that there's no real standard, hence the product name in the header, and then you have to look up the docs for that header. Well, then either 256 in the key or the sha256= in the value should at least go away.

I also liked that slack included the date/time in the signature to prevent reply attacks. Have we considered that? (But instead of adding a custom time/date header, why not just use the standard Date header?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key rotation is also not supported, as PagerDuty does

Comment on lines 365 to 366
# Note: This requires reading the entire body into memory to compute HMAC.
# For webhook use cases, messages are typically small event notifications.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all caps in the filename doesn't align with how we normally name files, and should it be in the root dir?

Could it be shorter? It's very verbose right now, maybe works better as a blog post. And just short summary/note in the README instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 short note in README + blog post

@carlhoerberg carlhoerberg marked this pull request as draft November 2, 2025 21:34
Copilot AI and others added 7 commits December 10, 2025 14:54
Switch from custom X-LavinMQ-Signature-256 header to Standard Webhooks
specification (standardwebhooks.com) format with three headers:
- webhook-id: Unique message ID (msg_<uuid>)
- webhook-timestamp: Unix timestamp in seconds
- webhook-signature: HMAC-SHA256 as v1,<base64>

Signature now covers "{id}.{timestamp}.{body}" which enables replay
attack prevention through timestamp validation.
@baelter baelter force-pushed the copilot/add-webhook-signature-verification branch from 49a3c63 to bcb1d82 Compare December 10, 2025 14:02
@claude
Copy link

claude bot commented Dec 10, 2025

Claude finished @copilot's task —— View job


Code Review Analysis

Reviewing PR for problems that need fixing:

  • Read repository guidelines (CLAUDE.md)
  • Examine core implementation files for issues
  • Check for bugs, security vulnerabilities, performance issues
  • Verify error handling and Crystal best practices
  • Report findings

Issues Found

1. Timing Attack Vulnerability - shovel.cr:380

The timestamp generation uses RoughTime.unix_ms // 1000 which provides timestamps with 100ms precision due to RoughTime's internal rounding (@@unix_ms = @@utc.to_unix_ms // 100 * 100). This makes the system vulnerable to timing attacks where attackers can predict valid timestamp windows.

Risk: An attacker could potentially replay requests within the 100ms window or brute force signatures knowing the reduced timestamp precision.

Fix: Use Time.utc.to_unix for full precision timestamps.

2. Missing Replay Attack Protection - shovel.cr:383-390

The signature verification follows Standard Webhooks format but lacks replay attack protection. Unlike GitHub/PagerDuty implementations, there's no timestamp validation or nonce tracking.

Risk: Valid webhook requests can be captured and replayed indefinitely.

Recommendation: Add timestamp validation (reject requests older than 5 minutes) and consider nonce tracking for critical applications.

3. Memory Exhaustion Risk - shovel.cr:352-356

While there's a size check for @max_signed_webhook_payload, the default 16MB limit is quite high and the error handling rejects without requeue, which could mask legitimate large messages.

Risk: Memory pressure from repeatedly processing large messages before rejection.

Improvement: Consider logging the rejection reason and potentially implementing backoff for repeated large messages.

4. Crystal Anti-pattern: Force Unwrapping - shovel.cr:358

Using msg.body_io.gets_to_end without proper error handling if the IO is closed or corrupted.

Risk: Could raise unexpected exceptions leading to shovel crashes.

Fix: Add proper IO error handling around body reading.

No issues found with the cryptographic implementation - HMAC-SHA256 usage and Base64 encoding are correct.

Remove standalone WEBHOOK_SIGNATURE_VERIFICATION.md and add a short
mention of Standard Webhooks signature support in the README features
list. Detailed documentation will be covered in a blog post.
- Add DEFAULT_MAX_SIGNED_WEBHOOK_PAYLOAD constant (16MB) in shovel/constants.cr
- Add max_signed_webhook_payload config option with INI parsing support
- Add AMQPSource#reject method for properly rejecting messages without requeue
- Add payload size validation in HTTPDestination before reading into memory
- Messages exceeding limit are logged and rejected without requeue
- Add comprehensive test coverage for payload size validation
- Fix default_user_only_loopback property definition (remove ?)
- Fix generate_timestamp to use integer division (//)
@baelter baelter force-pushed the copilot/add-webhook-signature-verification branch 2 times, most recently from 940c20d to 376a484 Compare December 11, 2025 09:03
@baelter baelter force-pushed the copilot/add-webhook-signature-verification branch from 376a484 to d278508 Compare December 11, 2025 09:04
Add webhook_server helper method to ShovelSpecHelpers that:
- Creates and starts an HTTP server for testing webhooks
- Captures headers, body, and path from requests
- Supports both String and Channel for body (for async tests)
- Returns server, address, and captured values as tuple

Replace 5 duplicated HTTP server setup blocks with helper calls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhook signature verification support

5 participants