Every webhook notification sent by CPN is digitally signed with an asymmetric
key. This guide demonstrates how to use the key and signature to verify that a
webhook notification was sent by Circle. Validating webhooks in this way can
reduce the risk of person-in-the-middle attacks on your subscriber endpoint.
Steps
Use the following steps to verify the Circle signature on a webhook
notification.
Step 1: Get the digital signature and ID of the notification
Every webhook notification is digitally signed with an asymmetric key. The
asymmetric key is random for each webhook, so you must perform this full
authentication flow to validate the key. This signature is available in the
header of the message. Each message contains the following headers:
X-Circle-Signature: the digital signature generated by Circle
X-Circle-Key-Id: the public key ID in UUID format
Extract those values from the header of the webhook message.
Step 2: Get the public key and encryption algorithm
Using the X-Circle-Key-Id value, query the
/v2/cpn/notifications/publicKey/{keyId}
endpoint.
curl --request GET \
--url "https://api.circle.com/v2/cpn/notifications/publicKey/${PUBLIC_KEY_ID}" \
--header "Accept: application/json" \
--header "authorization: Bearer ${YOUR_API_KEY}"
Response
{
"data": {
"id": "879dc113-5ca4-4ff7-a6b7-54652083fcf8",
"algorithm": "ECDSA_SHA_256",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg==",
"createDate": "2023-06-28T21:47:35.107250Z"
}
}
Note: To avoid making multiple requests to the public key endpoint, you
should cache the public key associated with a given public key ID.
Step 3: Verify the signature
Use the public key and the specified algorithm from the response in step 2,
along with the X-Circle-Signature value, to verify the integrity of the
webhook’s payload.
The following Python code demonstrates how to verify the X-Circle-Signature
value:
import base64
import json
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
# Load the public key from the base64 encoded string
public_key_base64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg=="
public_key_bytes = base64.b64decode(public_key_base64)
public_key = serialization.load_der_public_key(public_key_bytes)
# Load the signature you want to verify
signature_base64 = "MEQCIBlJPX7t0FDOcozsRK6qIQwik5Fq6mhAtCSSgIB/yQO7AiB9U5lVpdufKvPhk3cz4TH2f5MP7ArnmPRBmhPztpsIFQ=="
signature_bytes = base64.b64decode(signature_base64)
# Load and format the message you want to verify
message = "{\"subscriptionId\":\"00000000-0000-0000-0000-000000000000\",\"notificationId\":\"00000000-0000-0000-0000-000000000000\",\"notificationType\":\"webhooks.test\",\"notification\":{\"hello\":\"world\"},\"timestamp\":\"2024-01-26T18:22:19.779834211Z\",\"version\":2}"
message_bytes = message.encode(encoding="utf-8")
# Verify the signature
try:
public_key.verify(
signature_bytes,
message_bytes,
ec.ECDSA(hashes.SHA256())
)
print("Signature is valid.")
except InvalidSignature:
print("Signature is invalid.")
Tip: Ensure that the webhook payload that you input in the message field
is a properly formatted JSON string. Invalid JSON causes verification failure.