Note: To avoid making multiple requests to the public key endpoint, you should cache the public key associated with a given public key ID.
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.
Use the following steps to verify the Circle signature on a webhook 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 CircleX-Circle-Key-Id
: the public key ID in UUID formatExtract those values from the header of the webhook message.
Using the X-Circle-Key-Id
value, query the
/v2/cpn/notifications/publicKey/{keyId}
endpoint.
curl --request GET \
--url "https://api.circle.com/v2/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.
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.