It's never great to find out you're wrong, but that's how learning and personal growth happens.
HTTP Message Signatures are hard1. There are lots of complex parts and getting any aspect wrong means certain death2.
In a previous post, I wrote A simple(ish) guide to verifying HTTP Message Signatures in PHP. It turns out that it was too simple. And far too trusting.
An HTTP Message Signature is a header which is separate to the message it signs. You might receive a JSON message like this:
{
"actor": "https://example.com/user/Alice",
"message": "We strike at dawn!"
}
How do you know that really came from Alice? You look at the header of the message. It will be something like:
Signature:
keyId="https://example.org/user/Alice#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="/AJ4Dv/wSL3XE1dLjFHCYVc7AF4f3+Q10G/r8+6cPsooiUh2K3YX3z++Nclo4qKHYr61yu+T4OMqUry1T6ZHmZqmNkg1RpVg=="
We want to check that Alice signed this message with her private key. So we grab her public key given by the keyId
.
From there, we do some fancy maths using RSA-SHA256 and conclude that, when you put together the (request-target) host date digest content-type
and compare them to the public key, they can only have be signed by the private key. Hurrah!
Did you spot the mistake I made? It wasn't in the maths, or the complex ordering of the data, or the algorithm choice, or some weird Unicode problem.
I made an error in trust.
Take a look at the Signature again.
The keyId
is from example.org. But the actor is from example.com.
This message is signed correctly. It is cryptographically valid. But it wasn't signed by the actor in the message!
In this case, the fix is simple. Get the public key from keyId
. Then independently get the named actor's public key. If they match, all is well. If not, skulduggery is afoot.
I'm almost tempted to say that you should ignore the provided keyId
entirely; the source of truth is the actor's key - and the best way to get that is directly from the actor's profile.
Please explain why I'm wrong in the comments.