feat/verify http signature
This commit is contained in:
parent
2457a85564
commit
25bddf408f
3 changed files with 104 additions and 9 deletions
|
@ -46,3 +46,6 @@ ID = f"{_SCHEME}://{DOMAIN}"
|
||||||
BASE_URL = ID
|
BASE_URL = ID
|
||||||
USERNAME = CONFIG.username
|
USERNAME = CONFIG.username
|
||||||
KEY_PATH = (ROOT_DIR / "data" / "key.pem")
|
KEY_PATH = (ROOT_DIR / "data" / "key.pem")
|
||||||
|
|
||||||
|
USER_AGENT = "Fediverse Application/Foxhole-0.0.1"
|
||||||
|
AP_CONTENT_TYPE = "application/activity+json"
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import base64
|
import base64
|
||||||
from typing import Literal, TypedDict, cast, Any
|
from typing import Literal, TypedDict, cast, Any
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
from Crypto.Signature import PKCS1_v1_5
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
class HttpSignature:
|
class HttpSignature:
|
||||||
"""
|
"""
|
||||||
|
@ -11,9 +13,11 @@ class HttpSignature:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def calculation_digest(cls,
|
def calculation_digest(
|
||||||
|
cls,
|
||||||
body : bytes,
|
body : bytes,
|
||||||
algorithm="sha-256")-> str :
|
algorithm : str ="sha-256"
|
||||||
|
)-> str :
|
||||||
"""
|
"""
|
||||||
Calculates the digest header value for a given HTTP body
|
Calculates the digest header value for a given HTTP body
|
||||||
"""
|
"""
|
||||||
|
@ -23,4 +27,51 @@ class HttpSignature:
|
||||||
return "SHA-256=" + \
|
return "SHA-256=" + \
|
||||||
base64.b64encode(h.digest()).decode("utf-8")
|
base64.b64encode(h.digest()).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Not support algorithm {algorithm}")
|
raise ValueError(f"No support algorithm {algorithm}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify_signature(
|
||||||
|
cls,
|
||||||
|
signature_string : str,
|
||||||
|
signature : bytes,
|
||||||
|
pubkey,
|
||||||
|
) -> bool :
|
||||||
|
pubkey = RSA.importKey(pubkey)
|
||||||
|
signer = PKCS1_v1_5.new(pubkey)
|
||||||
|
digest = SHA256.new()
|
||||||
|
digest.update(signature_string.encode("utf-8"))
|
||||||
|
return signer.verify(digest, signature)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_signature(cls, signature):
|
||||||
|
_detail = {}
|
||||||
|
for item in signature.split(","):
|
||||||
|
name, value = item.split("=", 1)
|
||||||
|
value = value.strip('"')
|
||||||
|
_detail[name.lower()] = value
|
||||||
|
signature_details = {
|
||||||
|
"headers": _detail["headers"].split(),
|
||||||
|
"signature": base64.b64decode(_detail["signature"]),
|
||||||
|
"algorithm": _detail["algorithm"],
|
||||||
|
"keyid": _detail["keyid"],
|
||||||
|
}
|
||||||
|
return signature_details
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_signature_string(
|
||||||
|
cls,
|
||||||
|
method : str,
|
||||||
|
path : str,
|
||||||
|
signed_headers : dict,
|
||||||
|
body_digest : str,
|
||||||
|
headers,
|
||||||
|
) -> str :
|
||||||
|
signed_string = []
|
||||||
|
for signed_header in signed_headers:
|
||||||
|
if signed_header == "(request-target)":
|
||||||
|
signed_string.append("(request-target): " + method.lower() + " " + path)
|
||||||
|
elif signed_header == "digest" and body_digest:
|
||||||
|
signed_string.append("digest: " + body_digest)
|
||||||
|
else:
|
||||||
|
signed_string.append(signed_header + ": " + headers[signed_header])
|
||||||
|
return "\n".join(signed_string)
|
||||||
|
|
51
app/main.py
51
app/main.py
|
@ -1,6 +1,9 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import fastapi
|
import fastapi
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
@ -15,7 +18,7 @@ from loguru import logger
|
||||||
|
|
||||||
from app import models
|
from app import models
|
||||||
from app.database import get_db_session
|
from app.database import get_db_session
|
||||||
from app.config import DEBUG
|
from app.config import AP_CONTENT_TYPE, DEBUG, USER_AGENT
|
||||||
from app.activitypub import ME
|
from app.activitypub import ME
|
||||||
from app.config import BASE_URL
|
from app.config import BASE_URL
|
||||||
from app.config import DEBUG
|
from app.config import DEBUG
|
||||||
|
@ -24,6 +27,7 @@ from app.config import ID
|
||||||
from app.config import USERNAME
|
from app.config import USERNAME
|
||||||
from app.database import AsyncSession
|
from app.database import AsyncSession
|
||||||
from app.database import get_db_session
|
from app.database import get_db_session
|
||||||
|
from app.httpsig import HttpSignature
|
||||||
|
|
||||||
def _check_0rtt_early_data(request: Request) -> None:
|
def _check_0rtt_early_data(request: Request) -> None:
|
||||||
"""Disable TLS1.3 0-RTT requests for non-GET."""
|
"""Disable TLS1.3 0-RTT requests for non-GET."""
|
||||||
|
@ -47,8 +51,45 @@ async def inbox(
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
logger.info(f"headers={request.headers}")
|
logger.info(f"headers={request.headers}")
|
||||||
|
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
# logger.info(f"{payload=}")
|
logger.info(f"{payload=}")
|
||||||
|
|
||||||
|
parsec_signature = HttpSignature.parse_signature(
|
||||||
|
request.headers.get("signature"))
|
||||||
|
|
||||||
|
actor_url = payload["actor"]
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.get(
|
||||||
|
actor_url,
|
||||||
|
headers={
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
"Accept": AP_CONTENT_TYPE,
|
||||||
|
},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_actor = resp.json()
|
||||||
|
pubkey = _actor["publicKey"]["publicKeyPem"]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
body = await request.body()
|
||||||
|
signture_string = HttpSignature.build_signature_string(
|
||||||
|
request.method,
|
||||||
|
request.url.path,
|
||||||
|
parsec_signature["headers"],
|
||||||
|
HttpSignature.calculation_digest(body),
|
||||||
|
request.headers,
|
||||||
|
)
|
||||||
|
is_verify = HttpSignature.verify_signature(
|
||||||
|
signture_string,
|
||||||
|
parsec_signature["signature"],
|
||||||
|
pubkey)
|
||||||
|
logger.info(signture_string)
|
||||||
|
logger.info(f"verify? {is_verify}")
|
||||||
|
|
||||||
await new_incoming(db_session, payload)
|
await new_incoming(db_session, payload)
|
||||||
return Response(status_code=202)
|
return Response(status_code=202)
|
||||||
|
|
||||||
|
@ -70,9 +111,9 @@ async def new_incoming(
|
||||||
ap_id=ap_id,
|
ap_id=ap_id,
|
||||||
ap_object=payload,
|
ap_object=payload,
|
||||||
)
|
)
|
||||||
db_session.add(incoming_activity)
|
# db_session.add(incoming_activity)
|
||||||
await db_session.commit()
|
# await db_session.commit()
|
||||||
await db_session.refresh(incoming_activity)
|
# await db_session.refresh(incoming_activity)
|
||||||
return incoming_activity
|
return incoming_activity
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue