kopia lustrzana https://gitlab.com/jaywink/federation
Merge branch 'http-signature' into 'master'
Update http signatures processing See merge request jaywink/federation!170fix-like-payload
commit
420292679f
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -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=[
|
||||||
|
|
Ładowanie…
Reference in New Issue