Skip to content

Commit

Permalink
Merge pull request #357 from LIT-Protocol/wyatt/sws-accs
Browse files Browse the repository at this point in the history
Init SIWS doc pages
  • Loading branch information
spacesailor24 authored Oct 8, 2024
2 parents 4301e5a + 322011b commit 3fb1a93
Show file tree
Hide file tree
Showing 11 changed files with 473 additions and 44 deletions.
124 changes: 124 additions & 0 deletions docs/sdk/access-control/solana/siws-access-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Sign-in With Solana Access Control

This guide builds on the [Sign-in With Solana Authentication](../../authentication/authenticating-siws.md) guide to show how to use Lit Actions to implement access control for Solana wallets authenticated using SIWS.

We combine the SIWS authentication with Lit Access Control Conditions to demonstrate how to control access to to an app based on the user's Solana public key.

## Prerequisites

Before continuing with this guide, make sure you have the following:

- An understanding of [Lit Actions](../../serverless-signing/overview) and how they work
- An understanding of how to [authenticate SIWS messages](../../authentication/authenticating-siws) using a Lit Action
- A basic understanding of Phantom's [SIWS specification](https://github.com/phantom/sign-in-with-solana/tree/main)
- An understanding of [Lit Access Control Conditions](../../../sdk/access-control/evm/basic-examples)

## High Level Overview

The following diagram depicts the flow of authenticating SIWS messages and then using the authenticated public key to check against Lit Access Control Conditions:

![SIWS Access Control Flow](../../../../static/img/siws-accs.png)

