Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem: Solana wallet couln't be used to control the VM #700

Merged
merged 6 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 59 additions & 49 deletions doc/operator_auth.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,59 @@
Authentication protocol for VM owner
=======================================

This custom protocol allows a user (owner of a VM) to securely authenticate to a CRN, using their Ethereum wallet.
This scheme was designed in a way that's convenient to be integrated in the console web page.
This custom protocol allows a user (owner of a VM) to securely authenticate to a CRN, using their Ethereum or Solana
wallet. This scheme was designed in a way that's convenient to be integrated into the console web page.

It allows the user to control their VM. e.g : stop reboot, view their log, etc
It allows the user to control their VM. e.g: stop, reboot, view their log, etc.

## Motivations

This protocol ensures secure authentication between a blockchain wallet owner and an aleph.im compute node.

Signing operations is typically gated by prompts requiring manual approval for each operation.
With hardware wallets, users are prompted both by the software on their device and the hardware wallet itself.
Signing operations are typically gated by prompts requiring manual approval for each operation. With hardware wallets,
users are prompted both by the software on their device and the hardware wallet itself.

## Overview

The client generates a [JSON Web Key](https://www.rfc-editor.org/rfc/rfc7517) (JWK) key pair and signs the public key with their Ethereum account. The signed public key is sent
in the `X-SignedPubKey` header. The client also signs the operation payload with the private JWK, sending it in the
`X-SignedOperation` header. The server verifies both the public key and payload signatures, ensuring the request's
integrity and authenticity. If validation fails (e.g., expired key or invalid signature), the server returns a 401
Unauthorized error.
The client generates a [JSON Web Key](https://www.rfc-editor.org/rfc/rfc7517) (JWK) key pair and signs the public key
with their Ethereum or Solana account. The signed public key is sent in the `X-SignedPubKey` header. The client also
signs the operation payload with the private JWK, sending it in the `X-SignedOperation` header. The server verifies both
the public key and payload signatures, ensuring the request's integrity and authenticity. If validation fails (e.g.,
expired key or invalid signature), the server returns a 401 Unauthorized error.

Support for Solana wallets is planned in the near future.

## Authentication Method for HTTP Endpoints

Two custom headers are added to each authenticated request:

* X-SignedPubKey: This contains the public key and its associated metadata (such as the sender’s address and expiration
date), along with a signature that ensures its authenticity.
* X-SignedOperation: This includes the payload of the operation and its cryptographic signature, ensuring that the
- **X-SignedPubKey**: This contains the public key and its associated metadata (such as the sender’s address, chain, and
expiration date), along with a signature that ensures its authenticity.
- **X-SignedOperation**: This includes the payload of the operation and its cryptographic signature, ensuring that the
operation itself has not been tampered with.

### 1. Generate and Sign Public Key
### 1. Generate an ephemeral keys and Sign Public Key

A new JWK is generated using elliptic curve cryptography (EC, P-256).
An ephemeral key pair (as JWK) is generated using elliptic curve cryptography (EC, P-256).

The use of a temporary JWK key allows the user to delegate limited control to the console without needing to sign every
individual request with their Ethereum wallet. This is crucial for improving the user experience, as constantly signing
each operation would be cumbersome and inefficient. By generating a temporary key, the user can provide permission for a
set period of time (until the key expires), enabling the console to perform actions like stopping or rebooting the VM on
their behalf. This maintains security while streamlining interactions with the console, as the server verifies each
operation using the temporary key without requiring ongoing involvement from the user's wallet.
individual request with their Ethereum or Solana wallet. This is crucial for improving the user experience, as
constantly signing each operation would be cumbersome and inefficient. By generating a temporary key, the user can
provide permission for a set period of time (until the key expires), enabling the console to perform actions like
stopping or rebooting the VM on their behalf. This maintains security while streamlining interactions with the console,
as the server verifies each operation using the temporary key without requiring ongoing involvement from the user's
wallet.

The generated public key is converted into a JSON structure with additional metadata:
* `pubkey`: The public key information.
* `alg`: The signing algorithm, ECDSA.
* `domain`: The domain for which the key is valid.
* `address`: The Ethereum address of the sender, binding the public key to this identity.
* `expires`: The expiration time of the key.

Example
- **`pubkey`**: The public key information.
- **`alg`**: The signing algorithm, ECDSA.
- **`domain`**: The domain for which the key is valid.
- **`address`**: The wallet address of the sender, binding the temporary key to this identity.
- **`chain`**: Indicates the blockchain used for signing (`ETH` or `SOL`). Defaults to `ETH`.
- **`expires`**: The expiration time of the key.

Example:

```json
{
"pubkey": {
Expand All @@ -62,12 +65,13 @@ Example
"alg": "ECDSA",
"domain": "localhost",
"address": "0x8Dd070629F107e7946dD68BDcb8ABE8475F47B0E",
"chain": "ETH",
"expires": "2010-12-26T17:05:55Z"
}
```

This public key is signed using the Ethereum account to ensure its authenticity. The resulting signature is
combined with the public key into a payload and sent as the `X-SignedPubKey` header.
This public key is signed using either the Ethereum or Solana account, depending on the `chain` parameter. The resulting
signature is combined with the public key into a payload and sent as the `X-SignedPubKey` header.

### 2. Sign Operation Payload

Expand All @@ -83,7 +87,7 @@ integrity can be verified through signing. Below are the fields included:
- **`domain`**: (string) The domain associated with the request. This ensures the request is valid for the intended
CRN. (e.g., `localhost`).

Example
Example:

```json
{
Expand All @@ -97,55 +101,61 @@ Example
It is sent serialized as a hex string.

#### Signature
This payload is serialized in JSON, signed, and sent in the `X-SignedOperation` header to ensure the integrity and authenticity
of the request.

* The operation payload (containing details such as time, method, path, and domain) is serialized and converted into a byte array.
* The JWK (private key) is used to sign this operation payload, ensuring its integrity. This signature is then included in the X-SignedOperation header.

- The operation payload (containing details such as time, method, path, and domain) is JSON serialized and converted into a
hex string.
- The ephemeral key (private key) is used to sign this operation payload, ensuring its integrity. This signature is then included
in the `X-SignedOperation` header.

### 3. Include Authentication Headers

### 3. Include authentication Headers
These two headers are to be added to the HTTP Request:
These two headers are to be added to the HTTP request:

1. **`X-SignedPubKey` Header:**
- This header contains the public key payload and the signature of the public key generated by the Ethereum account.
1. **`X-SignedPubKey` Header**:
- This header contains the public key payload and the signature of the public key generated by the Ethereum or
Solana account.

Example:

```json
{
"payload": "<hexadecimal string of the public key payload>",
"signature": "<Ethereum signed public key>"
"payload": "<hexadecimal string of the public key payload>",
"signature": "<Ethereum or Solana signed public key>"
}
```

2. **`X-SignedOperation` Header:**
2. **`X-SignedOperation` Header**:
- This header contains the operation payload and the signature of the operation payload generated using the private
JWK.

Example:

```json
{
"payload": "<hexadecimal string of the operation payload>",
"signature": "<JWK signed operation payload>"
"payload": "<hexadecimal string of the operation payload>",
"signature": "<JWK signed operation payload>"
}
```

### Expiration and Validation

- The public key has an expiration date, ensuring that keys are not used indefinitely.
- Both the public key and the operation signature are validated for authenticity and integrity at the server side.
- Both the public key and the operation signature are validated for authenticity and integrity at the server side,
taking into account the specified blockchain (Ethereum or Solana).
- Requests failing verification or expired keys are rejected with `401 Unauthorized` status, providing an error message
indicating the reason.

### WebSocket Authentication Protocol
## WebSocket Authentication Protocol

In the WebSocket variant of the authentication protocol, the client establishes a connection and authenticates through
an initial message that includes their Ethereum-signed identity, ensuring secure communication.
an initial message that includes their Ethereum or Solana-signed identity, ensuring secure communication.

Due to web browsers not allowing custom HTTP headers in WebSocket connections, the two headers are sent in one JSON
packet, under the `auth` key.

Due to web browsers not allowing custom HTTP headers in WebSocket connections,
the two header are sent in one json packet, under the `auth` key.
Example authentication packet:

Example authentication packet
```json
{
"auth": {
Expand Down
2 changes: 1 addition & 1 deletion packaging/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ debian-package-code:
cp ../examples/instance_message_from_aleph.json ./aleph-vm/opt/aleph-vm/examples/instance_message_from_aleph.json
cp -r ../examples/data ./aleph-vm/opt/aleph-vm/examples/data
mkdir -p ./aleph-vm/opt/aleph-vm/examples/volumes
pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.9' 'eth-account==0.10' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'aleph-superfluid~=0.2.1' 'sqlalchemy[asyncio]>=2.0' 'aiosqlite==0.19.0' 'alembic==1.13.1' 'aiohttp_cors==0.7.0' 'pyroute2==0.7.12' 'python-cpuid==0.1.0'
pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.9' 'eth-account==0.10' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'aleph-superfluid~=0.2.1' 'sqlalchemy[asyncio]>=2.0' 'aiosqlite==0.19.0' 'alembic==1.13.1' 'aiohttp_cors==0.7.0' 'pyroute2==0.7.12' 'python-cpuid==0.1.0' 'solathon==1.0.2'
python3 -m compileall ./aleph-vm/opt/aleph-vm/

debian-package-resources: firecracker-bins vmlinux download-ipfs-kubo target/bin/sevctl
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ classifiers = [
"Topic :: System :: Distributed Computing",
]
dynamic = [ "version" ]

# Upon adding or updating dependencies, update `packaging/Makefile` for the Debian package
dependencies = [
"aiodns==3.1",
"aiohttp==3.9.5",
Expand All @@ -53,6 +55,7 @@ dependencies = [
"schedule==1.2.1",
"sentry-sdk==1.31",
"setproctitle==1.3.3",
"solathon==1.0.2",
"sqlalchemy[asyncio]>=2",
"systemd-python==235",
]
Expand Down
57 changes: 50 additions & 7 deletions src/aleph/vm/orchestrator/views/authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Functions for authentications

See /doc/operator_auth.md for the explaination of how the operator authentication works.
See /doc/operator_auth.md for the explanation of how the operator authentication works.

Can be enabled on an endpoint using the @require_jwk_authentication decorator
"""
Expand All @@ -16,11 +16,14 @@
import cryptography.exceptions
import pydantic
from aiohttp import web
from aleph_message.models import Chain
from eth_account import Account
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 solathon.utils import verify_signature

from aleph.vm.conf import settings

Expand All @@ -37,7 +40,7 @@
return expiry_datetime > current_datetime


def verify_wallet_signature(signature, message, address):
def verify_eth_wallet_signature(signature, message, address):
"""
Verifies a signature issued by a wallet
"""
Expand All @@ -46,6 +49,21 @@
return computed_address.lower() == address.lower()


def check_wallet_signature_or_raise(address, chain, payload, signature):
if chain == Chain.SOL:
try:
verify_signature(address, signature, payload.hex())
except BadSignatureError:
msg = "Invalid signature"
raise ValueError(msg)

Check warning on line 58 in src/aleph/vm/orchestrator/views/authentication.py

View check run for this annotation

Codecov / codecov/patch

src/aleph/vm/orchestrator/views/authentication.py#L56-L58

Added lines #L56 - L58 were not covered by tests
elif chain == "ETH":
if not verify_eth_wallet_signature(signature, payload.hex(), address):
msg = "Invalid signature"
raise ValueError(msg)
else:
raise ValueError("Unsupported chain")

Check warning on line 64 in src/aleph/vm/orchestrator/views/authentication.py

View check run for this annotation

Codecov / codecov/patch

src/aleph/vm/orchestrator/views/authentication.py#L64

Added line #L64 was not covered by tests


class SignedPubKeyPayload(BaseModel):
"""This payload is signed by the wallet of the user to authorize an ephemeral key to act on his behalf."""

Expand All @@ -55,6 +73,12 @@
# alg: Literal["ECDSA"]
address: str
expires: str
chain: Chain = Chain.ETH

def check_chain(self, v: Chain):
if v not in (Chain.ETH, Chain.SOL):
raise ValueError("Chain not supported")
return v

Check warning on line 81 in src/aleph/vm/orchestrator/views/authentication.py

View check run for this annotation

Codecov / codecov/patch

src/aleph/vm/orchestrator/views/authentication.py#L80-L81

Added lines #L80 - L81 were not covered by tests

@property
def json_web_key(self) -> jwk.JWK:
Expand Down Expand Up @@ -89,12 +113,10 @@
@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)
check_wallet_signature_or_raise(content.address, content.chain, payload, signature)
return values

@property
Expand Down Expand Up @@ -208,6 +230,7 @@
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}'")
Expand Down Expand Up @@ -236,6 +259,26 @@
def require_jwk_authentication(
handler: Callable[[web.Request, str], Coroutine[Any, Any, web.StreamResponse]]
) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
"""A decorator to enforce JWK-based authentication for HTTP requests.

The decorator ensures that the incoming request includes valid authentication headers
(as per the VM owner authentication protocol) and provides the authenticated wallet address (`authenticated_sender`)
to the handler. The handler can then use this address to verify access to the requested resource.

Args:
handler (Callable[[web.Request, str], Coroutine[Any, Any, web.StreamResponse]]):
The request handler function that will receive the `authenticated_sender` (the authenticated wallet address)
as an additional argument.

Returns:
Callable[[web.Request], Awaitable[web.StreamResponse]]:
A wrapped handler that verifies the authentication and passes the wallet address to the handler.

Note:
Refer to the "Authentication protocol for VM owner" documentation for detailed information on the authentication
headers and validation process.
"""

@functools.wraps(handler)
async def wrapper(request):
try:
Expand All @@ -247,7 +290,7 @@
logging.exception(e)
raise

# authenticated_sender is the authenticted wallet address of the requester (as a string)
# authenticated_sender is the authenticate wallet address of the requester (as a string)
response = await handler(request, authenticated_sender)
return response

Expand Down
2 changes: 1 addition & 1 deletion src/aleph/vm/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
elif hasattr(o, "dict"): # Pydantic
return o.dict()
elif is_dataclass(o):
return dataclass_as_dict(o)
return dataclass_as_dict(o) # type: ignore

Check warning on line 78 in src/aleph/vm/utils/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/aleph/vm/utils/__init__.py#L78

Added line #L78 was not covered by tests
else:
return str(o)

Expand Down
Loading