Skip to content

Commit

Permalink
Implemented basic AP mechanisms
Browse files Browse the repository at this point in the history
  • Loading branch information
yabirgb committed Feb 7, 2019
1 parent eaa127f commit 3d4b890
Show file tree
Hide file tree
Showing 19 changed files with 319 additions and 118 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ aiohttp = "*"
peewee-db-evolve = "*"
cachetools = "*"
async-lru = "*"
pyld = "*"
imageio-ffmpeg = "*"

[requires]
python_version = "3.6"
Expand Down
25 changes: 22 additions & 3 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/activityPub/activities/verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class Follow(Activity):
class Reject(Activity):
type = "Reject"

class Undo(Activity):
type = "Undo"

class Accept(Activity):
type = "Accept"

Expand All @@ -83,5 +86,5 @@ class RsaSignature2017(Activity):
"Follow": Follow,
"Accept": Accept,
"Reject": Reject,
"RsaSignature2017": RsaSignature2017
"Undo": Undo
})
40 changes: 40 additions & 0 deletions src/activityPub/activitypub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import json
import logging
from typing import (Dict, Any)
import aiohttp
import requests
from settings import BASE_URL
from models.user import UserProfile
from activityPub.data_signature import generate_signature, sign_headers, HTTPSignaturesAuthRequest
from activityPub.key import CryptoKey

log = logging.getLogger(__name__)


def push_to_remote_actor(target: UserProfile, body: Dict) -> Any:

"""
Send activity to target inbox
"""

# Create Key
k = CryptoKey(body["actor"])
k.new()
generate_signature(body, k)

data = json.dumps(body)
print(data)

auth = HTTPSignaturesAuthRequest(k)
r = requests.post(target.uris.inbox, json=body, auth = auth)
print(r.status_code)
#print(r.request.headers)
#print(r.content)
if r.status_code < 400:
return True

return False




97 changes: 69 additions & 28 deletions src/activityPub/data_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from requests.auth import AuthBase
from pyld import jsonld

from src.activityPub.key import CryptoKey
from activityPub.key import CryptoKey


log = logging.getLogger(__name__)

def _build_signed_string(
signed_headers: str, method: str, path: str, headers: Any, body_digest: str
) -> str:
out = []

class HTTPSignaturesAuth(AuthBase):
for signed_header in signed_headers.split(" "):
if signed_header == "(request-target)":
out.append("(request-target): " + method.lower() + " " + path)
elif signed_header == "digest":
out.append("digest: " + body_digest)
else:
out.append(signed_header + ": " + headers[signed_header])
return "\n".join(out)

def sign_headers(dsf,fdfs,fdff,fdf):
pass

class HTTPSignaturesAuthRequest(AuthBase):

