Skip to content

Commit

Permalink
refactor: extract signature.py from utils.py
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Aug 14, 2024
1 parent 94baa51 commit 6ca8a3f
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 100 deletions.
105 changes: 105 additions & 0 deletions pod/activitypub/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import base64
from pyld import jsonld
import datetime
import email.utils
import json
from urllib.parse import urlparse

from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from django.conf import settings
from django.urls import reverse


def payload_hash(payload):
payload_json = json.dumps(payload) if isinstance(payload, dict) else payload
payload_hash = SHA256.new(payload_json.encode("utf-8"))
digest = payload_hash.digest()
return digest


def base64_signature(private_key, string):
to_be_signed_str_bytes = bytes(string, "utf-8")
to_be_signed_str_hash = SHA256.new(bytes(to_be_signed_str_bytes))
sig = pkcs1_15.new(private_key).sign(to_be_signed_str_hash)
sig_base64 = base64.b64encode(sig).decode()
return sig_base64


def payload_normalize(payload):
return jsonld.normalize(
payload, {"algorithm": "URDNA2015", "format": "application/n-quads"}
)


def signed_payload_headers(payload, url):
"""Sign JSON-LD payload according to the 'Signing HTTP Messages' RFC draft.
This brings compatibility with peertube (and mastodon for instance).
More information here:
- https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
- https://framacolibri.org/t/rfc9421-replaces-the-signing-http-messages-draft/20911/2
"""
from .utils import ap_url

date = email.utils.formatdate(usegmt=True)
url = urlparse(url)

private_key = RSA.import_key(settings.ACTIVITYPUB_PRIVATE_KEY)
payload_hash_raw = payload_hash(payload)
payload_hash_base64 = base64.b64encode(payload_hash_raw).decode()

to_be_signed_str = (
f"(request-target): post {url.path}\n"
f"host: {url.netloc}\n"
f"date: {date}\n"
f"digest: SHA-256={payload_hash_base64}"
)
sig_base64 = base64_signature(private_key, to_be_signed_str)

public_key_url = ap_url(reverse("activitypub:account")) + "#main-key"
signature_header = f'keyId="{public_key_url}",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="{sig_base64}"'
request_headers = {
"host": url.netloc,
"date": date,
"digest": f"SHA-256={payload_hash_base64}",
"content-type": "application/activity+json",
"signature": signature_header,
}
return request_headers


def signature_payload(payload, url):
"""Sign JSON-LD payload according to the 'Linked Data Signatures 1.0' RFC draft.
This brings compatibility with peertube (and mastodon for instance).
More information here:
- https://web.archive.org/web/20170717200644/https://w3c-dvcg.github.io/ld-signatures/
- https://docs.joinmastodon.org/spec/security/#ld
"""
private_key = RSA.import_key(settings.ACTIVITYPUB_PRIVATE_KEY)

signature = {
"type": "RsaSignature2017",
"creator": payload["actor"],
"created": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
}
options_payload = {
"@context": [
"https://w3id.org/security/v1",
{"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
],
"creator": signature["creator"],
"created": signature["created"],
}
options_normalized = payload_normalize(options_payload)
options_hash = payload_hash(options_normalized)

document_normalized = payload_normalize(payload)
document_hash = payload_hash(document_normalized)

to_sign = options_hash.hex() + document_hash.hex()
signature["signatureValue"] = base64_signature(private_key, to_sign)

return signature
102 changes: 2 additions & 100 deletions pod/activitypub/utils.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import base64
from pyld import jsonld
import datetime
import email.utils
import hashlib
import json
import logging
import random
import uuid
from collections import namedtuple
from urllib.parse import urlencode, urlparse, urlunparse
from urllib.parse import urlencode, urlunparse

import requests
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from django.conf import settings
from django.contrib.sites.models import Site
from django.urls import reverse

from pod.video.models import Video

from .constants import AP_REQUESTS_TIMEOUT, BASE_HEADERS
from .signature import signature_payload, signed_payload_headers

logger = logging.getLogger(__name__)
URLComponents = namedtuple(
Expand Down Expand Up @@ -58,97 +51,6 @@ def ap_url(suffix=""):
return make_url(url=suffix)


def payload_hash(payload):
payload_json = json.dumps(payload) if isinstance(payload, dict) else payload
payload_hash = SHA256.new(payload_json.encode("utf-8"))
digest = payload_hash.digest()
return digest


def base64_signature(private_key, string):
to_be_signed_str_bytes = bytes(string, "utf-8")
to_be_signed_str_hash = SHA256.new(bytes(to_be_signed_str_bytes))
sig = pkcs1_15.new(private_key).sign(to_be_signed_str_hash)
sig_base64 = base64.b64encode(sig).decode()
return sig_base64


def payload_normalize(payload):
return jsonld.normalize(
payload, {"algorithm": "URDNA2015", "format": "application/n-quads"}
)


def signed_payload_headers(payload, url):
"""Sign JSON-LD payload according to the 'Signing HTTP Messages' RFC draft.
This brings compatibility with peertube (and mastodon for instance).
More information here:
- https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
- https://framacolibri.org/t/rfc9421-replaces-the-signing-http-messages-draft/20911/2
"""
date = email.utils.formatdate(usegmt=True)
url = urlparse(url)

private_key = RSA.import_key(settings.ACTIVITYPUB_PRIVATE_KEY)
payload_hash_raw = payload_hash(payload)
payload_hash_base64 = base64.b64encode(payload_hash_raw).decode()

to_be_signed_str = (
f"(request-target): post {url.path}\n"
f"host: {url.netloc}\n"
f"date: {date}\n"
f"digest: SHA-256={payload_hash_base64}"
)
sig_base64 = base64_signature(private_key, to_be_signed_str)

public_key_url = ap_url(reverse("activitypub:account")) + "#main-key"
signature_header = f'keyId="{public_key_url}",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="{sig_base64}"'
request_headers = {
"host": url.netloc,
"date": date,
"digest": f"SHA-256={payload_hash_base64}",
"content-type": "application/activity+json",
"signature": signature_header,
}
return request_headers


def signature_payload(payload, url):
"""Sign JSON-LD payload according to the 'Linked Data Signatures 1.0' RFC draft.
This brings compatibility with peertube (and mastodon for instance).
More information here:
- https://web.archive.org/web/20170717200644/https://w3c-dvcg.github.io/ld-signatures/
- https://docs.joinmastodon.org/spec/security/#ld
"""
private_key = RSA.import_key(settings.ACTIVITYPUB_PRIVATE_KEY)

signature = {
"type": "RsaSignature2017",
"creator": payload["actor"],
"created": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
}
options_payload = {
"@context": [
"https://w3id.org/security/v1",
{"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
],
"creator": signature["creator"],
"created": signature["created"],
}
options_normalized = payload_normalize(options_payload)
options_hash = payload_hash(options_normalized)

document_normalized = payload_normalize(payload)
document_hash = payload_hash(document_normalized)

to_sign = options_hash.hex() + document_hash.hex()
signature["signatureValue"] = base64_signature(private_key, to_sign)

return signature


def stable_uuid(seed, version=None):
"""Always returns the same UUID given the same input string."""

Expand Down

0 comments on commit 6ca8a3f

Please sign in to comment.