rra
4 years ago
1 changed files with 119 additions and 0 deletions
@ -0,0 +1,119 @@ |
|||
""" |
|||
|
|||
Mastodon instances won't accept requests that are not signed using this scheme. |
|||
|
|||
""" |
|||
import base64 |
|||
import hashlib |
|||
import logging |
|||
from datetime import datetime |
|||
from typing import Any |
|||
from typing import Dict |
|||
from typing import Optional |
|||
from urllib.parse import urlsplit |
|||
|
|||
|
|||
from Crypto.Hash import SHA256 |
|||
from Crypto.Signature import PKCS1_v1_5 |
|||
|
|||
from key import Key |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
def build_signing_string(headers, used_headers): |
|||
return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers)) |
|||
|
|||
|
|||
def _build_signed_string( |
|||
signed_headers: str, method: str, path: str, headers: Any, body_digest: str |
|||
) -> str: |
|||
out = [] |
|||
for signed_header in signed_headers.split(" "): |
|||
if signed_header == "(request-target)": |
|||
out.append("(request-target): " + method.lower() + " " + path) |
|||
elif signed_header == "digest": |
|||
out.append("digest: " + body_digest) |
|||
else: |
|||
out.append(signed_header + ": " + headers[signed_header]) |
|||
return "\n".join(out) |
|||
|
|||
|
|||
def _parse_sig_header(val: Optional[str]) -> Optional[Dict[str, str]]: |
|||
if not val: |
|||
return None |
|||
out = {} |
|||
for data in val.split(","): |
|||
k, v = data.split("=", 1) |
|||
out[k] = v[1: len(v) - 1] # noqa: black conflict |
|||
return out |
|||
|
|||
|
|||
def _verify_h(signed_string, signature, pubkey): |
|||
signer = PKCS1_v1_5.new(pubkey) |
|||
digest = SHA256.new() |
|||
digest.update(signed_string.encode("utf-8")) |
|||
return signer.verify(digest, signature) |
|||
|
|||
|
|||
def verify(hsig, request, actor): |
|||
k = Key(actor["id"]) |
|||
k.load_pub(actor["publicKey"]["publicKeyPem"]) |
|||
if k.key_id() != hsig["keyId"]: |
|||
return False |
|||
|
|||
signed_string = _build_signed_string( |
|||
hsig["headers"], request.method, request.path, |
|||
request.headers, _body_digest(request.body) |
|||
) |
|||
return _verify_h(signed_string, base64.b64decode(hsig["signature"]), k.pubkey) |
|||
|
|||
|
|||
def _body_digest(body: str) -> str: |
|||
h = hashlib.new("sha256") |
|||
h.update(body) # type: ignore |
|||
return "SHA-256=" + base64.b64encode(h.digest()).decode("utf-8") |
|||
|
|||
|
|||
class HTTPSigAuth: |
|||
"""Requests auth plugin for signing requests on the fly.""" |
|||
|
|||
def __init__(self, key, headers) -> None: |
|||
self.key = key |
|||
self.headers = headers |
|||
|
|||
def sign(self, url, body): |
|||
headers = self.headers.copy() |
|||
spl_url = urlsplit(url) |
|||
|
|||
bh = hashlib.new("sha256") |
|||
try: |
|||
body = body.encode("utf-8") |
|||
except AttributeError: |
|||
pass |
|||
bh.update(body) |
|||
|
|||
headers.update({ |
|||
'request-target': f'post {spl_url.path}', |
|||
"date": datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT"), |
|||
'host': spl_url.netloc, |
|||
'digest': "SHA-256=" + base64.b64encode(bh.digest()).decode("utf-8") |
|||
}) |
|||
|
|||
sigheaders = headers.keys() |
|||
sigstring = build_signing_string(headers, sigheaders) |
|||
|
|||
signer = PKCS1_v1_5.new(self.key['privkey']) |
|||
digest = SHA256.new() |
|||
digest.update(sigstring.encode("ascii")) |
|||
sigdata = base64.b64encode(signer.sign(digest)) |
|||
|
|||
sig = { |
|||
'keyId': self.key['keyId'], |
|||
'algorithm': 'rsa-sha256', |
|||
'headers': ' '.join(sigheaders), |
|||
'signature': sigdata.decode('ascii') |
|||
} |
|||
headers["signature"] = ','.join(['{}="{}"'.format(k, v) for k, v in sig.items()]) |
|||
|
|||
return headers |
Loading…
Reference in new issue