"""
Plugin for request to sign petitions
Expand All @@ -25,29 +42,9 @@ class HTTPSignaturesAuth(AuthBase):
def __init__(self, key: CryptoKey) -> None:
self.key = key

self.headers = ["(request-target)", "user-agent", "host", "date", "digest", "content-type"]

def _build_signed_string(self,
signed_headers: List,
method: str,
path: str,
headers: Any,
body_digest: str
) -> str:

production = []
self.headers = "(request-target) user-agent host date digest content-type"

for header in signed_headers:

if header == '(request-target)':
production.append('(request-target): ' + method.lower() + ' ' + path)
elif header == 'digest'
production.append('digest: ' + body_digest)
else:
production.append(header + ': ' + headers[header])


return '\n'.join(production)

def __call__(self, r):

Expand Down Expand Up @@ -76,16 +73,16 @@ def __call__(self, r):

r.headers.update(new_headers)

hstring = " ".join(self.headers)

to_sign = self._build_signed_string(
to_sign = _build_signed_string(
self.headers,
r.method,
r.path_url,
r.headers,
digest
)

#print(to_sign)

signer = PKCS1_v1_5.new(self.key.privkey)

# Digest of the headers
Expand All @@ -97,10 +94,54 @@ def __call__(self, r):
key_id = self.key.key_id()

headers = {
"Signature": f'keyId="{key_id}",algorithm="rsa-sha256",headers="{hstring}",signature="{sig}"'
"Signature": f'keyId="{key_id}",algorithm="rsa-sha256",headers="{self.headers}",signature="{sig}"'
}

log.debug(f'Signed request headers {headers}')

r.headers.update(headers)
return r
return r

# Manage RSA signatures

def _options_hash(doc):
doc = dict(doc["signature"])
for k in ["type", "id", "signatureValue"]:
if k in doc:
del doc[k]
doc["@context"] = "https://w3id.org/identity/v1"
normalized = jsonld.normalize(
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()


def _doc_hash(doc):
doc = dict(doc)
if "signature" in doc:
del doc["signature"]
normalized = jsonld.normalize(
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()

def generate_signature(doc: Dict, key: 'Key'):

options = {
'type': 'RsaSignature2017',
'creator': doc['actor'] + '#main-key',
'created': datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
}

doc["signature"] = options
to_be_signed = _options_hash(doc) + _doc_hash(doc)
signer = PKCS1_v1_5.new(key.privkey)
digest = SHA256.new()
digest.update(to_be_signed.encode("utf-8"))
sig = base64.b64encode(signer.sign(digest))
options["signatureValue"] = sig.decode("utf-8")
doc["signature"] = options
6 changes: 3 additions & 3 deletions src/activityPub/identity_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import requests
import logging
from urllib.parse import urlparse

from typing import (Any)
from settings import DOMAIN

#ActivityPub
Expand Down Expand Up @@ -44,7 +44,7 @@ def dereference(self):

return as_activitystream(res.json())

def get_or_create_remote_user(self):
def get_or_create_remote_user(self) -> UserProfile:
"""
Returns an instance of User after looking for it using it's ap_id
"""
Expand Down Expand Up @@ -76,7 +76,7 @@ def _local_uri(self, uri):
return uri == DOMAIN


def uri_to_resource(self, klass):
def uri_to_resource(self, klass) -> Any:

if self._local_uri(self.uri):
if klass.__name__ == 'User':
Expand Down
61 changes: 61 additions & 0 deletions src/activityPub/key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import base64
from typing import (Any, Dict, Optional)

from Crypto.PublicKey import RSA
from Crypto.Util import number

from database import DATABASE
from keys import import_keys


class CryptoKey:

DEFAULT_KEY_SIZE = 2048

def __init__(self, owner: str, id_: Optional[str]=None) -> None:
self.owner = owner
self.privkey_pem: Optional[str] = None
self.pubkey_pem: Optional[str] = None
self.privkey: Optional[Any] = None
self.pubkey: Optional[Any] = None
self.id_ = id_

def new(self) -> None:
self.load(import_keys()["actorKeys"]["privateKey"])

def key_id(self) -> str:
return f'{self.owner}#main-key'

def load_pub(self, pubkey_pem: str) -> None:
self.pubkey_pem = pubkey_pem
self.pubkey = RSA.importKey(pubkey_pem)

def load(self, privkey_pem: str) -> None:
self.privkey_pem = privkey_pem
self.privkey = RSA.importKey(self.privkey_pem)
self.pubkey_pem = self.privkey.publickey().exportKey("PEM").decode("utf-8")

def to_dict(self) -> Dict[str, Any]:
return {
"id": self.key_id(),
"owner": self.owner,
"publicKeyPem": self.pubkey_pem,
}

@classmethod
def from_dict(cls, data):
try:
k = cls(data["owner"], data["id"])
k.load_pub(data["publicKeyPem"])
except KeyError:
raise ValueError(f"bad key data {data!r}")
return k

def to_magic_key(self) -> str:
mod = base64.urlsafe_b64encode(
number.long_to_bytes(self.privkey.n) # type: ignore
).decode("utf-8")
pubexp = base64.urlsafe_b64encode(
number.long_to_bytes(self.privkey.e) # type: ignore
).decode("utf-8")
return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
9 changes: 6 additions & 3 deletions src/api/v1/activityPub/inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from activityPub import activities
from activityPub.activities import as_activitystream

from api.v1.activityPub.methods import (store, handle_follow, handle_note)
from api.v1.activityPub.methods import (store, handle_note)
from tasks.ap_methods import handle_follow
from activityPub.activities.verbs import (Accept)

from activityPub.identity_manager import ActivityPubId
Expand All @@ -27,7 +28,7 @@

class Inbox(BaseHandler):

async def post(self, username):
def post(self, username):

#First we check the headers
#Lowercase them to ensure all have the same name
Expand Down Expand Up @@ -55,7 +56,7 @@ async def post(self, username):
result = False
if activity.type == 'Follow':
logging.info(f"Starting follow process for {activity.object}" )
result = await handle_follow(activity)
result = handle_follow(activity)
print(result)
self.set_status(200)
elif activity.type == 'Accept':
Expand All @@ -64,6 +65,8 @@ async def post(self, username):
elif activity.type == 'Create':
#result = handle_create(activity)
pass
elif activity.type == 'Delete':
pass

#store(activity, user, remote = True)
#self.set_status(500)
Loading

0 comments on commit 3d4b890

Please sign in to comment.