Implement LD signature verification.

ld-signatures
Alain St-Denis 2023-03-12 17:15:47 -04:00
rodzic 49b29f6ab4
commit 012db475e1
3 zmienionych plików z 85 dodań i 33 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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")}')

Wyświetl plik

@ -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())