moviewyrm/bookwyrm/signatures.py

126 lines
4.2 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" signs activitypub activities """
import hashlib
from urllib.parse import urlparse
import datetime
from base64 import b64encode, b64decode
from Crypto import Random
from Crypto.PublicKey import RSA
2021-03-08 16:49:10 +00:00
from Crypto.Signature import pkcs1_15 # pylint: disable=no-name-in-module
from Crypto.Hash import SHA256
MAX_SIGNATURE_AGE = 300
2021-03-08 16:49:10 +00:00
def create_key_pair():
2021-04-26 16:15:42 +00:00
"""a new public/private key pair, used for creating new users"""
random_generator = Random.new().read
key = RSA.generate(1024, random_generator)
2021-03-08 16:49:10 +00:00
private_key = key.export_key().decode("utf8")
public_key = key.publickey().export_key().decode("utf8")
return private_key, public_key
def make_signature(sender, destination, date, digest):
2021-04-26 16:15:42 +00:00
"""uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination)
signature_headers = [
2021-09-18 04:39:18 +00:00
f"(request-target): post {inbox_parts.path}",
f"host: {inbox_parts.netloc}",
f"date: {date}",
f"digest: {digest}",
]
2021-03-08 16:49:10 +00:00
message_to_sign = "\n".join(signature_headers)
2020-12-01 03:53:42 +00:00
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
2021-03-08 16:49:10 +00:00
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
signature = {
2021-09-18 04:39:18 +00:00
"keyId": f"{sender.remote_id}#main-key",
2021-03-08 16:49:10 +00:00
"algorithm": "rsa-sha256",
"headers": "(request-target) host date digest",
"signature": b64encode(signed_message).decode("utf8"),
}
2021-09-18 04:39:18 +00:00
return ",".join(f'{k}="{v}"' for (k, v) in signature.items())
2020-09-21 17:25:26 +00:00
def make_digest(data):
2021-04-26 16:15:42 +00:00
"""creates a message digest for signing"""
2021-03-08 16:49:10 +00:00
return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode(
"utf-8"
)
2020-09-21 17:25:26 +00:00
def verify_digest(request):
2021-04-26 16:15:42 +00:00
"""checks if a digest is syntactically valid and matches the message"""
2021-03-08 16:49:10 +00:00
algorithm, digest = request.headers["digest"].split("=", 1)
if algorithm == "SHA-256":
hash_function = hashlib.sha256
2021-03-08 16:49:10 +00:00
elif algorithm == "SHA-512":
hash_function = hashlib.sha512
else:
2021-09-18 04:39:18 +00:00
raise ValueError(f"Unsupported hash function: {algorithm}")
expected = hash_function(request.body).digest()
if b64decode(digest) != expected:
raise ValueError("Invalid HTTP Digest header")
2021-03-08 16:49:10 +00:00
class Signature:
2021-04-26 16:15:42 +00:00
"""read and validate incoming signatures"""
2021-03-08 16:49:10 +00:00
def __init__(self, key_id, headers, signature):
self.key_id = key_id
self.headers = headers
self.signature = signature
2021-06-18 21:12:56 +00:00
# pylint: disable=invalid-name
@classmethod
def parse(cls, request):
2021-04-26 16:15:42 +00:00
"""extract and parse a signature from an http request"""
signature_dict = {}
2021-03-08 16:49:10 +00:00
for pair in request.headers["Signature"].split(","):
k, v = pair.split("=", 1)
v = v.replace('"', "")
signature_dict[k] = v
try:
2021-03-08 16:49:10 +00:00
key_id = signature_dict["keyId"]
headers = signature_dict["headers"]
signature = b64decode(signature_dict["signature"])
except KeyError:
2021-03-08 16:49:10 +00:00
raise ValueError("Invalid auth header")
return cls(key_id, headers, signature)
def verify(self, public_key, request):
2021-04-26 16:15:42 +00:00
"""verify rsa signature"""
2021-03-08 16:49:10 +00:00
if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE:
2021-09-18 04:39:18 +00:00
raise ValueError(f"Request too old: {request.headers['date']}")
public_key = RSA.import_key(public_key)
comparison_string = []
2021-03-08 16:49:10 +00:00
for signed_header_name in self.headers.split(" "):
if signed_header_name == "(request-target)":
2021-09-18 04:39:18 +00:00
comparison_string.append(f"(request-target): post {request.path}")
else:
2021-03-08 16:49:10 +00:00
if signed_header_name == "digest":
verify_digest(request)
2021-03-08 16:49:10 +00:00
comparison_string.append(
2021-09-18 04:39:18 +00:00
f"{signed_header_name}: {request.headers[signed_header_name]}"
2021-03-08 16:49:10 +00:00
)
comparison_string = "\n".join(comparison_string)
signer = pkcs1_15.new(public_key)
digest = SHA256.new()
digest.update(comparison_string.encode())
# raises a ValueError if it fails
signer.verify(digest, self.signature)
2020-09-21 17:25:26 +00:00
def http_date_age(datestr):
2021-04-26 16:15:42 +00:00
"""age of a signature in seconds"""
2021-03-08 16:49:10 +00:00
parsed = datetime.datetime.strptime(datestr, "%a, %d %b %Y %H:%M:%S GMT")
delta = datetime.datetime.utcnow() - parsed
return delta.total_seconds()