Supergroup A Protocol (1.0.0)

Download OpenAPI specification:Download

Description

Specification for federated social media protocol.

Terminology

  • Instance: a currently running server process, receiving HTTP requests/communicating with DB/etc.

  • Server: logical unit of the fed system, consisting of 1 or more instances and a single logical database.

  • Local server: the server a user is registered with, and accessing with that server's frontend.

  • Remote server: a server the user is not registered with, and interactions will be proxied through the local server using the protocol we are discussing.

  • Community: collection of posts related by a common topic on a single server (a subreddit).

  • Post: basic unit of content in our protocol, can be nested and frontends may choose to render top level posts as primary pieces of content, with nested posts rendered as comments.

  • User: entity representing a student/member of staff registered with a server. Users cannot be transferred between servers, but can of course view and interact with posts and communities on remote servers.

Caching

Caching

Caching can be streamlined by the addition of cache control headers in HTTP responses.

Servers SHOULD add cache control headers to their responses and MAY respect cache control headers that they receive.

A summary of cache control headers can be found here.

Implementations SHOULD (as much as possible) be RFC compliant. However this specification outlines a very restricted subset of this RFC that groups can implement without needing a library.

Basic Implementation Using Cache Control Headers

HTTP Server

Add Cache-Control: max-age=<seconds> to HTTP responses, where <seconds> is how long the client should cache the resourse for.

To prevent a resource bein cached, set <seconds> to 0.

A naive implementation might use a constant value, whereas a smarter implementation might change the value, depending on how likely the resource is to change.

HTTP Client

Cache resources for the number of seconds specified in max-age. After this time, fetch a new copy.

Security

Security

Without a way to verify that a request has originated from who it is claimed to have, anybody may send requests impersonating users (including administrators).

This propsal is based on a simplified version of the HTTP Signatures protocol.

Sending a Request

Creating the Digest Header

Add the Digest header to every HTTP request. For simplicity, this is restricted to sha-512.

How do I implement it?

  1. sha-512 hash the body of the HTTP request.
  2. Base64 encode the output of the hash.
  3. Add the header Digest: sha-512=<base64_sha512_hash> to the HTTP request where <base64_sha512_hash> is the value from step 2.
Warning

Some libraries may give the hash output as hex (rather than binary). In such a case, when Base64 encoding, you must convert from hex to Base64, not binary to Base64.

Creating the Signature Header

  1. Make the public key available at /fed/key.

    • If your server does not support this security proposal, then the /fed/key endpoint should return a 501 Not Implemented error.
    • The public key should be serialised using the ASN.1 structure SubjectPublicKeyInfo defined in X.509 and encoded using PEM.
  2. Construct the following string based on the values from the HTTP request (note that there is a line ending \n on all lines apart from the last):

    (request-target): post /fed/posts
    host: cooldomain.edu:8080
    client-host: anotherdomain.edu:7070
    user-id: johnsmith
    date: Tue, 07 Jun 2021 20:51:35 GMT
    digest: SHA-512=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=

    There is a single space after each colon :. The part before the colon must be all lower-case. The part following the colon (after the space) should be taken verbatim from the source (do not change the case). The order of the key/value pairs MUST be the same as above.

    • (request-target) - <HTTP Method> <Request Path> (separated by a single space).
      • <HTTP Method> - GET, POST, PUT or DELETE, made lower-case.
      • <Request Path> e.g. /fed/posts.
    • host - The value from the Host HTTP header.
    • client-host - The value from the Client-Host HTTP header.
    • user-id - The value from the User-ID HTTP header. If the User-ID HTTP header is not included in the request, then this field should be omitted.
    • date - The value from the Date HTTP header.
    • digest - The value from the Digest HTTP header.
  3. The string from step 2 should be rsa-sha512 signed using PKCS #1 (you will almost certainly need to use a library to do this).

  4. The signature from step 3 should be Base64 encoded. See the previous warning.

  5. Send the following header in the HTTP request (only including user-id if required by the request):

    Signature: keyId="rsa-global",algorithm="hs2019",headers="(request-target) host client-host user-id date digest",signature="<base64_signature>"

    All fields are static apart from the final signature field, which is the output from step 4.

Receiving a Request

You MUST NOT verify requests made to the GET /fed/key endpoint. All other requests MUST be verified.

