Update http signatures processing

fix-like-payload
Alain St-Denis 2023-01-21 15:35:48 +00:00
rodzic a4019d88f9
commit 52c96532dc
7 zmienionych plików z 61 dodań i 43 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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=[