Cleanup context and LD sig
parent
e01a9ddbe4
commit
489ed6cbe0
|
@ -20,6 +20,30 @@ AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
ACTOR_TYPES = ["Application", "Group", "Organization", "Person", "Service"]
|
ACTOR_TYPES = ["Application", "Group", "Organization", "Person", "Service"]
|
||||||
|
|
||||||
|
AS_EXTENDED_CTX = [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
# AS ext
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"},
|
||||||
|
# toot
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"featured": {"@id": "toot:featured", "@type": "@id"},
|
||||||
|
"Emoji": "toot:Emoji",
|
||||||
|
"blurhash": "toot:blurhash",
|
||||||
|
# schema
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value",
|
||||||
|
# ostatus
|
||||||
|
"ostatus": "http://ostatus.org#",
|
||||||
|
"conversation": "ostatus:conversation",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ObjectIsGoneError(Exception):
|
class ObjectIsGoneError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -41,45 +65,8 @@ class VisibilityEnum(str, enum.Enum):
|
||||||
}[key]
|
}[key]
|
||||||
|
|
||||||
|
|
||||||
MICROBLOGPUB = {
|
|
||||||
"@context": [
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://w3id.org/security/v1",
|
|
||||||
{
|
|
||||||
"Hashtag": "as:Hashtag",
|
|
||||||
"PropertyValue": "schema:PropertyValue",
|
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
|
||||||
"ostatus": "http://ostatus.org#",
|
|
||||||
"schema": "http://schema.org",
|
|
||||||
"sensitive": "as:sensitive",
|
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
|
||||||
"totalItems": "as:totalItems",
|
|
||||||
"value": "schema:value",
|
|
||||||
"Emoji": "toot:Emoji",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_CTX = COLLECTION_CTX = [
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://w3id.org/security/v1",
|
|
||||||
{
|
|
||||||
# AS ext
|
|
||||||
"Hashtag": "as:Hashtag",
|
|
||||||
"sensitive": "as:sensitive",
|
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
|
||||||
# toot
|
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
|
||||||
# "featured": "toot:featured",
|
|
||||||
# schema
|
|
||||||
"schema": "http://schema.org#",
|
|
||||||
"PropertyValue": "schema:PropertyValue",
|
|
||||||
"value": "schema:value",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
ME = {
|
ME = {
|
||||||
"@context": DEFAULT_CTX,
|
"@context": AS_EXTENDED_CTX,
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"id": config.ID,
|
"id": config.ID,
|
||||||
"following": config.BASE_URL + "/following",
|
"following": config.BASE_URL + "/following",
|
||||||
|
@ -235,7 +222,7 @@ def get_actor_id(activity: RawObject) -> str:
|
||||||
|
|
||||||
def wrap_object(activity: RawObject) -> RawObject:
|
def wrap_object(activity: RawObject) -> RawObject:
|
||||||
return {
|
return {
|
||||||
"@context": AS_CTX,
|
"@context": AS_EXTENDED_CTX,
|
||||||
"actor": config.ID,
|
"actor": config.ID,
|
||||||
"to": activity.get("to", []),
|
"to": activity.get("to", []),
|
||||||
"cc": activity.get("cc", []),
|
"cc": activity.get("cc", []),
|
||||||
|
|
|
@ -273,7 +273,7 @@ def send_create(
|
||||||
raise ValueError(f"Unhandled visibility {visibility}")
|
raise ValueError(f"Unhandled visibility {visibility}")
|
||||||
|
|
||||||
note = {
|
note = {
|
||||||
"@context": ap.AS_CTX,
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
"type": "Note",
|
"type": "Note",
|
||||||
"id": outbox_object_id(note_id),
|
"id": outbox_object_id(note_id),
|
||||||
"attributedTo": ID,
|
"attributedTo": ID,
|
||||||
|
|
24
app/ldsig.py
24
app/ldsig.py
|
@ -2,29 +2,31 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
import pyld # type: ignore
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
from Crypto.Signature import PKCS1_v1_5
|
||||||
from pyld import jsonld # type: ignore
|
from pyld import jsonld # type: ignore
|
||||||
|
|
||||||
|
from app import activitypub as ap
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from app.key import Key
|
from app.key import Key
|
||||||
|
|
||||||
|
|
||||||
_LOADER = jsonld.requests_document_loader()
|
requests_loader = pyld.documentloader.requests.requests_document_loader()
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(256)
|
def _loader(url, options={}):
|
||||||
def _caching_document_loader(url: str) -> Any:
|
# See https://github.com/digitalbazaar/pyld/issues/133
|
||||||
return _LOADER(url)
|
options["headers"]["Accept"] = "application/ld+json"
|
||||||
|
return requests_loader(url, options)
|
||||||
|
|
||||||
|
|
||||||
jsonld.set_document_loader(_caching_document_loader)
|
pyld.jsonld.set_document_loader(_loader)
|
||||||
|
|
||||||
|
|
||||||
def _options_hash(doc):
|
def _options_hash(doc: ap.RawObject) -> str:
|
||||||
doc = dict(doc["signature"])
|
doc = dict(doc["signature"])
|
||||||
for k in ["type", "id", "signatureValue"]:
|
for k in ["type", "id", "signatureValue"]:
|
||||||
if k in doc:
|
if k in doc:
|
||||||
|
@ -38,7 +40,7 @@ def _options_hash(doc):
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def _doc_hash(doc):
|
def _doc_hash(doc: ap.RawObject) -> str:
|
||||||
doc = dict(doc)
|
doc = dict(doc)
|
||||||
if "signature" in doc:
|
if "signature" in doc:
|
||||||
del doc["signature"]
|
del doc["signature"]
|
||||||
|
@ -50,7 +52,7 @@ def _doc_hash(doc):
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def verify_signature(doc, key: "Key"):
|
def verify_signature(doc: ap.RawObject, key: "Key") -> bool:
|
||||||
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
to_be_signed = _options_hash(doc) + _doc_hash(doc)
|
||||||
signature = doc["signature"]["signatureValue"]
|
signature = doc["signature"]["signatureValue"]
|
||||||
signer = PKCS1_v1_5.new(key.pubkey or key.privkey) # type: ignore
|
signer = PKCS1_v1_5.new(key.pubkey or key.privkey) # type: ignore
|
||||||
|
@ -59,7 +61,7 @@ def verify_signature(doc, key: "Key"):
|
||||||
return signer.verify(digest, base64.b64decode(signature)) # type: ignore
|
return signer.verify(digest, base64.b64decode(signature)) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def generate_signature(doc, key: "Key"):
|
def generate_signature(doc: ap.RawObject, key: "Key") -> None:
|
||||||
options = {
|
options = {
|
||||||
"type": "RsaSignature2017",
|
"type": "RsaSignature2017",
|
||||||
"creator": doc["actor"] + "#main-key",
|
"creator": doc["actor"] + "#main-key",
|
||||||
|
|
27
app/main.py
27
app/main.py
|
@ -133,25 +133,6 @@ async def add_security_headers(request: Request, call_next):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CTX = COLLECTION_CTX = [
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://w3id.org/security/v1",
|
|
||||||
{
|
|
||||||
# AS ext
|
|
||||||
"Hashtag": "as:Hashtag",
|
|
||||||
"sensitive": "as:sensitive",
|
|
||||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
|
||||||
# toot
|
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
|
||||||
# "featured": "toot:featured",
|
|
||||||
# schema
|
|
||||||
"schema": "http://schema.org#",
|
|
||||||
"PropertyValue": "schema:PropertyValue",
|
|
||||||
"value": "schema:value",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ActivityPubResponse(JSONResponse):
|
class ActivityPubResponse(JSONResponse):
|
||||||
media_type = "application/activity+json"
|
media_type = "application/activity+json"
|
||||||
|
|
||||||
|
@ -372,7 +353,7 @@ def outbox(
|
||||||
)
|
)
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
{
|
{
|
||||||
"@context": DEFAULT_CTX,
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
"id": f"{ID}/outbox",
|
"id": f"{ID}/outbox",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"totalItems": len(outbox_objects),
|
"totalItems": len(outbox_objects),
|
||||||
|
@ -402,7 +383,7 @@ def featured(
|
||||||
)
|
)
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
{
|
{
|
||||||
"@context": DEFAULT_CTX,
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
"id": f"{ID}/featured",
|
"id": f"{ID}/featured",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"totalItems": len(outbox_objects),
|
"totalItems": len(outbox_objects),
|
||||||
|
@ -512,7 +493,7 @@ def tag_by_name(
|
||||||
# if is_activitypub_requested(request):
|
# if is_activitypub_requested(request):
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
{
|
{
|
||||||
"@context": ap.AS_CTX,
|
"@context": ap.AS_CTX, # XXX: extended ctx?
|
||||||
"id": BASE_URL + f"/t/{tag}",
|
"id": BASE_URL + f"/t/{tag}",
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
|
@ -528,7 +509,7 @@ def emoji_by_name(name: str) -> ActivityPubResponse:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
return ActivityPubResponse({"@context": ap.AS_CTX, **emoji})
|
return ActivityPubResponse({"@context": ap.AS_EXTENDED_CTX, **emoji})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/inbox")
|
@app.post("/inbox")
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app import activitypub as ap
|
||||||
|
from app import ldsig
|
||||||
|
from app.key import Key
|
||||||
|
from tests import factories
|
||||||
|
|
||||||
|
_SAMPLE_CREATE = {
|
||||||
|
"type": "Create",
|
||||||
|
"actor": "https://microblog.pub",
|
||||||
|
"object": {
|
||||||
|
"type": "Note",
|
||||||
|
"sensitive": False,
|
||||||
|
"cc": ["https://microblog.pub/followers"],
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": "<p>Hello world!</p>",
|
||||||
|
"tag": [],
|
||||||
|
"attributedTo": "https://microblog.pub",
|
||||||
|
"published": "2018-05-21T15:51:59Z",
|
||||||
|
"id": "https://microblog.pub/outbox/988179f13c78b3a7/activity",
|
||||||
|
"url": "https://microblog.pub/note/988179f13c78b3a7",
|
||||||
|
},
|
||||||
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
|
"published": "2018-05-21T15:51:59Z",
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc": ["https://microblog.pub/followers"],
|
||||||
|
"id": "https://microblog.pub/outbox/988179f13c78b3a7",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Working but slow")
|
||||||
|
def test_linked_data_sig():
|
||||||
|
privkey, pubkey = factories.generate_key()
|
||||||
|
ra = factories.RemoteActorFactory(
|
||||||
|
base_url="https://microblog.pub",
|
||||||
|
username="dev",
|
||||||
|
public_key=pubkey,
|
||||||
|
)
|
||||||
|
k = Key(ra.ap_id, f"{ra.ap_id}#main-key")
|
||||||
|
k.load(privkey)
|
||||||
|
|
||||||
|
doc = deepcopy(_SAMPLE_CREATE)
|
||||||
|
|
||||||
|
ldsig.generate_signature(doc, k)
|
||||||
|
assert ldsig.verify_signature(doc, k)
|
Loading…
Reference in New Issue