:::info
A full implementation of this code example can be found [here](https://github.com/LIT-Protocol/developer-guides-code/blob/master/siws-access-control/browser).
:::

## Writing the Lit Action

The Lit Action used for this example does the following:

1. Parses the raw SIWS message values
2. Builds the SIWS message according to the SIWS specification
3. Validates that the provided Solana signature is valid for the SIWS message
4. Compares the derived public key against the public key specified in the Lit Access Control Conditions
5. Returns the result of the comparison

For steps 1 - 3, we reuse the SIWS authentication logic from the [SIWS Authentication](../../authentication/authenticating-siws#parsing-and-re-building-the-siws-message) guide, so this guide will pick up from step 4.

:::note
The full implementation of this Lit Action can be found [here](https://github.com/LIT-Protocol/developer-guides-code/blob/master/siws-accs/browser/src/litActionSiws.ts).
:::

### Checking the Access Control Conditions

At this point we have validated that the provided user signature is for the SIWS message, constructed according to the SIWS specification. Now we need to check the user's Solana public key against the public key specified in the Lit Access Control Conditions:

```js
try {
const result = await LitActions.checkConditions({
conditions: solRpcConditions,
authSig: {
sig: ethers.utils
.hexlify(ethers.utils.base58.decode(_siwsObject.signature))
.slice(2),
derivedVia: "solana.signMessage",
signedMessage: siwsMessage,
address: siwsInput.address,
},
chain: "solana",
});

return LitActions.setResponse({ response: result });
} catch (error) {
console.error("Error checking if authed sol pub key is permitted:", error);
return LitActions.setResponse({
response: JSON.stringify({
success: false,
message: "Error checking if authed sol pub key is permitted.",
error: error.toString(),
}),
});
}
```

For this example, the Access Control Conditions (`solRpcConditions`) look like:

```js
[
{
method: "",
params: [":userAddress"],
pdaParams: [],
pdaInterface: { offset: 0, fields: {} },
pdaKey: "",
chain: "solana",
returnValueTest: {
key: "",
comparator: "=",
// This address would be the Solana public key that is authorized to access the app,
// and you would replace it with the actual public key that you want to authorize.
value: address,
},
},
];
```

When `LitActions.checkConditions` is executed, it parses the `authSig` property and derives the Solana public key from the `signedMessage`.

It then compares the derived public key against the `returnValueTest` property defined in the Access Control Condition. If the derived public key is equal to the `value` property from the `returnValueTest` object, the Access Control Condition is met and the Solana public key is considered authorized.

We then simply return the result of the Access Control Condition check, which is a boolean value, to the frontend:

```js
return LitActions.setResponse({ response: result });
```

## Summary

This guide demonstrates implementing access control using Sign-in With Solana (SIWS) messages, Lit Actions, and Access Control Conditions.

By leveraging Phantom's SIWS specification and Lit Access Control Conditions, we have established a robust and secure method for verifying Solana wallet ownership. This authentication mechanism can be extended to authorize specific Solana public keys for various operations using Lit, such as:

- **Encrypted Data Access**: Restrict [decryption](../../../sdk/access-control/intro) of sensitive information to specific Solana wallet owners.
- **Access Control**: Restrict access to an application to specific Solana wallet owners.

Key takeaways from this implementation are that the Lit Action:

- Reconstructs and verifies the SIWS message, ensuring the integrity of the signed data.
- Allows for custom validation of SIWS message properties to meet specific application requirements.
- Integrates Lit Access Control Conditions to perform custom authorization checks.

:::note
A full implementation of this code example can be found [here](https://github.com/LIT-Protocol/developer-guides-code/blob/master/siws-accs/browser).
:::

Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@ sidebar_position: 4

import FeedbackComponent from "@site/src/pages/feedback.md";

# Solana Examples
# Access Control Conditions Examples

:::danger
The following examples demonstrate additional Solana RPC Conditions you can use to control access to your application. After [authenticating a Sign-in With Solana (SIWS) message](../../authentication/authenticating-siws.md), you can use these Solana RPC Conditions to check if the authenticated Solana public key meets certain conditions.

Solana access control currently is missing various security properties due to the Solana communities lack of a standard on a signed message format. You should not use Solana access control for anything mission critical in it's current form. Any EDDSA signature that is valid will allow auth via Solana. This means, if a given wallet has ever made a Solana txn, it's possilble to auth as that wallet by taking the signature from the chain and presenting it to Lit for auth. If you're building on Solana, please reach out, so we can work on a standard signed message format to solve this problem.

:::

:::info
Solana Access Control Conditions are supported only by Auth Sigs not Session Sigs (read more about the difference between the two approaches [here](../../authentication/overview.md)).
:::note
You can use Solana RPC Conditions in the same way you would use EVM conditions, but you should pass a `solRpcConditions` array instead of a `accessControlConditions` or `evmContractConditions` array.
:::

Solana Access Control conditions work a little different than EVM access control conditions. Solana conditions let you make a Solana RPC call, and then filter and parse the response. This is useful for things like checking the balance of an account, checking the owner of an account, or checking the number of tokens a user has.

Note that you can use Solana RPC Conditions in the same way you would use EVM conditions, but you should pass a `solRpcConditions` array instead of a `accessControlConditions` or `evmContractConditions` array.

## Must posess an NFT in a Metaplex collection

In this example, we are checking if the user owns one or more NFTs in the Metaplex collection with address `FfyafED6kiJUFwEhogyTRQHiL6NguqNg9xcdeoyyJs33`. The collection must be verified. Note that "balanceOfMetaplexCollection" is not a real Solana RPC call. It is a custom RPC call that is specific to Lit Protocol.
Expand Down Expand Up @@ -64,28 +56,6 @@ var solRpcConditions = [
];
```

## A specific wallet address

In this example, we are checking that the user is in posession of a specific wallet address `88PoAjLoSqrTjH2cdRWq4JEezhSdDBw3g7Qa6qKQurxA`. The parameter of ":userAddress" will be automatically substituted with the user's wallet address which was verified by checking the message signed by their wallet.

```js
var solRpcConditions = [
{
method: "",
params: [":userAddress"],
pdaParams: [],
pdaInterface: { offset: 0, fields: {} },
pdaKey: "",
chain: "solana",
returnValueTest: {
key: "",
comparator: "=",
value: "88PoAjLoSqrTjH2cdRWq4JEezhSdDBw3g7Qa6qKQurxA",
},
},
];
```

## Must posess a balance of a specific token (Fungible or NFT)

This example checks if the user owns at least 1 token with address `FrYwrqLcGfmXrgJKcZfrzoWsZ3pqQB9pjjUC9PxSq3xT`. This is done by deriving the user's token account address for the token contract with the user's wallet address, validated via the message signed by the user's wallet. Then, `getTokenAccountBalance` is run on the user's token account address and the result is checked against the `returnValueTest`. Note that "balanceOfToken" is not a real Solana RPC call. It is a custom RPC call that is specific to Lit Protocol.
Expand Down
178 changes: 178 additions & 0 deletions docs/sdk/authentication/authenticating-siws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Authenticating Sign-in With Solana Messages

Deriving a Solana public key (aka. address) from a signed message works slightly differently than when using Lit to authenticate signed messages on EVM chains.

Currently the Lit nodes support deriving an address from a Sign-in With Ethereum Message (EIP-5573), while also validating what was signed using the [EIP-5573 specification](https://eips.ethereum.org/EIPS/eip-5573) (e.g. the signed message hasn't expired and has the correct format).

However, the Lit nodes built-in support for authenticating signed Solana messages is limited to just deriving the Solana public key from the signed data, but they do **not** perform any validation on the what was signed. This means that any data signed by a specific Solana wallet will be accepted, which opens the door for signature malleability and replay attacks.

For example, if you had implemented access control that requires a message signed by a specific wallet to permit access, anyone could take any signed message from that specific wallet and use it to gain access, including all past transactions signed by the wallet. This is possible because the Lit nodes do not validate what was signed or when it was signed; they only validate that the data was signed by the specific wallet.

This guide covers how to authenticate SIWS messages using a Lit Action and the [SIWS specification](https://github.com/phantom/sign-in-with-solana/tree/main) created by Phantom.

## Prerequisites

Before continuing with this guide, make sure you have the following:

- An understanding of [Lit Actions](../serverless-signing/overview) and how they work
- A basic understanding of Phantom's [SIWS specification](https://github.com/phantom/sign-in-with-solana/tree/main)

## High Level Overview

Sign-in With Solana (SIWS) allows users to authenticate with applications by signing a standardized message using their Solana wallet. This signed message can then be verified by the app to authenticate the user securely.

The following diagram depicts the flow of authenticating SIWS messages using Lit Actions:

![SIWS Authentication Flow](../../../static/img/siws-authentication.png)

:::info
A full implementation of this code example can be found [here](https://github.com/LIT-Protocol/developer-guides-code/blob/master/siws-authentication/siws-authentication/browser).
:::

## Writing the Lit Action

The Lit Action used for this example does the following:

1. Parses the raw SIWS message values
2. Builds the SIWS message according to the SIWS specification
3. Validates that the provided Solana signature is valid for the SIWS message
4. Returns the authenticated Solana public key

:::info
The full implementation of this Lit Action can be found [here](https://github.com/LIT-Protocol/developer-guides-code/blob/master/siws-authentication/browser/src/litActionSiwsAuth.ts).
:::

### Parsing and Re-building the SIWS Message

The SIWS message that the user signs has a specific structure defined by the SIWS specification. In this code example, the SIWS is built by the frontend and submitted to the user's wallet to prompt the user to sign the message.

At this point the frontend has the raw SIWS message and the user's signature of the message. The frontend could verify the signature itself, but for the sake of this example we'll assume that the frontend is untrusted and we're using Lit Actions as the backend to verify the SIWS message.

To verify the SIWS message within a Lit Action, the frontend needs to submit the raw SIWS message properties and the user's signature to the Lit Action. The Lit Action will then parse the raw SIWS message values and build the SIWS message according to the [Sign-In Input Fields specification](https://github.com/phantom/sign-in-with-solana/tree/main?tab=readme-ov-file#sign-in-input-fields).

An example of this is as follows:

```js
function getSiwsMessage(siwsInput) {
let message = `${siwsInput.domain} wants you to sign in with your Solana account:\n${siwsInput.address}`;

if (siwsInput.statement) {
message += `\n\n${siwsInput.statement}`;
}

const fields = [];

if (siwsInput.uri !== undefined) fields.push(`URI: ${siwsInput.uri}`);
if (siwsInput.version !== undefined)
fields.push(`Version: ${siwsInput.version}`);
if (siwsInput.chainId !== undefined)
fields.push(`Chain ID: ${siwsInput.chainId}`);
if (siwsInput.nonce !== undefined) fields.push(`Nonce: ${siwsInput.nonce}`);
if (siwsInput.issuedAt !== undefined)
fields.push(`Issued At: ${siwsInput.issuedAt}`);
if (siwsInput.expirationTime !== undefined)
fields.push(`Expiration Time: ${siwsInput.expirationTime}`);
if (siwsInput.notBefore !== undefined)
fields.push(`Not Before: ${siwsInput.notBefore}`);
if (siwsInput.requestId !== undefined)
fields.push(`Request ID: ${siwsInput.requestId}`);
if (siwsInput.resources !== undefined && siwsInput.resources.length > 0) {
fields.push("Resources:");
for (const resource of siwsInput.resources) {
fields.push(`- ${resource}`);
}
}

if (fields.length > 0) {
message += `\n\n${fields.join("\n")}`;
}

return message;
}
```

:::info
During reconstruction of the SIWS message would be a good time to validate that the given message properties like `domain`, `chainId`, `expirationTime`, etc. conform to the requirements of your application.
:::

The output of this function is a string that represents the SIWS message that we're expecting the user to have signed:

```
localhost wants you to sign in with your Solana account:
5ZS9h2RYtKVnPM19JSdgKaEE4UJeSEQGgtwmfuFyqLan
URI: http://localhost:5173
Version: 1
Chain ID: 0
Nonce: 341972
Issued At: 2024-10-03T04:55:11.105Z
Expiration Time: 2024-10-03T05:05:11.105Z
```

### Validating the SIWS Message Signature

Now that we have the SIWS message, built according to the SIWS specification, we can validate is against the user's signature provided to the Lit Action:

```js
async function verifySiwsSignature(
message,
signatureBase58,
publicKeyBase58
) {
// Convert message to Uint8Array
const messageBytes = new TextEncoder().encode(message);

try {
const signatureBytes = ethers.utils.base58.decode(signatureBase58);
const publicKeyBytes = ethers.utils.base58.decode(publicKeyBase58);

// Import the public key
const publicKey = await crypto.subtle.importKey(
"raw",
publicKeyBytes,
{
name: "Ed25519",
namedCurve: "Ed25519",
},
false,
["verify"]
);

// Verify the signature
const isValid = await crypto.subtle.verify(
"Ed25519",
publicKey,
signatureBytes,
messageBytes
);

return isValid;
} catch (error) {
console.error("Error in verifySiwsSignature:", error);
throw error;
}
}
```

The result of this function is a boolean value indicating whether the signature provided to the Lit Action is for the SIWS message constructed using the provided SIWS message properties.

We then simply return the now authenticated Solana public key (aka. address) from the given SIWS message, which is a string, to the frontend:

```js
Lit.Actions.setResponse({ response: siwsInput.address });
```

## Summary

This guide demonstrates implementing Sign-in With Solana (SIWS) authentication using Lit Actions.

By implementing Phantom's SIWS specification, we have established a robust and secure method for verifying Solana wallet ownership. This authentication mechanism can be extended to authorize specific Solana public keys for various operations using Lit, such as:

- **Encrypted Data Access**: Restrict [decryption](../../sdk/access-control/intro) of sensitive information to specific Solana wallet owners.
- **Secure Session Management**: Generate [Session Signatures](../../sdk/authentication/session-sigs/get-lit-action-session-sigs) only for specific authenticated users.
- **Authorized PKP Signing**: Leverage [PKP Signing](../../user-wallets/pkps/quick-start#sign-a-transaction) to ensure only authenticated users can sign data and transactions with a specific PKP.

Key takeaways from this implementation are that the Lit Action:

- Reconstructs and verifies the SIWS message, ensuring the integrity of the signed data.
- Allows for custom validation of SIWS message properties to meet specific application requirements.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FeedbackComponent from "@site/src/pages/feedback.md";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Generating SessionSigs Using a Lit Action
# Using a Lit Action

This guide covers the `getLitActionSessionSigs` function from the Lit SDK. For an overview of what Session Signatures are and how they are to be used, please go [here](./intro).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FeedbackComponent from "@site/src/pages/feedback.md";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Generating SessionSigs Using a PKP
# Using a PKP

This guide covers the `getPkpSessionSigs` function from the Lit SDK. For an overview of what Session Signatures are and how they are to be used, please go [here](./intro).

Expand Down
2 changes: 1 addition & 1 deletion docs/sdk/authentication/session-sigs/get-session-sigs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FeedbackComponent from "@site/src/pages/feedback.md";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Generating SessionSigs Using an Auth Sig
# Using an Auth Sig

This guide covers the `getSessionSigs` function from the Lit SDK. For an overview of what Session Signatures are and how they are to be used, please go [here](./intro).

Expand Down
Loading

0 comments on commit 3fb1a93

Please sign in to comment.