kopia lustrzana https://gitlab.com/jaywink/federation
Update http signatures processing
rodzic
a4019d88f9
commit
52c96532dc
|
@ -1,6 +1,5 @@
|
|||
from cryptography.exceptions import InvalidSignature
|
||||
from django.http import JsonResponse, HttpResponse, HttpResponseNotFound
|
||||
from requests_http_signature import HTTPSignatureHeaderAuth
|
||||
|
||||
from federation.entities.activitypub.mappers import get_outbound_entity
|
||||
from federation.protocols.activitypub.signing import verify_request_signature
|
||||
|
@ -24,15 +23,10 @@ def get_and_verify_signer(request):
|
|||
body=request.body,
|
||||
method=request.method,
|
||||
headers=request.headers)
|
||||
sig = HTTPSignatureHeaderAuth.get_sig_struct(req)
|
||||
signer = sig.get('keyId', '').split('#')[0]
|
||||
key = get_public_key(signer)
|
||||
if key:
|
||||
try:
|
||||
verify_request_signature(req, key)
|
||||
return signer
|
||||
except InvalidSignature:
|
||||
return None
|
||||
try:
|
||||
return verify_request_signature(req, required=False)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def activitypub_object_view(func):
|
||||
|
|
|
@ -256,7 +256,7 @@ OBJECTS = [
|
|||
|
||||
|
||||
def set_public(entity):
|
||||
for attr in [getattr(entity, 'to', []), getattr(entity, 'cc' ,[])]:
|
||||
for attr in [entity.to, entity.cc]:
|
||||
if isinstance(attr, list):
|
||||
if NAMESPACE_PUBLIC in attr: entity.public = True
|
||||
elif attr == NAMESPACE_PUBLIC: entity.public = True
|
||||
|
@ -1318,8 +1318,8 @@ def extract_replies(replies):
|
|||
visited = []
|
||||
|
||||
def walk_reply_collection(replies):
|
||||
items = getattr(replies, 'items', [])
|
||||
if items and not isinstance(items, list): items = [items]
|
||||
items = replies.items if replies.items is not missing else []
|
||||
if not isinstance(items, list): items = [items]
|
||||
for obj in items:
|
||||
if isinstance(obj, Note):
|
||||
try:
|
||||
|
@ -1330,7 +1330,7 @@ def extract_replies(replies):
|
|||
continue
|
||||
elif not isinstance(obj, str): continue
|
||||
objs.append(obj)
|
||||
if getattr(replies, 'next_', None):
|
||||
if replies.next_ is not missing:
|
||||
if (replies.id != replies.next_) and (replies.next_ not in visited):
|
||||
resp = retrieve_and_parse_document(replies.next_)
|
||||
if resp:
|
||||
|
|
|
@ -5,7 +5,6 @@ from typing import Callable, Tuple, Union, Dict
|
|||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
from requests_http_signature import HTTPSignatureHeaderAuth
|
||||
|
||||
from federation.entities.activitypub.enums import ActorType
|
||||
from federation.entities.mixins import BaseEntity
|
||||
|
@ -88,16 +87,11 @@ class Protocol:
|
|||
if not skip_author_verification:
|
||||
try:
|
||||
self.verify_signature()
|
||||
except (KeyError, InvalidSignature) as exc:
|
||||
except (ValueError, KeyError, InvalidSignature) as exc:
|
||||
logger.warning(f'Signature verification failed: {exc}')
|
||||
return self.actor, {}
|
||||
return self.actor, self.payload
|
||||
|
||||
def verify_signature(self):
|
||||
# Verify the HTTP signature
|
||||
sig = HTTPSignatureHeaderAuth.get_sig_struct(self.request)
|
||||
signer = sig.get('keyId', '').split('#')[0] if sig.get('keyId') else self.actor
|
||||
key = self.get_contact_key(signer)
|
||||
if self.request.headers.get('Signature') and not key:
|
||||
raise KeyError(f'No public key found for {signer}')
|
||||
verify_request_signature(self.request, key)
|
||||
verify_request_signature(self.request)
|
||||
|
|
|
@ -6,10 +6,14 @@ https://funkwhale.audio/
|
|||
import datetime
|
||||
import logging
|
||||
from typing import Union
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import pytz
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
from requests_http_signature import HTTPSignatureHeaderAuth
|
||||
from httpsig.sign_algorithms import PSS
|
||||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
from httpsig.verify import HeaderVerifier
|
||||
|
||||
|
||||
from federation.types import RequestType
|
||||
from federation.utils.network import parse_http_date
|
||||
|
@ -18,24 +22,46 @@ from federation.utils.text import encode_if_text
|
|||
logger = logging.getLogger("federation")
|
||||
|
||||
|
||||
def get_http_authentication(private_key: RsaKey, private_key_id: str) -> HTTPSignatureHeaderAuth:
|
||||
def get_http_authentication(private_key: RsaKey, private_key_id: str, digest: bool=True) -> HTTPSignatureAuth:
|
||||
"""
|
||||
Get HTTP signature authentication for a request.
|
||||
"""
|
||||
key = private_key.exportKey()
|
||||
return HTTPSignatureHeaderAuth(
|
||||
headers=["(request-target)", "user-agent", "host", "date"],
|
||||
headers = ["(request-target)", "user-agent", "host", "date"]
|
||||
if digest: headers.append('digest')
|
||||
return HTTPSignatureAuth(
|
||||
headers=headers,
|
||||
algorithm="rsa-sha256",
|
||||
key=key,
|
||||
secret=key,
|
||||
key_id=private_key_id,
|
||||
)
|
||||
|
||||
|
||||
def verify_request_signature(request: RequestType, public_key: Union[str, bytes]):
|
||||
def verify_request_signature(request: RequestType, required: bool=True):
|
||||
"""
|
||||
Verify HTTP signature in request against a public key.
|
||||
"""
|
||||
key = encode_if_text(public_key)
|
||||
from federation.utils.activitypub import retrieve_and_parse_document
|
||||
|
||||
sig_struct = request.headers.get("Signature", None)
|
||||
if not sig_struct:
|
||||
if required:
|
||||
raise ValueError("A signature is required but was not provided")
|
||||
else:
|
||||
return None
|
||||
|
||||
# this should return a dict populated with the following keys:
|
||||
# keyId, algorithm, headers and signature
|
||||
sig = {i.split("=", 1)[0]: i.split("=", 1)[1].strip('"') for i in sig_struct.split(",")}
|
||||
signer = retrieve_and_parse_document(sig.get('keyId'))
|
||||
if not signer:
|
||||
raise ValueError(f"Failed to retrieve keyId for {sig.get('keyId')}")
|
||||
|
||||
if not getattr(signer, 'public_key_dict', None):
|
||||
raise ValueError(f"Failed to retrieve public key for {sig.get('keyId')}")
|
||||
|
||||
key = encode_if_text(signer.public_key_dict['publicKeyPem'])
|
||||
|
||||
date_header = request.headers.get("Date")
|
||||
if not date_header:
|
||||
raise ValueError("Request Date header is missing")
|
||||
|
@ -48,4 +74,10 @@ def verify_request_signature(request: RequestType, public_key: Union[str, bytes]
|
|||
if dt < now - past_delta or dt > now + future_delta:
|
||||
raise ValueError("Request Date is too far in future or past")
|
||||
|
||||
HTTPSignatureHeaderAuth.verify(request, key_resolver=lambda **kwargs: key)
|
||||
path = getattr(request, 'path', urlsplit(request.url).path)
|
||||
if not HeaderVerifier(request.headers, key, method=request.method,
|
||||
path=path, sign_header='signature',
|
||||
sign_algorithm=PSS() if sig.get('algorithm',None) == 'hs2019' else None).verify():
|
||||
raise ValueError("Invalid signature")
|
||||
|
||||
return signer.id
|
||||
|
|
|
@ -5,13 +5,13 @@ from federation.tests.fixtures.keys import get_dummy_private_key
|
|||
def test_signing_request():
|
||||
key = get_dummy_private_key()
|
||||
auth = get_http_authentication(key, "dummy_key_id")
|
||||
assert auth.algorithm == 'rsa-sha256'
|
||||
assert auth.headers == [
|
||||
assert auth.header_signer.headers == [
|
||||
'(request-target)',
|
||||
'user-agent',
|
||||
'host',
|
||||
'date',
|
||||
'digest',
|
||||
]
|
||||
assert auth.key == key.exportKey()
|
||||
assert auth.key_id == 'dummy_key_id'
|
||||
assert auth.header_signer.secret == key.exportKey()
|
||||
assert 'dummy_key_id' in auth.header_signer.signature_template
|
||||
|
||||
|
|
|
@ -3,17 +3,15 @@ import logging
|
|||
from typing import Optional, Any
|
||||
|
||||
from federation.protocols.activitypub.signing import get_http_authentication
|
||||
from federation.utils.django import get_federation_user
|
||||
from federation.utils.network import fetch_document, try_retrieve_webfinger_document
|
||||
from federation.utils.text import decode_if_bytes, validate_handle
|
||||
|
||||
logger = logging.getLogger('federation')
|
||||
|
||||
try:
|
||||
from federation.utils.django import get_federation_user
|
||||
federation_user = get_federation_user()
|
||||
except (ImportError, AttributeError):
|
||||
federation_user = None
|
||||
logger.warning("django is required for get requests signing")
|
||||
federation_user = get_federation_user()
|
||||
if not federation_user: logger.warning("django is required for get requests signing")
|
||||
|
||||
|
||||
def get_profile_id_from_webfinger(handle: str) -> Optional[str]:
|
||||
"""
|
||||
|
@ -43,7 +41,7 @@ def retrieve_and_parse_document(fid: str, cache: bool=True) -> Optional[Any]:
|
|||
"""
|
||||
from federation.entities.activitypub.models import element_to_objects # Circulars
|
||||
document, status_code, ex = fetch_document(fid, extra_headers={'accept': 'application/activity+json'}, cache=cache,
|
||||
auth=get_http_authentication(federation_user.rsa_private_key,f'{federation_user.id}#main-key') if federation_user else None)
|
||||
auth=get_http_authentication(federation_user.rsa_private_key,f'{federation_user.id}#main-key', digest=False) if federation_user else None)
|
||||
if document:
|
||||
try:
|
||||
document = json.loads(decode_if_bytes(document))
|
||||
|
|
4
setup.py
4
setup.py
|
@ -31,7 +31,7 @@ setup(
|
|||
"bleach>3.0",
|
||||
"calamus",
|
||||
"commonmark",
|
||||
"cryptography<=3.4.7",
|
||||
"cryptography",
|
||||
"cssselect>=0.9.2",
|
||||
"dirty-validators>=0.3.0",
|
||||
"lxml>=3.4.0",
|
||||
|
@ -47,7 +47,7 @@ setup(
|
|||
"redis",
|
||||
"requests>=2.8.0",
|
||||
"requests-cache",
|
||||
"requests-http-signature-jaywink>=0.1.0.dev0",
|
||||
"httpsig @ git+https://github.com/tripougnif/python-httpsig-socialhome.git@ce03fa7b25acfacc14fba2670c33246025db7be0#egg=httpsig==0.1",
|
||||
],
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
|
|
Ładowanie…
Reference in New Issue