Modified authorize and verify workflows to support 2 message types

pull/932/head
kompotkot 2023-10-16 10:31:54 +00:00
rodzic 7fb0963d24
commit 74f956ff64
3 zmienionych plików z 227 dodań i 54 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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:

Wyświetl plik

@ -0,0 +1,4 @@
[mypy]
[mypy-eth_keys.*]
ignore_missing_imports = True