Validating Signatures in PHP - Something's wrong and I can't figure out what

Okay, so, I’m working on upgrading my long time comics engine project to run on Activity Pub protocols. And while I’ve got the comics-engine part, I’ve hit a huge wall (going on 8 weeks) of problems with simple validating incoming requests. (Never mind signing outgoing requests, I haven’t even GONE there yet.)

I’ve tried working w/ landrok/activity-pub 's validator, only to have it fail over and over. I’ve taken the validators from the Wordpress/activity-pub plugin, the one from The tutorial on Building an ActivityPub Server, the validator from simonuet/flox and the one from pixelfed. (All have been re-written to skip over the various repo-specific pieces, but generally they’re all following the same flow as their originals).

All of which are returning the same unhelpful response. “Validation failed”

I’ve used helgekr/pasture validation server on my docker, sending information in, but for my local postman mockups, I’m getting “Digests do not match” and for actual Follow calls from Mastodon… It breaks, throwing a 500 and saying it’s incapable of decoding the signature.

All of this tells me I’m doing something wrong. Signatures cannot be so broken that 5 different in-production methods AND an external validator service are all failing.

So, what am I doing wrong? Help me SocialHub, I’m at my wit’s end.

So some stats. I’m working on ClickthuluFed in the dev/activity-pub branch.

I’m running it in Docker, using nginx, mariadb and php-fpm (Will be adding redis later)

I’m using ngrok to put a proxy in front of my local docker so mastodon servers outside of my local can see the server.

If anyone who has some time and is willing to lend a helping hand and/or useful (or even sarcastic) advice, it’d be much oblidged

Hi, I agree it does sound like you have a bug in your signature impl. It would be helpful if you could provide some sample code or examples of requests that you’re making that aren’t working (with the signature header included, of course)

Sure no problem.

First off, this is not finished, but the specific issue is showing up here:

InboxController starting around line 40.

Specifically I’m trying 2 approaches in this block

        $signature = new HttpSignature($server);
        $isValidLandrok = $signature->verify($request);
        $isValidHome = SignatureHelper::validate($actor->get(), $request);

The isValidLandrok is using landrok/activity-pub’s built in validator. The isValidHome is the one I re-built from Building an Activity-Pub Server

Neither works.

Recently I’ve added Henge’s pasture/httpsig server to my docker-compose.yml. I send the request I receive to a method that constructs a call w/ all the appropriate headers and the body of the request to his validator. Things still fail however:

A call constructed running via PostMan…

  "headers": {
    "Accept-Encoding": "gzip",
    "X-Forwarded-Proto": "https",
    "X-Forwarded-Host": "",
    "X-Forwarded-For": "",
    "Signature": "keyId=\"https:\/\/\/users\/daemionfox#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"OBo+sFl\/dSYRdwNDJwd1Y3XJJrx+Y4BUfjUuiE5ThhGHeoWMJlpVG5SdzJhgnlBFuv0dYvQE1hjTfyKGa1PjKXpWfQayfN8mvt87it8+vp40IvoBzhvFYYWE5v65ckjEV8hqBdf+N43hHY8pNmBoEZ+twOlhBdcik9GDtnm\/fApROq2AH1TMSDU4CEWXt3IUoBLD6VP9ttjcrvH8RPvDfdEYYIvquxw7B+0mfJ7vpouTFWP3ELWCmtKdHu3MtieGJPmSf4ajXCZhasN0q\/JjQQ\/OVNtD5Pkl+YOH5rK54ZX\/uQSJWBgv9F39dr0gPQtc+Tg1xrCiHjULxJDKUuk0iQ==\"",
    "Digest": "SHA-256=xdk1OkkEwowdIF3qtehsLYyQOGYK\/XUebaYmjFROieM=",
    "Date": "Tue, 05 Dec 2023 13:34:36 GMT",
    "Content-Type": "application\/activity+json",
    "Content-Length": "258",
    "User-Agent": "http.rb\/5.1.1 (Mastodon\/4.2.0; +https:\/\/\/)",
    "Host": ""
  "body": {
    "@context": "https:\/\/\/ns\/activitystreams",
    "id": "https:\/\/\/361d56d1-a883-4d7e-adba-cd1f4e8753d2",
    "type": "Follow",
    "actor": "https:\/\/\/users\/daemionfox",
    "object": "https:\/\/\/@cutloose"


  "steps": [
    "Got post request",
    "With headers: Remote-Addr:\r\nHost:\r\nAccept-Encoding: gzip\r\nX-Forwarded-Proto: https\r\nX-Forwarded-Host:\r\nX-Forwarded-For:\r\nSignature: keyId=\"https:\/\/\/users\/daemionfox#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"OBo+sFl\/dSYRdwNDJwd1Y3XJJrx+Y4BUfjUuiE5ThhGHeoWMJlpVG5SdzJhgnlBFuv0dYvQE1hjTfyKGa1PjKXpWfQayfN8mvt87it8+vp40IvoBzhvFYYWE5v65ckjEV8hqBdf+N43hHY8pNmBoEZ+twOlhBdcik9GDtnm\/fApROq2AH1TMSDU4CEWXt3IUoBLD6VP9ttjcrvH8RPvDfdEYYIvquxw7B+0mfJ7vpouTFWP3ELWCmtKdHu3MtieGJPmSf4ajXCZhasN0q\/JjQQ\/OVNtD5Pkl+YOH5rK54ZX\/uQSJWBgv9F39dr0gPQtc+Tg1xrCiHjULxJDKUuk0iQ==\"\r\nDigest: SHA-256=xdk1OkkEwowdIF3qtehsLYyQOGYK\/XUebaYmjFROieM=\r\nDate: Tue, 05 Dec 2023 13:34:36 GMT\r\nContent-Type: application\/activity+json\r\nUser-Agent: http.rb\/5.1.1 (Mastodon\/4.2.0; +https:\/\/\/)\r\nX-Php-Ob-Level: 0\r\nContent-Length: 258\r\n\r\n",
    "Signature header 'keyId=\"https:\/\/\/users\/daemionfox#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"OBo+sFl\/dSYRdwNDJwd1Y3XJJrx+Y4BUfjUuiE5ThhGHeoWMJlpVG5SdzJhgnlBFuv0dYvQE1hjTfyKGa1PjKXpWfQayfN8mvt87it8+vp40IvoBzhvFYYWE5v65ckjEV8hqBdf+N43hHY8pNmBoEZ+twOlhBdcik9GDtnm\/fApROq2AH1TMSDU4CEWXt3IUoBLD6VP9ttjcrvH8RPvDfdEYYIvquxw7B+0mfJ7vpouTFWP3ELWCmtKdHu3MtieGJPmSf4ajXCZhasN0q\/JjQQ\/OVNtD5Pkl+YOH5rK54ZX\/uQSJWBgv9F39dr0gPQtc+Tg1xrCiHjULxJDKUuk0iQ==\"'",
    "Got fields (request-target), host, date, digest, content-type",
    "Got body: '{\"@context\": \"https:\/\/\/ns\/activitystreams\",\"id\": \"https:\/\/\/361d56d1-a883-4d7e-adba-cd1f4e8753d2\",\"type\": \"Follow\",\n\"actor\": \"https:\/\/\/users\/daemionfox\",\"object\": \"https:\/\/\/@cutloose\"}'",
    "Computed digest sha-256=UnNLmYxOzk9zk9pr76sGyFHerymLUgY8RY0wH9HWRA8="
  "x error": "Digests do not match"

Whereas the same request coming from Mastodon (different timestamps/sigs/etc) returns as a 500 error with a

fediverse_pasture.types:['Something went wrong when verifying signature', "ValueError('Could not deserialize key data. The data may be in an incorrect format, it may be encrypted with an unsupported algorithm, or it may be an unsupported key type (e.g. EC curves with explicit parameters).', [<OpenSSLError(code=75497580, lib=9, reason=108, reason_text=no start line)>])"

So as I said, I’m stumped. (I started working on this piece of the code in December, and have gotten no closer to complete)

I haven’t looked at the internals of landrok/activitypub verification code, but one thing that is easy to overlook is checking for x-forwarded-proto before host, I can see in your example they don’t have the same values.

They don’t?

I mean, aside from one being a JSON encoded block of the request (meaning that the \r\n is missing and quotes have been added around strings) it looks the same?

X-Forwarded-Proto is https on both, and the Host also matches? I’m not adding the windows style line feed, that’s the return from the validator… But other than that, they look the same to me?

What am I missing?

How do you compute the digest?

In [29]: body
Out[29]: '{"@context": "","id": "","type": "Follow",\n"actor": "","object": ""}'

In [30]: base64.standard_b64encode(hashlib.sha256((body).encode()).digest())
Out[30]: b'UnNLmYxOzk9zk9pr76sGyFHerymLUgY8RY0wH9HWRA8='

In particular, the binary input to sha256 is relevant, here:

{"@context": "","id": "","type": "Follow",\n"actor": "","object": ""}

If you use a different input in regards of how the json is serialized, you will get a different answer.

1 Like