Implement (broken) HTTP signatures

This commit is contained in:
Anthony Wang 2023-01-18 05:01:59 +00:00
parent 80e5041182
commit 53f0c72117
No known key found for this signature in database
GPG key ID: 42A5B952E6DD8D38
16 changed files with 319 additions and 11 deletions

View file

@ -24,7 +24,7 @@ That wasn't so bad, was it? (sobbing sounds)
Alright, time for the real deal. Start up `python main.py` on your server. If you want to customize the port or whatever, just change the source code.
Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/) and `curl`ing some homemade JSON. That's rough, buddy.
Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/), writing some homemade JSON, and figuring out how `python client.py` works.
Like and subscribe and enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳

14
activity.jsonld Normal file
View file

@ -0,0 +1,14 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://0.exozy.me/users/test.outbox/hello-world2",
"type": "Create",
"actor": "https://0.exozy.me/users/test.jsonld",
"object": {
"id": "https://0.exozy.me/users/test.statuses/hello-world2",
"type": "Note",
"attributedTo": "https://0.exozy.me/users/test.jsonld",
"inReplyTo": "https://social.exozy.me/@a/109707513227348721",
"content": "Hello from fuwuqi! 2",
"to": "https://www.w3.org/ns/activitystreams#Public"
}
}

35
client.py Normal file
View file

@ -0,0 +1,35 @@
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from base64 import b64encode
from email.utils import formatdate
from requests import post
date = formatdate(usegmt=True)
with open('activity.jsonld', 'rb') as f:
activity = f.read()
digester = hashes.Hash(hashes.SHA256())
digester.update(activity)
digest = b64encode(digester.finalize()).decode()
message = f'(request-target): post /users/a/inbox\nhost: social.exozy.me\ndate: {date}\ndigest: SHA-256={digest}'
with open('private.pem', 'rb') as f:
privkey = serialization.load_pem_private_key(f.read(), None)
signature = b64encode(privkey.sign(
message.encode('utf8'),
padding.PKCS1v15(),
hashes.SHA256()
)).decode()
header = f'keyId="https://0.exozy.me/users/test.jsonld#main-key",headers="(request-target) host date digest",signature="{signature}"'
resp = post('http://localhost:4200/users/test.outbox', headers={
'(request-target)': 'post /users/a/inbox',
'Host': 'social.exozy.me',
'Date': date,
'Digest': f'SHA-256={digest}',
'Signature': header,
}, data=activity)
print(resp)
print(resp.text)

10
main.py
View file

