Merge branch 'http-signature' into 'master'

Update http signatures processing

See merge request jaywink/federation!170
fix-like-payload
Alain St-Denis 2023-01-21 15:35:48 +00:00
commit 420292679f
7 zmienionych plików z 61 dodań i 43 usunięć

Wyświetl plik

@ -1,6 +1,5 @@
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from django.http import JsonResponse, HttpResponse, HttpResponseNotFound from django.http import JsonResponse, HttpResponse, HttpResponseNotFound
from requests_http_signature import HTTPSignatureHeaderAuth
from federation.entities.activitypub.mappers import get_outbound_entity from federation.entities.activitypub.mappers import get_outbound_entity
from federation.protocols.activitypub.signing import verify_request_signature from federation.protocols.activitypub.signing import verify_request_signature
@ -24,15 +23,10 @@ def get_and_verify_signer(request):
body=request.body, body=request.body,
method=request.method, method=request.method,
headers=request.headers) headers=request.headers)
sig = HTTPSignatureHeaderAuth.get_sig_struct(req) try:
signer = sig.get('keyId', '').split('#')[0] return verify_request_signature(req, required=False)
key = get_public_key(signer) except ValueError:
if key: return None
try:
verify_request_signature(req, key)
return signer
except InvalidSignature:
return None
def activitypub_object_view(func): def activitypub_object_view(func):

Wyświetl plik

@ -256,7 +256,7 @@ OBJECTS = [
def set_public(entity): 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 isinstance(attr, list):
if NAMESPACE_PUBLIC in attr: entity.public = True if NAMESPACE_PUBLIC in attr: entity.public = True
elif attr == NAMESPACE_PUBLIC: entity.public = True elif attr == NAMESPACE_PUBLIC: entity.public = True
@ -1318,8 +1318,8 @@ def extract_replies(replies):
visited = [] visited = []
def walk_reply_collection(replies): def walk_reply_collection(replies):
items = getattr(replies, 'items', []) items = replies.items if replies.items is not missing else []
if items and not isinstance(items, list): items = [items] if not isinstance(items, list): items = [items]
for obj in items: for obj in items:
if isinstance(obj, Note): if isinstance(obj, Note):
try: try:
@ -1330,7 +1330,7 @@ def extract_replies(replies):
continue continue
elif not isinstance(obj, str): continue elif not isinstance(obj, str): continue
objs.append(obj) 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): if (replies.id != replies.next_) and (replies.next_ not in visited):
resp = retrieve_and_parse_document(replies.next_) resp = retrieve_and_parse_document(replies.next_)
if resp: if resp:

Wyświetl plik

@ -5,7 +5,6 @@ from typing import Callable, Tuple, Union, Dict
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from Crypto.PublicKey.RSA import RsaKey from Crypto.PublicKey.RSA import RsaKey
from requests_http_signature import HTTPSignatureHeaderAuth
from federation.entities.activitypub.enums import ActorType from federation.entities.activitypub.enums import ActorType
from federation.entities.mixins import BaseEntity from federation.entities.mixins import BaseEntity
@ -88,16 +87,11 @@ class Protocol:
if not skip_author_verification: if not skip_author_verification:
try: try:
self.verify_signature() self.verify_signature()
except (KeyError, InvalidSignature) as exc: except (ValueError, KeyError, InvalidSignature) as exc:
logger.warning(f'Signature verification failed: {exc}') logger.warning(f'Signature verification failed: {exc}')
return self.actor, {} return self.actor, {}
return self.actor, self.payload return self.actor, self.payload
def verify_signature(self): def verify_signature(self):
# Verify the HTTP signature # Verify the HTTP signature
sig = HTTPSignatureHeaderAuth.get_sig_struct(self.request) verify_request_signature(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)

Wyświetl plik

@ -6,10 +6,14 @@ https://funkwhale.audio/
import datetime import datetime
import logging import logging
from typing import Union from typing import Union
from urllib.parse import urlsplit
import pytz import pytz
from Crypto.PublicKey.RSA import RsaKey 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.types import RequestType
from federation.utils.network import parse_http_date from federation.utils.network import parse_http_date
@ -18,24 +22,46 @@ from federation.utils.text import encode_if_text
logger = logging.getLogger("federation") 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. Get HTTP signature authentication for a request.
""" """
key = private_key.exportKey() 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", algorithm="rsa-sha256",
key=key, secret=key,
key_id=private_key_id, 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. 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") date_header = request.headers.get("Date")
if not date_header: if not date_header:
raise ValueError("Request Date header is missing") 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: if dt < now - past_delta or dt > now + future_delta:
raise ValueError("Request Date is too far in future or past") 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(): def test_signing_request():
key = get_dummy_private_key() key = get_dummy_private_key()
auth = get_http_authentication(key, "dummy_key_id") auth = get_http_authentication(key, "dummy_key_id")
assert auth.algorithm == 'rsa-sha256' assert auth.header_signer.headers == [
assert auth.headers == [
'(request-target)', '(request-target)',
'user-agent', 'user-agent',
'host', 'host',
'date', 'date',
'digest',
] ]
assert auth.key == key.exportKey() assert auth.header_signer.secret == key.exportKey()
assert auth.key_id == 'dummy_key_id' 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 typing import Optional, Any
from federation.protocols.activitypub.signing import get_http_authentication 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.network import fetch_document, try_retrieve_webfinger_document
from federation.utils.text import decode_if_bytes, validate_handle from federation.utils.text import decode_if_bytes, validate_handle
logger = logging.getLogger('federation') logger = logging.getLogger('federation')
try: federation_user = get_federation_user()
from federation.utils.django import get_federation_user if not federation_user: logger.warning("django is required for get requests signing")
federation_user = get_federation_user()
except (ImportError, AttributeError):
federation_user = None
logger.warning("django is required for get requests signing")
def get_profile_id_from_webfinger(handle: str) -> Optional[str]: 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 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, 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: if document:
try: try:
document = json.loads(decode_if_bytes(document)) document = json.loads(decode_if_bytes(document))

Wyświetl plik

@ -31,7 +31,7 @@ setup(
"bleach>3.0", "bleach>3.0",
"calamus", "calamus",
"commonmark", "commonmark",
"cryptography<=3.4.7", "cryptography",
"cssselect>=0.9.2", "cssselect>=0.9.2",
"dirty-validators>=0.3.0", "dirty-validators>=0.3.0",
"lxml>=3.4.0", "lxml>=3.4.0",
@ -47,7 +47,7 @@ setup(
"redis", "redis",
"requests>=2.8.0", "requests>=2.8.0",
"requests-cache", "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, include_package_data=True,
classifiers=[ classifiers=[