kopia lustrzana https://github.com/bugout-dev/moonstream
Modified authorize and verify workflows to support 2 message types
rodzic
7fb0963d24
commit
74f956ff64
|
@ -19,23 +19,17 @@ import argparse
|
|||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Any, cast, Dict
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import eth_keys
|
||||
from eip712.messages import EIP712Message, _hash_eip191_message
|
||||
from eth_account import Account
|
||||
from eth_account._utils.signing import sign_message_hash
|
||||
import eth_keys
|
||||
from eth_typing import ChecksumAddress
|
||||
from hexbytes import HexBytes
|
||||
from web3 import Web3
|
||||
|
||||
|
||||
AUTH_PAYLOAD_NAME = "MoonstreamAuthorization"
|
||||
AUTH_VERSION = "1"
|
||||
|
||||
# By default, authorizations will remain active for 24 hours.
|
||||
DEFAULT_INTERVAL = 60 * 60 * 24
|
||||
|
||||
|
||||
class MoonstreamAuthorizationVerificationError(Exception):
|
||||
"""
|
||||
Raised when invalid signer is provided.
|
||||
|
@ -48,12 +42,48 @@ class MoonstreamAuthorizationExpired(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class MoonstreamAuthorization(EIP712Message):
|
||||
_name_: "string"
|
||||
_version_: "string"
|
||||
class MoonstreamAuthorizationStructureError(Exception):
|
||||
"""
|
||||
Raised when signature has incorrect structure.
|
||||
"""
|
||||
|
||||
address: "address"
|
||||
deadline: "uint256"
|
||||
|
||||
class MoonstreamAuthorization(EIP712Message):
|
||||
_name_: "string" # type: ignore
|
||||
_version_: "string" # type: ignore
|
||||
|
||||
address: "address" # type: ignore
|
||||
deadline: "uint256" # type: ignore
|
||||
|
||||
|
||||
class MetaTXAuthorization(EIP712Message):
|
||||
_name_: "string" # type: ignore
|
||||
_version_: "string" # type: ignore
|
||||
|
||||
caller: "address" # type: ignore
|
||||
expires_at: "uint256" # type: ignore
|
||||
|
||||
|
||||
EIP712_AUTHORIZATION_TYPES = {
|
||||
"MoonstreamAuthorization": {
|
||||
"name": "MoonstreamAuthorization",
|
||||
"version": "1",
|
||||
"eip712_message_class": MoonstreamAuthorization,
|
||||
"primary_types": [
|
||||
{"name": "address", "type": "address"},
|
||||
{"name": "deadline", "type": int},
|
||||
],
|
||||
},
|
||||
"MetaTXAuthorization": {
|
||||
"name": "MetaTXAuthorization",
|
||||
"version": "1",
|
||||
"eip712_message_class": MetaTXAuthorization,
|
||||
"primary_types": [
|
||||
{"name": "caller", "type": "address"},
|
||||
{"name": "expires_at", "type": int},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexBytes:
|
||||
|
@ -64,52 +94,77 @@ def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexByte
|
|||
return signed_message_bytes
|
||||
|
||||
|
||||
def authorize(deadline: int, address: str, private_key: HexBytes) -> Dict[str, Any]:
|
||||
message = MoonstreamAuthorization(
|
||||
_name_=AUTH_PAYLOAD_NAME,
|
||||
_version_=AUTH_VERSION,
|
||||
address=address,
|
||||
deadline=deadline,
|
||||
)
|
||||
def authorize(
|
||||
authorization_type: Dict[str, Any],
|
||||
primary_types: Dict[str, Any],
|
||||
private_key: HexBytes,
|
||||
signature_name_output: str,
|
||||
) -> Dict[str, Any]:
|
||||
# Initializing instance of EIP712Message class
|
||||
attrs: Dict[str, Any] = {
|
||||
"_name_": authorization_type["name"],
|
||||
"_version_": authorization_type["version"],
|
||||
}
|
||||
attrs.update(primary_types)
|
||||
message = authorization_type["eip712_message_class"](**attrs)
|
||||
|
||||
# Generating message hash and signature
|
||||
msg_hash_bytes = HexBytes(_hash_eip191_message(message.signable_message))
|
||||
|
||||
signed_message = sign_message(msg_hash_bytes, private_key)
|
||||
|
||||
api_payload: Dict[str, Any] = {
|
||||
"address": address,
|
||||
"deadline": deadline,
|
||||
"signed_message": signed_message.hex(),
|
||||
}
|
||||
api_payload: Dict[str, Any] = {signature_name_output: signed_message.hex()}
|
||||
api_payload.update(primary_types)
|
||||
|
||||
return api_payload
|
||||
|
||||
|
||||
def verify(authorization_payload: Dict[str, Any]) -> bool:
|
||||
def verify(
|
||||
authorization_type: Dict[str, Any],
|
||||
authorization_payload: Dict[str, Any],
|
||||
signature_name_input: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Verifies provided signature signer by correct address.
|
||||
|
||||
**Important** Assume that not address field is timefield (live_at, expires_at, deadline, etc)
|
||||
"""
|
||||
# Initializing instance of EIP712Message class
|
||||
attrs: Dict[str, Any] = {
|
||||
"_name_": authorization_type["name"],
|
||||
"_version_": authorization_type["version"],
|
||||
}
|
||||
|
||||
time_now = int(time.time())
|
||||
web3_client = Web3()
|
||||
address = Web3.toChecksumAddress(cast(str, authorization_payload["address"]))
|
||||
deadline = cast(int, authorization_payload["deadline"])
|
||||
signature = cast(str, authorization_payload["signed_message"])
|
||||
|
||||
message = MoonstreamAuthorization(
|
||||
_name_=AUTH_PAYLOAD_NAME,
|
||||
_version_=AUTH_VERSION,
|
||||
address=address,
|
||||
deadline=deadline,
|
||||
)
|
||||
address: Optional[ChecksumAddress] = None
|
||||
time_field: Optional[int] = None
|
||||
|
||||
for pt in authorization_type["primary_types"]:
|
||||
pt_name = pt["name"]
|
||||
pt_type = pt["type"]
|
||||
if pt_type == "address":
|
||||
address = Web3.toChecksumAddress(cast(str, authorization_payload[pt_name]))
|
||||
attrs[pt_name] = address
|
||||
else:
|
||||
time_field = cast(pt_type, authorization_payload[pt_name])
|
||||
attrs[pt_name] = time_field
|
||||
if address is None or time_field is None:
|
||||
raise MoonstreamAuthorizationStructureError(
|
||||
"Field address or time_field could not be None"
|
||||
)
|
||||
|
||||
message = authorization_type["eip712_message_class"](**attrs)
|
||||
signature = cast(str, authorization_payload[signature_name_input])
|
||||
signer_address = web3_client.eth.account.recover_message(
|
||||
message.signable_message, signature=signature
|
||||
)
|
||||
if signer_address != address:
|
||||
raise MoonstreamAuthorizationVerificationError("Invalid signer")
|
||||
|
||||
if deadline < time_now:
|
||||
raise MoonstreamAuthorizationExpired("Deadline exceeded")
|
||||
if time_field < time_now:
|
||||
raise MoonstreamAuthorizationExpired("Time field exceeded")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -121,15 +176,37 @@ def decrypt_keystore(keystore_path: str, password: str) -> HexBytes:
|
|||
|
||||
|
||||
def handle_authorize(args: argparse.Namespace) -> None:
|
||||
address, private_key = decrypt_keystore(args.signer, args.password)
|
||||
authorization = authorize(args.deadline, address, private_key)
|
||||
if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
|
||||
raise Exception("Provided unsupported EIP712 Authorization type")
|
||||
|
||||
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
|
||||
primary_types = json.loads(args.primary_types)
|
||||
for ptk in authorization_type["primary_types"]:
|
||||
if ptk["name"] not in primary_types:
|
||||
raise Exception(f"Lost primary type: {ptk}")
|
||||
|
||||
_, private_key = decrypt_keystore(args.signer, args.password)
|
||||
authorization = authorize(
|
||||
authorization_type=authorization_type,
|
||||
primary_types=primary_types,
|
||||
private_key=private_key,
|
||||
signature_name_output=args.signature_name_output,
|
||||
)
|
||||
print(json.dumps(authorization))
|
||||
|
||||
|
||||
def handle_verify(args: argparse.Namespace) -> None:
|
||||
if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
|
||||
raise Exception("Provided unsupported EIP712 Authorization type")
|
||||
|
||||
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
|
||||
payload_json = base64.decodebytes(args.payload).decode("utf-8")
|
||||
payload = json.loads(payload_json)
|
||||
verify(payload)
|
||||
verify(
|
||||
authorization_type=authorization_type,
|
||||
authorization_payload=payload,
|
||||
signature_name_input=args.signature_name_input,
|
||||
)
|
||||
print("Verified!")
|
||||
|
||||
|
||||
|
@ -140,13 +217,6 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
subcommands = parser.add_subparsers()
|
||||
|
||||
authorize_parser = subcommands.add_parser("authorize")
|
||||
authorize_parser.add_argument(
|
||||
"-t",
|
||||
"--deadline",
|
||||
type=int,
|
||||
default=int(time.time()) + DEFAULT_INTERVAL,
|
||||
help="Authorization deadline (seconds since epoch timestamp).",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"-s",
|
||||
"--signer",
|
||||
|
@ -159,6 +229,30 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
required=False,
|
||||
help="(Optional) password for signing account. If you don't provide it here, you will be prompte for it.",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"-t",
|
||||
"--authorization-type",
|
||||
required=True,
|
||||
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
|
||||
help="One of supported EIP712 Message authorization types",
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"--primary-types",
|
||||
required=True,
|
||||
help="Primary types for specified EIP712 Message authorization in JSON format {0}. Available keys: {1}".format(
|
||||
{"name_1": "value", "name_2": "value"},
|
||||
[
|
||||
f"{v['primary_types']} for {k}"
|
||||
for k, v in EIP712_AUTHORIZATION_TYPES.items()
|
||||
],
|
||||
),
|
||||
)
|
||||
authorize_parser.add_argument(
|
||||
"--signature-name-output",
|
||||
type=str,
|
||||
default="signed_message",
|
||||
help="Key in output dictionary of signature",
|
||||
)
|
||||
authorize_parser.set_defaults(func=handle_authorize)
|
||||
|
||||
verify_parser = subcommands.add_parser("verify")
|
||||
|
@ -168,6 +262,19 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
required=True,
|
||||
help="Base64-encoded payload to verify",
|
||||
)
|
||||
verify_parser.add_argument(
|
||||
"-t",
|
||||
"--authorization-type",
|
||||
required=True,
|
||||
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
|
||||
help="One of supported EIP712 Message authorization types",
|
||||
)
|
||||
verify_parser.add_argument(
|
||||
"--signature-name-input",
|
||||
type=str,
|
||||
default="signed_message",
|
||||
help="Key for signature in payload",
|
||||
)
|
||||
verify_parser.set_defaults(func=handle_verify)
|
||||
|
||||
return parser
|
||||
|
|
|
@ -6,18 +6,23 @@ from uuid import UUID
|
|||
|
||||
from bugout.data import BugoutResource, BugoutResources, BugoutUser
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from eip712.messages import EIP712Message, _hash_eip191_message
|
||||
from eth_account.messages import encode_defunct
|
||||
from fastapi import Header, HTTPException, Request, Response
|
||||
from hexbytes import HexBytes
|
||||
from pydantic import AnyHttpUrl, parse_obj_as
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import Response
|
||||
from starlette.types import ASGIApp
|
||||
from web3 import Web3
|
||||
from web3.auto import w3 as w3_auto
|
||||
|
||||
from . import data
|
||||
from .auth import (
|
||||
EIP712_AUTHORIZATION_TYPES,
|
||||
MoonstreamAuthorizationExpired,
|
||||
MoonstreamAuthorizationStructureError,
|
||||
MoonstreamAuthorizationVerificationError,
|
||||
verify,
|
||||
)
|
||||
|
@ -93,7 +98,7 @@ async def user_for_auth_header(
|
|||
status_code=403, detail="Wrong authorization header"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if user_token != "":
|
||||
|
@ -118,6 +123,57 @@ async def user_for_auth_header(
|
|||
return user
|
||||
|
||||
|
||||
async def metatx_sign_header(
|
||||
authorization: str = Header(None),
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
message: Optional[Dict[str, Any]] = None
|
||||
if authorization is not None:
|
||||
try:
|
||||
auth_format, user_token = parse_auth_header(auth_header=authorization)
|
||||
except InvalidAuthHeaderFormat:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="Wrong authorization header"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if auth_format != "metatx":
|
||||
raise EngineHTTPException(
|
||||
status_code=403,
|
||||
detail=f"Wrong authorization header format: {auth_format}",
|
||||
)
|
||||
|
||||
try:
|
||||
json_payload_str = base64.b64decode(user_token).decode("utf-8")
|
||||
payload = json.loads(json_payload_str)
|
||||
verify(
|
||||
authorization_type=EIP712_AUTHORIZATION_TYPES["MetaTXAuthorization"],
|
||||
authorization_payload=payload,
|
||||
signature_name_input="signature",
|
||||
)
|
||||
message = {
|
||||
"caller": Web3.toChecksumAddress(payload.get("caller")),
|
||||
"expires_at": payload.get("expires_at"),
|
||||
}
|
||||
except MoonstreamAuthorizationVerificationError as e:
|
||||
logger.info("MetaTX authorization verification error: %s", e)
|
||||
raise EngineHTTPException(status_code=403, detail="Invalid signer")
|
||||
except MoonstreamAuthorizationExpired as e:
|
||||
logger.info("MetaTX authorization expired: %s", e)
|
||||
raise EngineHTTPException(status_code=403, detail="Authorization expired")
|
||||
except MoonstreamAuthorizationStructureError as e:
|
||||
logger.info("MetaTX authorization incorrect structure error: %s", e)
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="Incorrect signature structure"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Unexpected exception: %s", e)
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class BroodAuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Checks the authorization header on the request. If it represents a verified Brood user,
|
||||
|
@ -155,7 +211,7 @@ class BroodAuthMiddleware(BaseHTTPMiddleware):
|
|||
except InvalidAuthHeaderFormat:
|
||||
return Response(status_code=403, content="Wrong authorization header")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Brood response: {str(e)}")
|
||||
logger.error(f"Error parsing auth header: {str(e)}")
|
||||
return Response(status_code=500, content="Internal server error")
|
||||
|
||||
try:
|
||||
|
@ -226,9 +282,15 @@ class EngineAuthMiddleware(BaseHTTPMiddleware):
|
|||
authorization_header_components[-1]
|
||||
).decode("utf-8")
|
||||
|
||||
json_payload = json.loads(json_payload_str)
|
||||
verified = verify(json_payload)
|
||||
address = json_payload.get("address")
|
||||
payload = json.loads(json_payload_str)
|
||||
verified = verify(
|
||||
authorization_type=EIP712_AUTHORIZATION_TYPES[
|
||||
"MoonstreamAuthorization"
|
||||
],
|
||||
authorization_payload=payload,
|
||||
signature_name_input="signed_message",
|
||||
)
|
||||
address = payload.get("address")
|
||||
if address is not None:
|
||||
address = Web3.toChecksumAddress(address)
|
||||
else:
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[mypy]
|
||||
|
||||
[mypy-eth_keys.*]
|
||||
ignore_missing_imports = True
|
Ładowanie…
Reference in New Issue