2018-07-03_How-to-make-friends-and-verify-people.md 5.15 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
---
title: "How to make friends and verify requests"
subtitle: "Implementing an ActivityPub inbox"
date: 2018-07-03
draft: true
author: gargron
categories:
- Guides
tags:
- mastodon
- activitypub
---

In the previous tutorial we have learned [how to send a reply to another ActivityPub server]({{< ref "posts/2018-06-23_How-to-implement-a-basic-activitypub-server.md" >}}), and we have used mostly static parts to do it. Now it's time to talk about how to subscribe to other people and receive messages.

### The inbox

Primarily this means having a publicly accessible inbox and validating HTTP signatures. Once that works, everything else is just semantics. Let's use a Sinatra web server to implement the inbox.

In fact, I intend to omit persistence from this tutorial. How you would want to store data in a real application is very much up for debate and depends on your goals and requirements. So, we're going to store data in a variable and implement a simple way to inspect it.

```ruby
require 'sinatra'

INBOX = []

get '/inspect' do
  [200, INBOX.join("\n\n")]
end

post '/inbox' do
  request.body.rewind
  INBOX << request.body.read
  [200, 'OK']
end
```

38
That's an absolutely basic implementation. Save it in `server.rb`. You can run the server with `ruby server.rb` (you need the Sinatra gem installed before that: `gem install sinatra`). Now on this server you can navigate to `/inspect` to see the contents of your inbox, and you (and anyone, really) can POST to the `/inbox` to add something there.
39 40 41 42 43 44 45 46

Of course, anyone being able to put anything in there is not ideal. We need to check the incoming POST requests for a HTTP signature and validate it. Here is what a HTTP signature header looks like:

    Signature: keyId="https://my-example.com/actor#main-key",headers="(request-target) host date",signature="Y2FiYW...IxNGRiZDk4ZA=="

We need to read the `Signature` header, split it into its parts (`keyId`, `headers` and `signature`), fetch the public key linked from `keyId`, create a comparison string from the plaintext headers we got in the same order as was given in the signature header, and then verify that string using the public key and the original signature.

```ruby
47 48 49
require 'json'
require 'http'

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
post '/inbox' do
  signature_header = request.headers['Signature'].split(',').map do |pair|
    pair.split('=').map do |value|
      value.gsub(/\A"/, '').gsub(/"\z/, '') # "foo" -> foo
    end
  end.to_h

  key_id    = signature_header['keyId']
  headers   = signature_header['headers']
  signature = Base64.decode64(signature_header['signature'])

  actor = JSON.parse(HTTP.get(key_id).to_s)
  key   = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem'])

  comparison_string = headers.split(' ').map do |signed_header_name|
    if signed_header_name == '(request-target)'
      '(request-target): post /inbox'
    else
      "#{signed_header_name}: #{request.headers[signed_header_name.capitalize]}"
    end
  end

  if key.verify(OpenSSL::Digest::SHA256.new, signature, comparison_string)
    request.body.rewind
    INBOX << request.body.read
    [200, 'OK']
  else
    [401, 'Request signature could not be verified']
  end
end
```

The code above is **somewhat simplified** and missing some checks that I would advise implementing in a serious production application. For example:

* The request contains a `Date` header. Compare it with current date and time within a reasonable time window to prevent replay attacks.
* It is advisable that requests with payloads in the body also send a `Digest` header, and that header be signed along in the signature. If it's present, it should be checked as another special case within the comparison string: Instead of taking the digest value from the received header, recompute it from the received body.
* While this proves the request comes from an actor, what if the payload contains an attribution to someone else? In reality you'd want to check that both are the same, otherwise one actor could forge messages from other people.

Still, now you have a reasonably secure toy inbox. Moving on.

### Following people

To register as a follower of someone, you need to send them a `Follow` activity. The receiver may manually decide whether to allow that or not, or their server may do it automatically, but in the case of success you will receive an `Approve` activity back referring to your `Follow`. Here is how a `Follow` may look like:

```json
{
	"@context": "https://www.w3.org/ns/activitystreams",
	"id": "https://my-example.com/my-first-follow",
	"type": "Follow",
	"actor": "https://my-example.com/actor",
	"object": "https://mastodon.social/users/Mastodon"
}
```

Make sure your actor JSON points to your inbox, and your inbox server is running and publicly accessible under that URL, then deliver that activity to the target user's inbox, in our example it would be `https://mastodon.social/inbox`.

If everything works correctly, inspecting your inbox you should find an `Approve` activity. Afterwards, you will find other activities in there from the person you followed, like `Create`, `Announce` and `Delete`.

Ideally, you'd follow your own Mastodon account, just so you can control when to post, otherwise you may end up waiting for your inbox to fill for a long time.