Verifying the Signature Header

  1. Obtain the public key from http://<host>/fed/key, where <host> is the value from the Host HTTP header.
    • If the server returns a 501 error, then it has not implemented this security proposal.
    • You may decide whether to accept the request without verification, or reject it.
    • You can not continue any further with verification (including verification of the Digest header).
  2. Generate the string from step 1 of Creating the Signature Header.
  3. Obtain the <base64_signature> from the Signature header:
    Signature: keyId="global",algorithm="rsa-sha512",headers="(request-target) host client-host user-id date digest",signature="<base64_signature>"
  4. Using the rsa-sha512 algorithm, verify, using the public key from step 1, that <base64_signature> is valid using PKCS #1 (you will almost certainly need to use a library to do this).

Verifying the Digest Header

  1. sha-512 hash the body of the HTTP request.
  2. Base64 encode the output of the hash.
  3. Verify that the output from step 2 matches the <base64_sha512_hash> value from the Digest header: Digest: sha-512=<base64_sha512_hash>.

Some Examples

Python

Uses the crytography library.

Exporting a Public Key

from cryptography.hazmat.primitives import serialization

with open("private.pem", "rb") as key_file:
   private_key = serialization.load_pem_private_key(
      key_file.read(),
      password=None,
   )

public_key = private_key.public_key()
pem = public_key.public_bytes(
   encoding=serialization.Encoding.PEM,
   format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print(pem.decode("utf-8"))

Generating a Digest

import base64
from cryptography.hazmat.primitives import hashes

digest = hashes.Hash(hashes.SHA512())
digest.update(b"A message")

base64encoded = base64.b64encode(digest.finalize())

print(base64encoded.decode("ascii"))

Signing a String

import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

with open("private.pem", "rb") as key_file:
   private_key = serialization.load_pem_private_key(
      key_file.read(),
      password=None,
   )

message = b"A message I want to sign"
signature = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA512()
)

print(base64.b64encode(signature).decode("ascii"))

Verifying a Signature

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

# You should get the public key from `/fed/key`.
with open("public.pem", "rb") as key_file:
    public_key = serialization.load_pem_public_key(key_file.read())

# Get the signature from the HTTP header.
encoded_signature = "ij3WqCSE+289DW4KV3Rh//4mZ1dev5I7m6rUrYyvcojmPBhuVzqxJ0XLoWPKtGz6aVYi4k+Ide1zTGUIAOWQEwCiT4WP/GrsYukwgAfgS9q80YgiKIyqVBvc953XLVzgnOT+8X2HQ/LTg+BwP23kLeEXPabxhMN323L+gVWVyoiIUYEf0B34PbPq/KTPqW/rHtup6ovSRfvy8Bqeqmtpmc0gJwR7WnKRYEiVn40yRQDxtO6zSjvmObv5U2BKCjprnOAp5yfKzROkpfqui1yjKMp5RfA+NILGiJSSQwgGe1eG0QOWYoW8JecLOrxBHOJMuFc0wDQ0k9cip/nAc/T5Cw=="

decoded_signature = base64.b64decode(encoded_signature)

# If this throws an InvalidSignature exception, then the signature
# was invalid.
public_key.verify(
    decoded_signature,
    message,
    padding.PKCS1v15(),
    hashes.SHA512()
)

Node

Exporting a Public Key

const crypto = require("crypto");
const fs = require("fs");

const publicKey = crypto.createPublicKey({
  key: fs.readFileSync("private.pem"),
  format: "pem"
});

const encodedPublicKey = publicKey.export({
  type: "spki",
  format: "pem"
});

console.log(encodedPublicKey);

Generating a Digest

const crypto = require("crypto");

const hash = crypto.createHash("sha512");
hash.update("A message");

const digest = hash.digest("base64");

console.log(digest);

Signing a String

const crypto = require("crypto");
const fs = require("fs");

const privateKey = crypto.createPrivateKey({
  key: fs.readFileSync("private.pem"),
  format: "pem"
});

const sign = crypto.createSign("SHA512");
sign.write("A message I want to sign");
sign.end();

const signature = sign.sign(privateKey, "base64");

console.log(signature);

Verifying a Signature

const crypto = require("crypto");
const fs = require("fs");

const publicKey = crypto.createPublicKey({
  key: fs.readFileSync("private.pem"),
  format: "pem"
});

