kopia lustrzana https://gitlab.com/jaywink/federation
Implement LD signature verification.
rodzic
49b29f6ab4
commit
012db475e1
|
@ -16,6 +16,7 @@ from marshmallow.utils import EXCLUDE, missing
|
|||
from pyld import jsonld
|
||||
|
||||
import federation.entities.base as base
|
||||
import federation.utils.jsonld_helper
|
||||
from federation.entities.activitypub.constants import CONTEXT, CONTEXT_SETS, NAMESPACE_PUBLIC
|
||||
from federation.entities.mixins import BaseEntity, RawContentMixin
|
||||
from federation.entities.utils import get_base_attributes, get_profile
|
||||
|
@ -23,39 +24,11 @@ from federation.outbound import handle_send
|
|||
from federation.types import UserType, ReceiverVariant
|
||||
from federation.utils.activitypub import retrieve_and_parse_document, retrieve_and_parse_profile, \
|
||||
get_profile_id_from_webfinger
|
||||
from federation.utils.django import get_configuration, get_redis
|
||||
from federation.utils.django import get_configuration
|
||||
from federation.utils.text import with_slash, validate_handle
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
||||
cache = get_redis() or {}
|
||||
EXPIRATION = int(timedelta(weeks=2).total_seconds())
|
||||
|
||||
# This is required to workaround a bug in pyld that has the Accept header
|
||||
# accept other content types. From what I understand, precedence handling
|
||||
# is broken
|
||||
# from https://github.com/digitalbazaar/pyld/issues/133
|
||||
# cacheing loosely inspired by https://github.com/digitalbazaar/pyld/issues/70
|
||||
def get_loader(*args, **kwargs):
|
||||
requests_loader = jsonld.requests_document_loader(*args, **kwargs)
|
||||
|
||||
def loader(url, options={}):
|
||||
key = f'ld_cache:{url}'
|
||||
try:
|
||||
return json.loads(cache[key])
|
||||
except KeyError:
|
||||
options['headers']['Accept'] = 'application/ld+json'
|
||||
doc = requests_loader(url, options)
|
||||
if isinstance(cache, dict):
|
||||
cache[url] = json.dumps(doc)
|
||||
else:
|
||||
cache.set(f'ld_cache:{url}', json.dumps(doc), ex=EXPIRATION)
|
||||
return doc
|
||||
|
||||
return loader
|
||||
|
||||
jsonld.set_document_loader(get_loader())
|
||||
|
||||
|
||||
def get_profile_or_entity(fid):
|
||||
obj = get_profile(fid=fid)
|
||||
|
@ -316,7 +289,8 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation):
|
|||
# This is to ensure the original payload is relayed in order
|
||||
# to preserve the validity of the LD signature.
|
||||
# Note: the function name comes from the Diaspora logic and does
|
||||
# not reflect what is actually happening here
|
||||
# not reflect what is actually happening here. For AP, the parent
|
||||
# user's key is used for the http signature.
|
||||
def sign_with_parent(self, private_key):
|
||||
self.outbound_doc = getattr(self, '_source_object', None)
|
||||
|
||||
|
@ -1365,7 +1339,7 @@ def extract_replies(replies):
|
|||
obj = obj.to_base()
|
||||
extract_and_validate(obj)
|
||||
except ValueError as ex:
|
||||
logger.error("extract_replies - Failed to validate entity %s: %s", entity, ex)
|
||||
logger.error("extract_replies - Failed to validate entity %s: %s", obj, ex)
|
||||
continue
|
||||
elif not isinstance(obj, str): continue
|
||||
objs.append(obj)
|
||||
|
|
|
@ -5,16 +5,22 @@ https://funkwhale.audio/
|
|||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from base64 import b64decode
|
||||
from copy import copy
|
||||
from funcy import omit
|
||||
from pyld import jsonld
|
||||
from typing import Union
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import pytz
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
from Crypto.PublicKey.RSA import RsaKey, import_key
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from httpsig.sign_algorithms import PSS
|
||||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
from httpsig.verify import HeaderVerifier
|
||||
|
||||
import federation.utils.jsonld_helper
|
||||
from federation.types import RequestType
|
||||
from federation.utils.network import parse_http_date
|
||||
from federation.utils.text import encode_if_text
|
||||
|
@ -91,4 +97,40 @@ def verify_ld_signature(payload):
|
|||
"""
|
||||
Verify inbound payload LD signature
|
||||
"""
|
||||
pass
|
||||
signature = copy(payload.get('signature'))
|
||||
if not signature:
|
||||
logger.warning('ld_signature - No LD signature in the payload')
|
||||
return None # Maybe False would be better?
|
||||
|
||||
# retrieve the author's public key
|
||||
from federation.utils.activitypub import retrieve_and_parse_document
|
||||
|
||||
profile = retrieve_and_parse_document(signature.get('creator'))
|
||||
if not profile:
|
||||
logger.warning(f'ld_signature - Failed to retrieve profile for {signature.get("creator")}')
|
||||
try:
|
||||
pkey = import_key(profile.public_key)
|
||||
except ValueError as exc:
|
||||
logger.warning(f'ld_signature - {exc}')
|
||||
return None
|
||||
verifier = pkcs1_15.new(pkey)
|
||||
|
||||
# Compute digests and verify signature
|
||||
sig = omit(signature, ('type', 'signatureValue'))
|
||||
sig.update({'@context':'https://w3id.org/security/v1'})
|
||||
sig_nquads = jsonld.normalize(sig, options={'format':'application/nquads','algorithm':'URDNA2015'}).encode('utf-8')
|
||||
sig_digest = SHA256.new(sig_nquads).hexdigest()
|
||||
obj = omit(payload, 'signature')
|
||||
obj_nquads = jsonld.normalize(obj, options={'format':'application/nquads','algorithm':'URDNA2015'}).encode('utf-8')
|
||||
obj_digest = SHA256.new(obj_nquads).hexdigest()
|
||||
digest = (sig_digest + obj_digest).encode('utf-8')
|
||||
|
||||
sig_value = b64decode(signature.get('signatureValue'))
|
||||
try:
|
||||
verifier.verify(SHA256.new(digest), sig_value)
|
||||
logger.debug(f'ld_signature - {payload.get("id")} has a valid signature')
|
||||
except ValueError as exc:
|
||||
logger.warning(f'ld_signature - invalid signature for {payload.get("id")}')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
from datetime import timedelta
|
||||
from pyld import jsonld
|
||||
|
||||
from federation.utils.django import get_redis
|
||||
|
||||
|
||||
cache = get_redis() or {}
|
||||
EXPIRATION = int(timedelta(weeks=4).total_seconds())
|
||||
|
||||
|
||||
# This is required to workaround a bug in pyld that has the Accept header
|
||||
# accept other content types. From what I understand, precedence handling
|
||||
# is broken
|
||||
# from https://github.com/digitalbazaar/pyld/issues/133
|
||||
# cacheing loosely inspired by https://github.com/digitalbazaar/pyld/issues/70
|
||||
def get_loader(*args, **kwargs):
|
||||
requests_loader = jsonld.requests_document_loader(*args, **kwargs)
|
||||
|
||||
def loader(url, options={}):
|
||||
key = f'ld_cache:{url}'
|
||||
try:
|
||||
return json.loads(cache[key])
|
||||
except KeyError:
|
||||
options['headers']['Accept'] = 'application/ld+json'
|
||||
doc = requests_loader(url, options)
|
||||
if isinstance(cache, dict):
|
||||
cache[url] = json.dumps(doc)
|
||||
else:
|
||||
cache.set(f'ld_cache:{url}', json.dumps(doc), ex=EXPIRATION)
|
||||
return doc
|
||||
|
||||
return loader
|
||||
|
||||
|
||||
jsonld.set_document_loader(get_loader())
|
Ładowanie…
Reference in New Issue