Validating webhook request signatures
Unit21 provides the ability to verify incoming webhooks by HMAC-SHA256 with a random 20-character key that is unique per-webhook.
This key is visible to Agents in the Unit21 web app when configuring webhooks:
You can copy the secret key by using the two buttons next to it.
Sample request headers
{
"content-length": "452",
"content-type": "application/json",
"unit21-signature": "t=1582702424,s0=cbe7c96a57abff5e43e1b4b394f85ff6100d062f5bf551142d0527ae3213cfcc"
}
{
"unit21_id": 123,
"object_type": "ALERT",
"alert_id": null,
"alert_type": "kyc",
"change": "CLOSED",
"change_time": 1570062682,
"disposition": "FALSE_POSITIVE",
"status": "CLOSED",
"changed_by": "[email protected]",
"title": "Sample alert title",
"description": "Sample alert description",
"start_date": 1570052582,
"end_date": 1570062582,
"entities": [{"entity_id": "entity-886313e1", "entity_type": "user", "unit21_id": 46, "resolution": null}, {"entity_id": "entity-91a31re2", "entity_type": "user", "unit21_id": 72, "resolution": "false_positive"}],
"events": [{"event_id": "event-1063e4e3e1", "event_type": "transaction", "unit21_id": 111, "resolution": null}],
"instruments": [{"instrument_id": "instrument-4112950a", "instrument_type": "wallet", "unit21_id": 401, "resolution": null}],
"triggered_by_rules": [{"unit21_id": 6, "rule_id": null}],
"assigned_to": "[email protected]",
"tags": ["rule_type:layering"],
"custom_data": {"internal_priority": 3}
}
Verifying requests
Requests can be verified by using the Unit21-Signature
HTTP header, which is sent in the format: unit21-signature: t=<timestamp>,s0=<signature>
where:
--<timestamp>
is an integer representing Unix time when the payload was sent, and
--<signature>
is a signature generated as follows:
- Take the timestamp value from
t
and prepend it to the request body with a.
- Generate a signature using
HMAC-SHA256
with the secret key provided in the application console
Given t=100
and a request body of '{"foo": "bar", "baz": "foo"}'
the value of s0
would be the signature of 100.{"foo": "bar", "baz": "foo"}
with the secret key.
Sample Python code to validate a signature:
import hashlib
import hmac
def verify_signature(secret_key: str, signature_header: str, request_body: str) -> bool:
# Extract the timestamp from signature_header
timestamp = signature_header.split(",")[0].split("=")[1]
# Extract the signature from signature_header
signature = signature_header.split(",")[1].split("=")[1]
# Construct the expected signature using the timestamp and request_body
signature_payload = f"{timestamp}.{request_body}"
expected_signature = hmac.new(
key=str.encode(secret_key),
msg=str.encode(signature_payload),
digestmod=hashlib.sha256,
).hexdigest()
# Confirm that the signature is as expected
return signature == expected_signature
u21_secret_key = "5b010867f0aeaa8c75b6"
body = '{"foo": "bar", "baz": "foo"}'
header = "t=1676417774,s0=1de43c487e72e51b74b83216cde0c6f6c990f3254585e855c71ec235473578bc"
assert verify_signature(
secret_key=u21_secret_key,
signature_header=header,
request_body=body,
)
A deeper look:
The unit21-signature
header contains a timestamp and a signature. The signature (s0) is a hex code generated by using a per-endpoint secret key found on your unit21 webhooks dashboard as a hash key and a message created by concatenating the following in order:
- timestamp (t) seen in the signature as a string
- The
.
character - and the actual request body as an un-prettified JSON string
The sample on the right would be represented by this pre-hashed string:
1582702424.{"unit21_id": 123, "object_type": "ALERT", "alert_id": null, "alert_type": "kyc", "change": "CLOSED", "change_time": 1570062682, "disposition": "FALSE_POSITIVE", "status": "CLOSED", "changed_by": "[email protected]", "title": "Sample alert title", "description": "Sample alert description", "start_date": 1570052582, "end_date": 1570062582, "entities": [{"entity_id": "entity-886313e1", "entity_type": "user", "unit21_id": 46}, {"entity_id": "entity-91a31re2", "entity_type": "user", "unit21_id": 72, "resolution": "false_positive"}], "events": [{"event_id": "event-1063e4e3e1", "event_type": "transaction", "unit21_id": 111}], "instruments": [{"instrument_id": "instrument-4112950a", "instrument_type": "wallet", "unit21_id": 401}], "triggered_by_rules": [{"unit21_id": 6, "rule_id": null}], "assigned_to": "[email protected]", "tags": ["rule_type:layering"], "custom_data": {"internal_priority": 3}}
Using secret key 4acff285d1de621a4077
outputs the s0 signature:
c2c035b8759cb142441e7c3a40a7571adbdbb03a63d84c117c6772432e7c1bd9
Note that the secret key and concatenated body are used as UTF-8 encoded strings when generating the HMAC and the digest algorithm is SHA256