const signature = "ij3WqCSE+289DW4KV3Rh//4mZ1dev5I7m6rUrYyvcojmPBhuVzqxJ0XLoWPKtGz6aVYi4k+Ide1zTGUIAOWQEwCiT4WP/GrsYukwgAfgS9q80YgiKIyqVBvc953XLVzgnOT+8X2HQ/LTg+BwP23kLeEXPabxhMN323L+gVWVyoiIUYEf0B34PbPq/KTPqW/rHtup6ovSRfvy8Bqeqmtpmc0gJwR7WnKRYEiVn40yRQDxtO6zSjvmObv5U2BKCjprnOAp5yfKzROkpfqui1yjKMp5RfA+NILGiJSSQwgGe1eG0QOWYoW8JecLOrxBHOJMuFc0wDQ0k9cip/nAc/T5Cw==";

const verify = crypto.createVerify("SHA512");
verify.write("A message I want to sign");
verify.end();

const result = verify.verify(publicKey, signature, "base64");
console.log(result);

N.B.

There has been a newly published (November 2020) diverging specification for signing HTTP messages: Signing HTTP Messages. This proposal is based on the predessor specification.

PKCS #1 is used over PSS to simplify implementation.

At the moment, a MITM attack could replace the public key at /fed/key with another. This can be solved either by:

  • enforcing HTTPS (perhaps using our own agreed upon certificate authority);
  • using a X.509 certificiate instead of a public key, which we use our own authority to sign.

Communities

Gets a list of the IDs of communities on the server

header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

Responses

Response samples

Content type
application/json
[
  • "cats",
  • "dogs",
  • "cs3099",
  • "physics"
]

Gets a community by ID

Note the reacts paramater in the community response is entierly optional and may not be present. Also when implementing reacts every community must return a like and dislike react. This is so instances which implement an up/down vote system can use these reacts for this. Another item to note about reacts is that they contain a path and a unicode property. Only one of these has to be set. The unicode property should represent a single unicode character to be displayed as that react. With the unicode for a react there is no length limit placed on it becuase calculating the length of a unicode string is hard. For example the character '🏴󠁧󠁢󠁳󠁣󠁴󠁿' has length 7/9 but only appears as a single character.

path Parameters
id
required
string <^[a-zA-Z0-9-_]{1,24}$>

ID of the community being requested

header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

Responses

Response samples

Content type
application/json
{
  • "id": "cs3099",
  • "title": "CS3099: Group Project",
  • "description": "CS3099 community for discussion, tutorials and quizzes!",
  • "admins": [
    ],
  • "reacts": [
    ]
}

Gets the timestamps of last modification of all posts in a community

path Parameters
id
required
string <^[a-zA-Z0-9-_]{1,24}$>

ID of the community being requested

header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

Responses

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    }
]

Posts

Gets all posts matching filters

query Parameters
limit
integer

Filters by the n latest posts

community
string

Filters posts by community

minDate
integer <unix_timestamp>

Filters by minimum creation date

author
string <^[a-zA-Z0-9-_]{1,24}$>
Example: author=coolusername123

Filters by author

host
string
Example: host=cool.servername.net

Filters by server hostname

parentPost
string <uuidv4>

Filters by ID of parent post (will exclude posts with no parent)

includeSubChildrenPosts
boolean
Default: true

Include children posts, children of children posts and so on.

contentType
string
Example: contentType=markdown

Filters by type of post content.

header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

User-ID
required
string <^[a-zA-Z0-9-_]{1,24}$>

The user making the request.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Creates a new post

If the post is in response to another post, the title field must be set to null.

Servers may decide which types of contnent they will accept as posts (including comments), however they must at least accept text content objects. If the server does not accept a particular type of content then it should return a 501 error.

If a server can not render markdown content objects, it should display it as plain text (as opposed to rejecting the request).

content can contain multiple objects containing different kinds of content. For example, markdown could be combined with a (not yet implemented) poll and/or image type, allowing text, images and a poll in a single post. However, only a single object of each kind is allowed and there are restrictions on certain combinations, currently text and markdown are mutually exclusive.

header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

User-ID
required
string <^[a-zA-Z0-9-_]{1,24}$>

The user making the request.

Request Body schema: application/json

New post to be added to a community

community
required
string <^[a-zA-Z0-9-_]{1,24}$>
parentPost
string <uuidv4>
title
required
string
required
Array of PostContentText (object) or PostContentMarkdown (object) or PostContentReact (object) (PostContent)