@ -1,10 +0,0 @@
from http.server import SimpleHTTPRequestHandler, HTTPServer
class fuwuqi(SimpleHTTPRequestHandler):
def do_POST(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write('hi!'.encode('utf-8'))
HTTPServer(('localhost', 4200), fuwuqi).serve_forever()

28
private.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc+fpJFDUlPCoU
wUmrsGDExb/hX8dhKMc9u0ov/ePBc5OY4tTPjCvghvUjCIqmyC7PS7JfUuNt7epD
OpoJm7mF2gaj64mxqfPrICgtjARN8gUiOpCvcWggZDFgkMQuSFI7yG1s4oqhGJUq
yMPnHhBwSRmQsBp5/xZWHTywSoTd8Li4uY6VJq/JxswuZqH1R2GWMVxpVJDuqjH0
cW8oXZ6LgGY3ZZ0wZnwk3bSdzQ7GhmkGkwCtGoXSSuMiSEDn/0K8hkRQs9AKFgvo
ByYtLSP/JPETzZ4UwEG/J6DwgIxHNxsKg36FWe/dxv/v4HA12hCNmq3REb+GFulz
xEq35g71AgMBAAECggEACqnGcS67ZMyfSn/4xP4QYhLRc89MSBLQ5KYeJirCd70l
97riMY5i2qKARhbJ8wYNJqK3OqzIQIrqoLzQguTo/NPbI0kYNjvbvbXrqhsP8yAv
bhb5W27s36ppWkHAjyj1gRRz2R4obu9bDqggT/Olso2cpsyiTSA2qwyFt1n7MueQ
ag/lSYAAnjRfnxnGiBwy+wURxZH+Y9uBhb9E8MZ+W+sUc7l8RDUfYPN9d7NmxfdV
x6x+jk+OLpYotj1v6aTfdwBCqpdt2ulMZexTCfjwAIU/XI2uhqaO1dzKI+Ssr/WY
j8R7LXV94pI1ZXdwAJ/8aWGllAhj1hpnhacjnr7JAQKBgQDZPj5zJVk7TBoRxQVw
VCfmGwPwAkfmGGqn9g41VlKtyG+JhWQ7aAs0gMFwq6QGDEBloAiXQw7z033ZSNTP
p61QCePq6fLGEgdsfgjGotEbZ2XDZdB6fdzK+sKJLQtXr8FVrywyNmlX0bk8IRSs
hnJXYco/VTLeDiQePO2bjm0UdQKBgQC4+0hR/se6AHgN9449ruHLSOCcC+Y6WQJ1
BM+ydk7OnnfDmxzXQMbzri5kwDTBw+Y01avbRkpMZbHtWip9QWL96Kf8D6OUoX6d
KJuheKsuJUKmXTPF8YyJiY+KXmx1yz7hu8X/SN955s4BAttIZGQvSIFpy+izzQWc
FghcplvAgQKBgQCIkuIN37AOYFSPUU6PBMkkl11NWRG8bSM4Pq9GBuPpjvXX/f06
f7lzo3J5E98FUlR1zzs3ZRgUX6RhorDvb1m81Mrtl3Bh51m1cjKwNhHB6aoHQo3j
RBc3oJgGR0Q3Ny4TYRIm6yAk7ptGWwG1SLy/hKHyWOymvzsjq2gxgEPBNQKBgEyM
6KvOBPdLVGNrS/jo01Yd7Z2GKxuAVEz61bzjys8ksylGmpPVob+cGGTnSa3aFP1O
Y1VV7E9bUluIEcdN9NpgmovsKOTMRCpjcKxM1II/NyrDrTZANMmCHN3FH5tLpdUi
sNhpXtoCksPGW9rEeNU8axnOIZmuwaCLWaCF07iBAoGAX494Slca2EnERTtcX9jq
SM/srMTz+cm5zNA3npMj1eZ7zNglD5tZapc3f5OErrDdZz5wlmIo/eheQxb8TMQW
F5goYHwpq6bghHlbAxK2I353s8Q8WriLTvAYouHfEqd14AS42OXD44CBwU0V5rRV
MmlfOVBGqPVHLQK/tOebZIw=
-----END PRIVATE KEY-----

9
public.pem Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg
xMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5
hdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q
cEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e
i4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j
/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO
9QIDAQAB
-----END PUBLIC KEY-----

87
server.py Normal file
View file

@ -0,0 +1,87 @@
from base64 import b64decode
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from json import dump, load, loads
from re import search
from requests import get, post
from os.path import isfile
from urllib.parse import quote_plus
def collection_append(file, item):
with open(file) as f:
collection = load(f)
collection['totalItems'] += 1
collection['orderedItems'].append(item)
with open(file, 'w') as f:
dump(collection, f)
class fuwuqi(SimpleHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length']))
activity = loads(body)
print(activity)
print(self.headers)
print(self.path)
# Get actor public key
keyid = search('keyId="(.*?)"', self.headers['Signature']).group(1)
actorfile = f'users/{quote_plus(keyid)}'
if not isfile(actorfile):
with open(actorfile, 'w') as f:
f.write(get(keyid).text)
with open(actorfile) as f:
pubkeypem = load(f)['publicKey']['publicKeyPem'].encode('utf8')
pubkey = serialization.load_pem_public_key(pubkeypem, None)
# Assemble headers
headers = search('headers="(.*?)"', self.headers['Signature']).group(1)
message = ''
for header in headers.split():
headerval = self.headers[header]
message += f'{header}: {headerval}\n'
# Verify HTTP signature
signature = search('signature="(.*?)"', self.headers['Signature']).group(1)
pubkey.verify(
b64decode(signature),
message[:-1].encode('utf8'),
padding.PKCS1v15(),
hashes.SHA256()
)
# Make sure activity doer matches HTTP signature
actor = keyid.removesuffix('#main-key')
if 'actor' in activity and activity['actor'] != actor:
self.send_response(401)
return
if 'attributedTo' in activity and activity['attributedTo'] != actor:
self.send_response(401)
return
username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
if self.path.endswith('inbox'):
# S2S
id = activity['id'].split('/')[-1]
with open(f'users/{username}/{id}', 'w') as f:
dump(activity, f)
elif self.path.endswith('outbox'):
# C2S
collection_append(f'users/{username}.outbox', activity)
if activity['type'] == 'Create':
id = activity['id'].split('/')[-1]
with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f)
print(self.headers)
print(body)
resp = post('https://social.exozy.me/inbox', headers=self.headers, data=body)
print(resp)
print(resp.text)
self.send_response(200)
ThreadingHTTPServer(('localhost', 4200), fuwuqi).serve_forever()

View file

@ -0,0 +1,24 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://0.exozy.me/users/test.jsonld",
"type": "Person",
"preferredUsername": "test",
"name": "Billiam Wender",
"inbox": "https://0.exozy.me/users/test.inbox",
"outbox": "https://0.exozy.me/users/test.outbox",
"followers": "https://0.exozy.me/users/test.followers",
"following": "https://0.exozy.me/users/test.following",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://0.exozy.me/users/test.png"
},
"publicKey": {
"id": "https://0.exozy.me/users/test.jsonld#main-key",
"owner": "https://0.exozy.me/users/test.jsonld",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg\nxMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5\nhdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q\ncEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e\ni4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j\n/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO\n9QIDAQAB\n-----END PUBLIC KEY-----\n"
}
}

File diff suppressed because one or more lines are too long

6
users/test.followers Normal file
View file

@ -0,0 +1,6 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}

6
users/test.following Normal file
View file

@ -0,0 +1,6 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}

6
users/test.inbox Normal file
View file

@ -0,0 +1,6 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}

View file

@ -9,6 +9,8 @@
"name": "Billiam Wender",
"inbox": "https://0.exozy.me/users/test.inbox",
"outbox": "https://0.exozy.me/users/test.outbox",
"followers": "https://0.exozy.me/users/test.followers",
"following": "https://0.exozy.me/users/test.following",
"icon": {
"type": "Image",
"mediaType": "image/png",

1
users/test.outbox Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"id": "https://0.exozy.me/users/test.statuses/hello-world", "type": "Note", "attributedTo": "https://0.exozy.me/users/test.jsonld", "inReplyTo": "https://social.exozy.me/@a/109707513227348721", "content": "Hello from fuwuqi!", "to": "https://www.w3.org/ns/activitystreams#Public"}

View file

@ -0,0 +1 @@
{"id": "https://0.exozy.me/users/test.statuses/hello-world2", "type": "Note", "attributedTo": "https://0.exozy.me/users/test.jsonld", "inReplyTo": "https://social.exozy.me/@a/109707513227348721", "content": "Hello from fuwuqi! 2", "to": "https://www.w3.org/ns/activitystreams#Public"}