Are there any cryptographic flaws in my webhook signing process? Out of band my services have exchanged secret keys. These secret keys are then used to prevent two things of the webhooks (HTTP calls triggered by Service A to Service B when an event occurs in Service A).


Webhook Spoofing
Webhook Replaying


The basic problems with webhooks involve the fact that, short of Client SSL Certificates, there's no way to know that the sender of the HTTP call is who we think they are. Because of these, a mechanism is needed to ensure that the webhook is not being spoofed and that it isn't being replayed. For this, I'm using a Nonce and HMAC-SHA256. The definition of the Signing process (and a Python example) follows:

Creating the Signature


Generate a Random UUID.
Add this UUID as the HTTP Request Header "x-webhook-nonce".
A new, empty string is created, henceforth known as the signing string.
In alphabetical order of the Header names, append the HTTP Request headers (names and values, in that order, in lower case) onto the signing string.
Base64 encode the request body. Append the resulting Base64 encoded string to the signing string.
Using HMAC SHA256, create the signature. The previously exchanged key is to be used as the key and the signing string as the value to be signed.
Take the resulting HMAC value and Base64 encode it. Create the HTTP Header “x-webhook-signature” with the resulting Base64 encoded HMAC string as the value.


Verifying the signature:

This process is almost the exact same as generating the signature with a few minor changes.
 1. Verifiers should not generate their own nonce, but use the one provided in the “x-webhook-nonce” header.
 2. Verifiers should first check and ensure that the nonce has not been used before. If it has, it should ignore the webhook.
 3. When arranging the headers for the signing string, do not add the “x-webhook-signature” header.
 4. Once you have generated a signing string, compare it to the included “x-webhook-signature” to ensure that the generated signature and the header value are the same. If they are, the request is valid. If not, ignore the webhook.

Generating Signature Example in Python:

signing_key = "SIGNING_KEY_FROM_SUBSCRIPTION"
nonce = uuid.uuid4()

body = "{\"url\": \"https://example.com/\"}"
headers =   { 'Content-Type': 'application/json',
    'Content-Length': 25,
    'Host': 'example.com',
    'x-webhook-nonce': nonce
}

to_sign = ""

for key in sorted(headers):
    if key != "x-webhook-signature":
        to_sign += key + headers[key]

to_sign += base64.b64encode(body)
signature = base64.b64encode(hmac.new(signing_key, to_sign, digestmod=hashlib.sha256).digest())
headers["x-webhook-signature"] = signature


So, after that large wall of text, does this solution correctly prevent against spoofing and replaying of webhooks?
I do not recommend inventing your own protocol for this.

Instead, I recommend using TLS client certificates.  They solve exactly your problem, and solve it well.  The TLS folks thought a lot about the security challenges involved in designing such a protocol, so you wouldn't have to.

You mention that you perceive the complexity of client certificates to be significant.  My sense is that the complexity of rolling your own is actually higher than just using an existing TLS library's support for client certs; and the chances of making a subtle security mistake are significantly higher when rolling your own.   Therefore, I do not recommend the direction you are taking of trying to design your own protocol: I recommend using a well-vetted existing mechanism (like TLS client certs) whenever you possibly can.