Signing follow activity requests

Hello,

I’m trying to acknowledge an incoming follow request from Mastodon on my blog. Through trial and error, I have progressed through the error codes thrown from mastodon (src).

However, I cannot get past Verification failed for alice@example.com .... I’m fairly certain this is happening because mastodon is not finding the right public key or I am hashing too much data.

Currently, my PHP code looks like the following:

$document = [
  "@context" => "https://www.w3.org/ns/activitystreams",
  "summary" => "Alice accepted a follow",
  "type" => "Accept",
  "actor" => 'https://voidsurf.world/activityPub/users/1',
  "object" => 'https://mastodon.social/96cb3649-7a75-49c5-b246-bf551dfd440a'
];

$inboxUrl = 'https://mastodon.social/users/codoxuba/inbox';
$url = parse_url($inboxUrl);
$host = data_get($url, 'host');
$path = data_get($url, 'path');
$method = "post";

$publicKeyId = 'https://voidsurf.world/activityPub/users/1#main-key';

$document_str = json_encode($document, JSON_UNESCAPED_SLASHES);
$digest = base64_encode(hash('sha256', $document_str, true));

$date = now()->toRfc7231String();
$dataToBeSigned = "(request-target): {$method} {$path}\nhost: {$host}\ndate: {$date}\ndigest: {$digest}";

$privateKey = openssl_pkey_get_private(ActivityPub::rsaKey('private'));

$base64=null;
if (openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
  $base64 = base64_encode($signature);
} else {
  throw new Exception('Could not sign activityPub data');
}

$signatureHeader = sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $id, $base64 )

$headersToSend = [
  'Host' => $host,
  'Date' => $date,
  'Digest' => 'SHA-256='.$digest,
  'Signature' => $signatureHeader,
  'Content-Type' => 'application/activity+json',
];

$activityResponse = Http::withHeaders($headersToSend)
            ->withBody($document_str, 'application/activity+json')
            ->post($inboxUrl, $document);

Here is the error message that comes back from the activityResponse:

{"error":"Verification failed for alice@voidsurf.world https://voidsurf.world/activityPub/users/1 using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)","signed_string":"(request-target): post /users/codoxuba/inbox\nhost: mastodon.social\ndate: Mon, 28 Aug 2023 04:26:31 GMT\ndigest: SHA-256=/hyLUf+Fq57fWSlJmQs9npln4nvbOOe8doCOjMJ5FjA=","signature":"TOtQ4KSHvwiDul7m8tYVR7mOCXJiIY9/pvbmRR++5HaNmkaAFu/6gZ+oSkpnTIIZP1DixEcNPu+ZXLc1o+GkmFMVdT57yazz6AaR+MMjjsOLnOXhTnFMKKTMAS0jkTtRRY4ZL9utMQiGOiFLkgJIWqfqCQezKUIBUrOrYpDw0xwgc+rGIu5IJI23IGoiLMtZ8gBE2L+/uEyCtWHvJUepZfooLkPBaA5TwqT+6nL28WGzl5Rmh2BAdL9x5/M62+nFQAF9VipHuQKz2eWpzs2sELA7IpTLLY3qH8JLuaReV2XuTXWga5iVYb3A40BPDTc5F0+27Ln+k7zC7zkCxO8+cA=="}

Can someone help me make heads or tails of what I’m doing wrong? Any help is much appreciated!

The “digest” value of the signing string must be equal to the value of the header, but you seem to prepend the algorithm identifier to the header value after signing, making the two differ.

So, instead of signing Digest: {$digest}, you should sign with Digest: SHA256={$digest} since the remote server will check against the digest tag + digest, not just the digest.

I would generally recommend to just implement some unit tests that run against the test vectors from the Draft Cavage RFC.

2 Likes

That did the trick! Thank you!

How did you know to follow that draft RFC? Does ActivityPub reference it somewhere?

AFAIK it’s not an official part of the ActivityPub protocol at all (nor is there an FEP). Back when I was first implementing the protocol I was chatting with another implementor and they linked me to it.

It’s also documented in Mastodon’s source

1 Like

I’ve tried to document parts of the algorithm in the linked Gherkin feature

It’s actually quite complicated to write down the entire HTTP signature algorithm as used in the Fediverse, so I doubt anybody will write a specification for it. The problem arises with how Actors are fetched. As these have to be potentially signed requests.

2 Likes