From 664d7901b1f5cf829e46580306213e64b59c6c2e Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Wed, 18 Sep 2024 12:23:57 +0200 Subject: [PATCH] Problem: Solana wallet couln't be used to control the VM --- pyproject.toml | 3 +- .../vm/orchestrator/views/authentication.py | 22 +++++- tests/supervisor/test_authentication.py | 74 ++++++++++++++++++- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33457e45..c2854dbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,8 @@ dependencies = [ "aiohttp_cors~=0.7.0", "pyroute2==0.7.12", "jwcrypto==1.5.6", - "python-cpuid==0.1.0" + "python-cpuid==0.1.0", + "solathon==1.0.2", ] [project.urls] diff --git a/src/aleph/vm/orchestrator/views/authentication.py b/src/aleph/vm/orchestrator/views/authentication.py index 2d08246b..db51bd81 100644 --- a/src/aleph/vm/orchestrator/views/authentication.py +++ b/src/aleph/vm/orchestrator/views/authentication.py @@ -20,6 +20,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA +from nacl.exceptions import BadSignatureError from pydantic import BaseModel, ValidationError, root_validator, validator from aleph.vm.conf import settings @@ -55,6 +56,7 @@ class SignedPubKeyPayload(BaseModel): # alg: Literal["ECDSA"] address: str expires: str + chain: Literal["ETH"] | Literal["SOL"] = "ETH" @property def json_web_key(self) -> jwk.JWK: @@ -89,12 +91,23 @@ def check_expiry(cls, values) -> dict[str, bytes]: @root_validator(pre=False, skip_on_failure=True) def check_signature(cls, values) -> dict[str, bytes]: """Check that the signature is valid""" - signature: bytes = values["signature"] + signature: list = values["signature"] payload: bytes = values["payload"] content = SignedPubKeyPayload.parse_raw(payload) - if not verify_wallet_signature(signature, payload.hex(), content.address): - msg = "Invalid signature" - raise ValueError(msg) + if content.chain == "SOL": + from solathon.utils import verify_signature + + try: + verify_signature(content.address, signature, payload.hex()) + except BadSignatureError: + msg = "Invalid signature" + raise ValueError(msg) + elif content.chain == "ETH": + if not verify_wallet_signature(signature, payload.hex(), content.address): + msg = "Invalid signature" + raise ValueError(msg) + else: + raise ValueError("Unsupported chain") return values @property @@ -208,6 +221,7 @@ def verify_signed_operation(signed_operation: SignedOperation, signed_pubkey: Si async def authenticate_jwk(request: web.Request) -> str: """Authenticate a request using the X-SignedPubKey and X-SignedOperation headers.""" signed_pubkey = get_signed_pubkey(request) + signed_operation = get_signed_operation(request) if signed_operation.content.domain != settings.DOMAIN_NAME: logger.debug(f"Invalid domain '{signed_operation.content.domain}' != '{settings.DOMAIN_NAME}'") diff --git a/tests/supervisor/test_authentication.py b/tests/supervisor/test_authentication.py index 77ba154d..f28889c5 100644 --- a/tests/supervisor/test_authentication.py +++ b/tests/supervisor/test_authentication.py @@ -3,6 +3,7 @@ import eth_account.messages import pytest +import solathon from aiohttp import web from eth_account.datastructures import SignedMessage from jwcrypto import jwk, jws @@ -14,7 +15,6 @@ ) from aleph.vm.utils.test_helpers import ( generate_signer_and_signed_headers_for_operation, - patch_datetime_now, to_0x_hex, ) @@ -238,6 +238,78 @@ async def view(request, authenticated_sender): assert "ok" == r +async def generate_sol_signer_and_signed_headers_for_operation( + patch_datetime_now, operation_payload: dict +) -> tuple[solathon.Keypair, dict]: + """Generate a temporary eth_account for testing and sign the operation with it""" + + kp = solathon.Keypair() + key = jwk.JWK.generate( + kty="EC", + crv="P-256", + # key_ops=["verify"], + ) + + pubkey = { + "pubkey": json.loads(key.export_public()), + "alg": "ECDSA", + "domain": "localhost", + "address": str(kp.public_key), + "expires": (patch_datetime_now.FAKE_TIME + datetime.timedelta(days=1)).isoformat() + "Z", + "chain": "SOL", + } + pubkey_payload = json.dumps(pubkey).encode("utf-8").hex() + import nacl.signing + + signed_message: nacl.signing.SignedMessage = kp.sign(pubkey_payload) + pubkey_signature = to_0x_hex(signed_message.signature) + pubkey_signature_header = json.dumps( + { + "payload": pubkey_payload, + "signature": pubkey_signature, + } + ) + payload_as_bytes = json.dumps(operation_payload).encode("utf-8") + from jwcrypto.jwa import JWA + + payload_signature = JWA.signing_alg("ES256").sign(key, payload_as_bytes) + headers = { + "X-SignedPubKey": pubkey_signature_header, + "X-SignedOperation": json.dumps( + { + "payload": payload_as_bytes.hex(), + "signature": payload_signature.hex(), + } + ), + } + return kp, headers + + +@pytest.mark.asyncio +async def test_require_jwk_authentication_good_key_solana(aiohttp_client, patch_datetime_now): + """An HTTP request to a view decorated by `@require_jwk_authentication` + auth correctly a temporary key signed by a wallet and an operation signed by that key""" + + app = web.Application() + payload = {"time": "2010-12-25T17:05:55Z", "method": "GET", "path": "/", "domain": "localhost"} + + signer_account, headers = await generate_sol_signer_and_signed_headers_for_operation(patch_datetime_now, payload) + + @require_jwk_authentication + async def view(request, authenticated_sender): + assert authenticated_sender == str(signer_account.public_key) + return web.Response(text="ok") + + app.router.add_get("", view) + client = await aiohttp_client(app) + + resp = await client.get("/", headers=headers) + assert resp.status == 200, await resp.text() + + r = await resp.text() + assert "ok" == r + + @pytest.fixture def valid_jwk_headers(mocker): mocker.patch("aleph.vm.orchestrator.views.authentication.is_token_still_valid", lambda timestamp: True)