I made a mistake in verifying HTTP Message Signatures


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.


  1. You might think the Entscheidungsproblem is hard, but that's just peanuts compared to etc. etc. 
  2. Or cake. 

Share this post on…

5 thoughts on “I made a mistake in verifying HTTP Message Signatures”

  1. says:

    @blog you have two problems:

    1/ Verify that the HTTP Signature is valid, and as Evan told you some time ago: you should always use the key in the header signature for that.

    2/ You need to verify that the Activity actor is the same as the actor that owns that key.

    In my opinion trying to simplify this is incorrect and you should always treat them as separate steps happening at different stages in the validation process.

    | Reply to original comment on metalhead.club
    1. says:

      @blog on why they should be different steps: it could be that you want certain actors to be able to effect activities on behalf of other actors.

      Think something like, posting on behalf of a Group actor.

      Or anonymizing a Flag activity. You put a generic Actor in the Activity and you sign it as the instance Service actor on behalf of a regular user.

      It's up to every server/instance if they want to support these things, but they seem pretty useful to me.

      | Reply to original comment on metalhead.club
  2. It also looks like you included "content-type" in your verification step when it wasn't included in the signature.

    But I did totally miss the org/com switch. I'll have to check my implementation, but I'm pretty sure I don't use the keyId location at all. I start from the Actor and go to webfinger.

    Reply

What are your reckons?

All comments are moderated and may not be published immediately. Your email address will not be published.Allowed HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <p> <pre> <br> <img src="" alt="" title="" srcset="">