Repository navigation
Signed webhooks: Standard Webhooks signatures, key rotation, key screens, receiver docs (#5632 Phase C C2) #5998
Description
Claude posting for Erik Darling.
Part of #5632 (Phase C, wave 1).
What it does
Signed webhooks, security-sensitive. The generic webhook gets Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature, HMAC-SHA256 over id.timestamp.body). Darling generates the key and shows it once. Rotation keeps two keys live for 7 days and sends both signatures. The viewer and the web page get key screens. The receiver docs carry a starter sample check that enforces the 5-minute timestamp window and dedupes on the id.
Security: the signing key is sealed at rest the same way the existing bearer token is. The bearer token lives in generic_headers, sealed with PasswordSeal under a binding that names the slot, the row, the channel proxy and the channel URL (Darling/PerformanceMonitor.Darling.Storage/PasswordBinding.cs:43-45, WebhookSlots; opened at Darling/PerformanceMonitor.Darling.Service/DarlingWebhookSecrets.cs:65, and per route at :80). The signing key takes a new slot in that same path: sealed with the password key ring on save, opened by DarlingWebhookSecrets.Open, and a key that will not open turns the generic channel off for that destination with a health text and never sends unsigned (the rule at DarlingWebhookSecrets.cs:51 for headers). The key never appears in logs, the journal, health text, MCP output or the diagnostics bundle (DiagnosticsBundleWebhookOpenTests.cs is the pattern). The viewer shows it once at generation and never again; a saved value reads back as "saved", the way ViewerWebhookSealing.IsSaved does.
Decisions this slice carries (agreed by Erik on 2026-10-16)
- D1 (A). "Standard Webhooks:
webhook-id,webhook-timestamp,webhook-signature(HMAC-SHA256 overid.timestamp.body). Receivers have libraries in most languages." The id header doubles as D6's event id. - D2 (A). "One sealed key per generic channel and per route with its own URL, in the existing sealed-value path. One more sealed field. Darling generates it and shows it once."
- D3 (A). "Two keys live for a 7-day overlap, both signatures sent. Receivers keep working while they update. Replay: a 5-minute timestamp window and the id as dedupe key, enforced by the receiver (docs and the starter sample)."
- D4 (A). "Generic webhook only, on once a key exists. Teams, Slack and PagerDuty have their own auth."
Files to touch
Pointers re-checked on dev at c12233ce3.
PerformanceMonitor.Notifications/WebhookAlertService.cs:TrySendGenericAlertAsyncat 2152; the operator headers are applied only to the generic URL's own origin at 2172-2180 (HeadersApplyTodefined at 2236);BuildGenericPayloadat 2335; the shared sendPostWebhookAsyncat 3390 and 3402 andApplyHeadersat 3462. The signature must be computed over the exact body bytes that go on the wire. Route sends reachTrySendGenericAlertAsyncthrough the call at 2865. Teams and Slack sharePostWebhookAsyncand must carry no signing headers (headersisnullfor them).Darling/PerformanceMonitor.Darling.Storage/PasswordBinding.cs:43-45(WebhookSlots),Darling/PerformanceMonitor.Darling.Service/DarlingWebhookSecrets.cs:47(class),:65(settings row),:80(route rows),Darling/PerformanceMonitor.Darling.Viewer/ViewerWebhookSealing.cs:23(save-side sealing, route sealing near line 148).NotificationRouteatPerformanceMonitor.Notifications/NotificationRoute.cs:53(a route carriesGenericUrlbut no headers).- The viewer route dialog
Darling/PerformanceMonitor.Darling.Viewer/NotificationRouteEditDialog.xaml(.cs), the notification settings in the viewer, and the web endpoints inDarling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs. - Docs: add the receiver doc and the sample check where the existing webhook docs live.
- Columns come from Phase C store: the one migration, grants and retention (#5632 Phase C C1) #5997; do not add a migration.
Depends on
#5997 (the key and previous-key columns).
Behaviour to pin
- A receiver recomputing the HMAC accepts a send.
- One changed byte in the body fails the check.
- Both keys verify during the rotation overlap, and only the new key after 7 days.
- The sample check rejects a 6-minute-old timestamp.
- Teams and Slack sends carry no signing headers.
- A route URL is signed with the route's key, not the settings row's.
- A signing key that will not open turns the channel off and does not send unsigned.
- The key never appears in health text or logs.
Tests
Behaviour pins only, one test per behaviour listed above. No census, roster or source-text test (the #5833 ruling in .github/REVIEW-TRAPS.md).
Notes
- Add the new sealed slot to
PasswordBindingCensusTestsonly as far as the existing pins need it to compile; add no new census test. - Nothing under
deprecated/. - At most four parts. If the work needs more, stop and say so in the PR body rather than growing the slice.
- Report the changelog entry in the PR body (the coordinator splices it after the wave; do not edit
CHANGELOG.md).
Activity
Signed webhooks: the viewer key blocks and the web key status (#5998)
Signed webhooks: security review round 1 fixes (#5998)
Signed webhooks: security review round 2 fixes (#5998)
Signed webhooks: fix the two CI failures (#5998)
Signed generic webhooks: sealed keys, Standard Webhooks signatures, 7…
erikdarlingdata commentedon Oct 17, 2026
Claude posting for Erik Darling.
Fixed by #6028 (adc6b31) on dev.