Responses

Request samples

Content type
application/json
{
  • "community": "sailing",
  • "parentPost": "dafca76d-5883-4eff-959a-d32bc9f72e1a",
  • "title": "Bezos's Wealth Overflows 64-bit Unsigned Integer, Is Now Homeless",
  • "content": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "5ab3acce-e9d1-4b3a-be97-60d2cbe32a4c",
  • "community": "sailing",
  • "parentPost": "dafca76d-5883-4eff-959a-d32bc9f72e1a",
  • "children": [
    ],
  • "title": "Bezos's Wealth Overflows 64-bit Signed Integer, Now Massively In Debt",
  • "content": [
    ],
  • "author": {
    },
  • "modified": 1552832552,
  • "created": 1552832584,
  • "reacts": [
    ]
}

Gets a post

path Parameters
id
required
string <uuidv4>
header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

User-ID
required
string <^[a-zA-Z0-9-_]{1,24}$>

The user making the request.

Responses

Response samples

Content type
application/json
{
  • "id": "5ab3acce-e9d1-4b3a-be97-60d2cbe32a4c",
  • "community": "sailing",
  • "parentPost": "dafca76d-5883-4eff-959a-d32bc9f72e1a",
  • "children": [
    ],
  • "title": "Bezos's Wealth Overflows 64-bit Signed Integer, Now Massively In Debt",
  • "content": [
    ],
  • "author": {
    },
  • "modified": 1552832552,
  • "created": 1552832584,
  • "reacts": [
    ]
}

Edits a post

Only allowed if request is made by the server associated with the author or the admins of the community the post belongs to, 403 returned otherwise

path Parameters
id
required
string <uuidv4>
header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

User-ID
required
string <^[a-zA-Z0-9-_]{1,24}$>

The user making the request.

Request Body schema: application/json

New post to be added to a community

title
required
string
required
Array of PostContentText (object) or PostContentMarkdown (object) or PostContentReact (object) (PostContent)

Responses

Request samples

Content type
application/json
{
  • "title": "Bezos's Wealth Overflows 64-bit Signed Integer, Now Massively In Debt",
  • "content": [
    ]
}

Response samples

Content type
application/json
{
  • "title": "A short description of the erorr",
  • "message": "A long description of the error, giving instructions on how it can be solved and why it occured."
}

Deletes a post

Only allowed if request is made by the server associated with the author or the admins of the community the post belongs to, 403 returned otherwise

path Parameters
id
required
string <uuidv4>
header Parameters
Client-Host
required
string

The hostname (including port) of the client server.

User-ID
required
string <^[a-zA-Z0-9-_]{1,24}$>

The user making the request.

Responses

Response samples

Content type
application/json
{
  • "title": "A short description of the erorr",
  • "message": "A long description of the error, giving instructions on how it can be solved and why it occured."
}

Users

Search for users

All users should be returned if no filters are passed.

query Parameters
prefix
string
Example: prefix=joh

Filters users by ID prefix

Responses

Response samples

Content type
application/json
[
  • "john"
]

Gets a user by ID

path Parameters
id
required
string <^[a-zA-Z0-9-_]{1,24}$>

ID of the user being requested

Responses

Response samples

Content type
application/json
{
  • "id": "john",
  • "about": "A place for a user to write an about / bio",
  • "avatarUrl": "cooldomain.edu/media/profile_imgs/avatar.png",
  • "posts": [
    ]
}

Send the user a message

path Parameters
id
required
string <^[a-zA-Z0-9-_]{1,24}$>

ID of the user being messaged

Request Body schema: application/json

The message being sent

title
required
string
required
PostContentText (object) or PostContentMarkdown (object) or PostContentReact (object) (PostContent)

Responses

Request samples

Content type
application/json
{
  • "title": "Why ban?",
  • "content": {
    }
}

Other

Gets the server's public key

Responses

Response samples

Content type
application/json
{
  • "title": "A short description of the erorr",
  • "message": "A long description of the error, giving instructions on how it can be solved and why it occured."
}

Return all known servers

Returns all of the servers that are known by the current server.

The array should be ordered with more 'important' hosts at the beginning and the least 'important' at the end.

The 'importance' of a server is implementation specific, but some examples include using the total number of posts made to that server by the current server.

Responses

Response samples

Content type
application/json
[
  • "cooldomain.edu"
]