From 74f956ff649bce45635a8bf2178cba280dc93c88 Mon Sep 17 00:00:00 2001 From: kompotkot Date: Mon, 16 Oct 2023 10:31:54 +0000 Subject: [PATCH] Modified authorize and verify workflows to support 2 message types --- engineapi/engineapi/auth.py | 203 +++++++++++++++++++++++------- engineapi/engineapi/middleware.py | 74 ++++++++++- engineapi/mypy.ini | 4 + 3 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 engineapi/mypy.ini diff --git a/engineapi/engineapi/auth.py b/engineapi/engineapi/auth.py index 2b17b2d1..4c5cab5e 100644 --- a/engineapi/engineapi/auth.py +++ b/engineapi/engineapi/auth.py @@ -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 diff --git a/engineapi/engineapi/middleware.py b/engineapi/engineapi/middleware.py index 5986b417..b9d95a1b 100644 --- a/engineapi/engineapi/middleware.py +++ b/engineapi/engineapi/middleware.py @@ -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: diff --git a/engineapi/mypy.ini b/engineapi/mypy.ini new file mode 100644 index 00000000..c4a4c5c1 --- /dev/null +++ b/engineapi/mypy.ini @@ -0,0 +1,4 @@ +[mypy] + +[mypy-eth_keys.*] +ignore_missing_imports = True