@@ -812,23 +812,43 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
812812 """Async context manager exit."""
813813 await self .close ()
814814
815- def _verify_webhook_signature (self , payload : dict [str , Any ], signature : str ) -> bool :
815+ def _verify_webhook_signature (
816+ self , payload : dict [str , Any ], signature : str , timestamp : str
817+ ) -> bool :
816818 """
817819 Verify HMAC-SHA256 signature of webhook payload.
818820
821+ The verification algorithm matches get_adcp_signed_headers_for_webhook:
822+ 1. Constructs message as "{timestamp}.{json_payload}"
823+ 2. JSON-serializes payload with compact separators
824+ 3. UTF-8 encodes the message
825+ 4. HMAC-SHA256 signs with the shared secret
826+ 5. Compares against the provided signature (with "sha256=" prefix stripped)
827+
819828 Args:
820829 payload: Webhook payload dict
821- signature: Signature to verify
830+ signature: Signature to verify (with or without "sha256=" prefix)
831+ timestamp: ISO 8601 timestamp from X-AdCP-Timestamp header
822832
823833 Returns:
824834 True if signature is valid, False otherwise
825835 """
826836 if not self .webhook_secret :
827837 return True
828838
829- payload_bytes = json .dumps (payload , separators = ("," , ":" ), sort_keys = True ).encode ("utf-8" )
839+ # Strip "sha256=" prefix if present
840+ if signature .startswith ("sha256=" ):
841+ signature = signature [7 :]
842+
843+ # Serialize payload to JSON with consistent formatting (matches signing)
844+ payload_bytes = json .dumps (payload , separators = ("," , ":" ), sort_keys = False ).encode ("utf-8" )
845+
846+ # Construct signed message: timestamp.payload (matches get_adcp_signed_headers_for_webhook)
847+ signed_message = f"{ timestamp } .{ payload_bytes .decode ('utf-8' )} "
848+
849+ # Generate expected signature
830850 expected_signature = hmac .new (
831- self .webhook_secret .encode ("utf-8" ), payload_bytes , hashlib .sha256
851+ self .webhook_secret .encode ("utf-8" ), signed_message . encode ( "utf-8" ) , hashlib .sha256
832852 ).hexdigest ()
833853
834854 return hmac .compare_digest (signature , expected_signature )
@@ -941,6 +961,7 @@ async def _handle_mcp_webhook(
941961 task_type : str ,
942962 operation_id : str ,
943963 signature : str | None ,
964+ timestamp : str | None = None ,
944965 ) -> TaskResult [AdcpAsyncResponseData ]:
945966 """
946967 Handle MCP webhook delivered via HTTP POST.
@@ -949,7 +970,8 @@ async def _handle_mcp_webhook(
949970 payload: Webhook payload dict
950971 task_type: Task type from application routing
951972 operation_id: Operation identifier from application routing
952- signature: Optional HMAC-SHA256 signature for verification
973+ signature: Optional HMAC-SHA256 signature for verification (X-AdCP-Signature header)
974+ timestamp: Optional timestamp for signature verification (X-AdCP-Timestamp header)
953975
954976 Returns:
955977 TaskResult with parsed task-specific response data
@@ -960,8 +982,10 @@ async def _handle_mcp_webhook(
960982 """
961983 from adcp .types .generated_poc .core .mcp_webhook_payload import McpWebhookPayload
962984
963- # Verify signature before processing
964- if signature and not self ._verify_webhook_signature (payload , signature ):
985+ # Verify signature before processing (requires both signature and timestamp)
986+ if signature and timestamp and not self ._verify_webhook_signature (
987+ payload , signature , timestamp
988+ ):
965989 logger .warning (
966990 f"Webhook signature verification failed for agent { self .agent_config .id } "
967991 )
@@ -1151,6 +1175,7 @@ async def handle_webhook(
11511175 task_type : str ,
11521176 operation_id : str ,
11531177 signature : str | None = None ,
1178+ timestamp : str | None = None ,
11541179 ) -> TaskResult [AdcpAsyncResponseData ]:
11551180 """
11561181 Handle incoming webhook and return typed result.
@@ -1176,8 +1201,10 @@ async def handle_webhook(
11761201 /webhook/{task_type}/{agent_id}/{operation_id}
11771202 operation_id: Operation identifier from application routing.
11781203 Used to correlate webhook notifications with original task submission.
1179- signature: Optional HMAC-SHA256 signature for MCP webhook verification.
1180- Ignored for A2A webhooks (they use authenticated connections).
1204+ signature: Optional HMAC-SHA256 signature for MCP webhook verification
1205+ (X-AdCP-Signature header). Ignored for A2A webhooks.
1206+ timestamp: Optional timestamp for MCP webhook signature verification
1207+ (X-AdCP-Timestamp header). Required when signature is provided.
11811208
11821209 Returns:
11831210 TaskResult with parsed task-specific response data. The structure
@@ -1197,9 +1224,10 @@ async def handle_webhook(
11971224 >>> @app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
11981225 >>> async def webhook_handler(task_type: str, operation_id: str, request: Request):
11991226 >>> payload = await request.json()
1200- >>> signature = request.headers.get("X-Signature")
1227+ >>> signature = request.headers.get("X-AdCP-Signature")
1228+ >>> timestamp = request.headers.get("X-AdCP-Timestamp")
12011229 >>> result = await client.handle_webhook(
1202- >>> payload, task_type, operation_id, signature
1230+ >>> payload, task_type, operation_id, signature, timestamp
12031231 >>> )
12041232 >>> if result.success:
12051233 >>> print(f"Task completed: {result.data}")
@@ -1232,7 +1260,9 @@ async def handle_webhook(
12321260 return await self ._handle_a2a_webhook (payload , task_type , operation_id )
12331261 else :
12341262 # MCP webhook (dict payload)
1235- return await self ._handle_mcp_webhook (payload , task_type , operation_id , signature )
1263+ return await self ._handle_mcp_webhook (
1264+ payload , task_type , operation_id , signature , timestamp
1265+ )
12361266
12371267
12381268class ADCPMultiAgentClient :
0 commit comments