Lit Protocolの暗号化とアクセス制御を学ぶ
暗号化とアクセス制御
署名やウォレットとして使用されることの多いLitProtocolですが、今回は暗号化キーを共有せずに復号を行うことができる暗号化ツールとして、Docsをもとに使用してみます。
後々、Lit Actionを使った復号条件の自由度の高い暗号化も実装してみます。
今回はクライアント側での実装だけを取り扱います。
エラー回避等のハンドリングはぜひ、公式docsに書いてあるので見てみてください。
どんな暗号化とアクセス制御ができるの?
なんせ、LitProtocolはできることが多いので逆に何ができるのかよくわかんないという状態になっちゃいやすいと思うので、できることを整理していきます。
Quick Start
まずは、基本的な使い方一覧を見てみましょう。
Litを使用して簡単なメッセージを暗号化し、アクセス制御条件(Access Control Condition, ACC)を作成し、設定した条件を満たすユーザーに復号を許可する方法を見てみます。
・Litは暗号化キーの生成と保存にのみ使用されるため、暗号文とメタデータは自分で選択したストレージプロバイダー(例: IPFS、Arweave、または中央集権的なストレージ)に保存する必要があります。
・データが暗号化されると、Litネットワークは誰がそれを復号できるかを制御します。
このガイドでは、LitのMainnet BetaであるDatilネットワークを使用します。テスト環境ではDatil-testネットワークを推奨します。
SDKのインポート
公式docsを見てください
Lit nodeとの接続
まずはLit Networkと接続するために、Litクラスをセットアップします。
要は、chainの選択とlitNodeClientとのconnectですね。
import * as LitJsSdk from "@lit-protocol/lit-node-client";
import { LIT_NETWORK } from "@lit-protocol/constants";
class Lit {
litNodeClient;
chain;
constructor(chain) {
this.chain = chain;
}
async connect() {
this.litNodeClient = new LitJsSdk.LitNodeClient({
litNetwork: LIT_NETWORK.DatilDev,
});
await this.litNodeClient.connect();
}
}
const chain = "ethereum";
let myLit = new Lit(chain);
await myLit.connect();
暗号化の実装
必要な手順は以下の3つに集約されます。
- アクセス制御条件(ACC)を作成。
- 静的コンテンツ(文字列、ファイル、ZIPなど)を暗号化。
- 暗号文とメタデータをストレージプロバイダーに保存。
1. アクセス制御条件(ACC)を作成
まずは、Litでの暗号化における全ての復号条件の基本となるアクセス制御条件(ACC)を設定します。
この復号条件が、後で出てくる様々なカスタム復号条件のベースの条件となることを理解してください。
ウォレットが0.000001 ETH以上を持つ条件を設定
const accessControlConditions = [
{
contractAddress: "",
standardContractType: "",
chain: "ethereum",
method: "eth_getBalance",
parameters: [":userAddress", "latest"],
returnValueTest: {
comparator: ">=",
value: "1000000000000", // 0.000001 ETH
},
},
];
2. 静的コンテンツ(文字列、ファイル、ZIPなど)を暗号化
実際に暗号化を行います。
ポイントは「1. アクセス制御条件(ACC)を作成」で設定したACCを引数にとることです。
文字列を暗号化する例
import { encryptString } from "@lit-protocol/auth-helpers";
async function encrypt(message) {
const { ciphertext, dataToEncryptHash } = await encryptString({
accessControlConditions,
dataToEncrypt: message,
}, this.litNodeClient);
return { ciphertext, dataToEncryptHash };
}
3. 暗号文とメタデータをストレージプロバイダーに保存
「2. 静的コンテンツ(文字列、ファイル、ZIPなど)を暗号化」の返り値
return { ciphertext, dataToEncryptHash };
のciphertextをストレージプロバイダーに保存しましょう。
ここは、Lit Protocol外の話なので割愛します。
復号
アクセス制御条件に基づいた復号処理の実装は以下のコードで行えます。
LitJsSdk.decryptToString({
accessControlConditions,
chain: "ethereum",
ciphertext,
dataToEncryptHash,
sessionSigs,
});
ここでも、ACC, chain, 暗号文(ciphertext),を含めるのがポイントですね。
セッション署名の生成
最後に、LitNodeを動かすために必要なsessionSigsを作成します。
この署名の認証については、別記事で詳細を解説してるのでみてみてください。
ここでは、軽く解説します。
Litノードとやり取りするにはセッション署名を生成する必要があります。
const sessionSigs = await this.litNodeClient.getSessionSigs({
chain: this.chain,
resourceAbilityRequests: [
{
resource: new LitAccessControlConditionResource("*"),
ability: LIT_ABILITY.AccessControlConditionDecryption,
},
],
authNeededCallback,
});
今回は、ACCを用いた復号条件の設定を行なっているので、resourceAbilityRequestsに
ability: LIT_ABILITY.AccessControlConditionDecryption
を設定します。
ここまでが、暗号化とアクセス制御条件の設定の基本的な流れです。
では、ここからLit暗号化とアクセス制御でできることを詳しくみていきましょう。
アクセス制御
この暗号化とアクセス制御のtopicにおいて、開発者の自由度が高いのは「アクセス制御」です。
このアクセス制御が、オンチェーンの動きをキャッチしたり、オフチェーンでの実行結果に基づいたりと、様々なソースから条件をカスタムできることが魅力です。
つまり、アクセス制御条件が設定、利用できるパターンを知ることができれば、Litのアクセス制御の概観を把握できたと言えます。
現状、アクセス制御条件が設定、利用できるパターンは主に5つです。
・JWT Auth
・EVM
・Solana
・Cosmos
・Lit Action(Offchain)
さらに、オンチェーンのアクセス条件は組み合わせて一つのアクセス制御条件を作ることも可能です。
順番に見ていきましょう。
JWT Auth
概要
アクセス制御条件(Access Control Conditions, ACC)を利用して、JWTの署名を管理し、サーバーから動的コンテンツを読み込む際の認証ゲートとして使用できます。
Dapp開発者は、特定のURLがアクセス制御条件を満たしている場合のみコンテンツを提供するよう設定できます。この条件は静的(明示的に宣言)にも動的(オンザフライで設定)にもできます。
BLSネットワークは、ユーザーが条件を満たしていることを確認し、該当条件を含むJWTを署名します。
JWTを生成する手順は以下の通りです:
- クライアントが、特定のDapp URL用のアクセス制御条件を含むJWTの署名シェアを生成するようBLSネットワークにリクエスト。
- BLSネットワークノードが、ユーザーがアクセス制御条件を満たしていることを確認後、JWTペイロードを構築し、署名。
- クライアントが署名シェアを再結合し、完全なJWTを生成。
- Dappのウェブページが、JWTがBLSネットワークによって署名されたことを検証し、JWTのクレームに含まれるアクセス制御条件が期待どおりであることを確認。
動的コンテンツへのアクセス許可
Dapp開発者は、オンチェーンまたはオフチェーンの条件を基に動的コンテンツを制限できます。この条件は各ウェブページごとに静的または動的に宣言されます。
JWTの検証
JWTの検証は通常サーバーサイド(Node.js)で行われますが、ブラウザでも可能です。
手順:
- JWTを取得(通常、ユーザーが提示)。
- LitJsSdk.verifyJwtを使用して、BLSネットワークが署名したことを確認。
- JWTのクレームに含まれるアクセス制御条件がアプリケーションで宣言された条件と一致することを確認。
以下は、静的に宣言されたアクセス制御条件を検証する例です
import * as LitJsSdk from "@lit-protocol/lit-node-client-nodejs";
const jwt = "eyJhbGciOiJCTFMxMi0zODEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJMSVQiLCJzdWIiOiIweGRiZDM2MGYzMDA5N2ZiNmQ5MzhkY2M4YjdiNjI4NTRiMzYxNjBiNDUiLCJjaGFpbiI6ImZhbnRvbSIsImlhdCI6MTYyODAzMTM1OCwiZXhwIjoxNjI4MDc0NTU4LCJiYXNlVXJsIjoiaHR0cHM6Ly9teS1keW5hbWljLWNvbnRlbnQtc2VydmVyLmNvbSIsInBhdGgiOiIvYV9wYXRoLmh0bWwiLCJvcmdJZCI6IiJ9.lX_aBSgGVYWd2FL6elRHoPJ2nab0IkmmX600cwZPCyK_SazZ-pzBUGDDQ0clthPVAtoS7roHg14xpEJlcSJUZBA7VTlPiDCOrkie_Hmulj765qS44t3kxAYduLhNQ-VN";
// JWTの検証
// verified: JWTの署名が正しく検証されたかを示すブール値。
const { verified, header, payload } = LitJsSdk.verifyJwt({
jwt,
publicKey: litNodeClient.networkPubKey,
});
// アクセス制御条件を静的に宣言
const accessControlCondtionsForProtectedPath1 = {
accessControlConditions: [{
chain: 'polygon',
contractAddress: '',
method: '',
parameters: [':userAddress'],
returnValueTest: {
comparator: '=',
value: "YOUR_WALLET_ADDRESS",
},
standardContractType: '',
}]
};
// アクセス制御条件のハッシュを比較
const expectedHash = await litNodeClient.getHashedAccessControlConditions(accessControlCondtionsForProtectedPath1);
const actualHash = await litNodeClient.getHashedAccessControlConditions(payload);
if (expectedHash.toString() !== actualHash.toString()) {
// リクエストを拒否
throw new Error("アクセス制御条件が一致しません");
}
JWTを使用したリソースへのアクセス
Litネットワークから署名済みJWTを取得するには、getSignedToken関数を使用します。
const litNodeClient = new LitJsSdk.LitNodeClient();
await litNodeClient.connect();
const jwt = await litNodeClient.getSignedToken({
accessControlConditions,
chain: "ethereum",
sessionSigs,
});
取得したJWTをサーバーに提示し、verifyJwtを使用して検証します。
JWTについては以下の記事がわかりやすく解説してくれていました。
EVM
以下は、EVMチェーン(Ethereumチェーン)上の標準的なコントラクトタイプ(ERC20、ERC721、ERC1155など)を使用したアクセス制御条件のサンプルです。また、ウォレットアドレスの所有確認やProof of Humanity、POAP所持に基づく条件も含まれています。
1. 特定のERC1155トークン(ID指定)を1つ以上所持
特定のERC1155トークン(コントラクトアドレス:0x3110c39b428221012934A7F617913b095BC1078C
、トークンID:9541)を1つ以上持っていることを確認。
const accessControlConditions = [
{
contractAddress: '0x3110c39b428221012934A7F617913b095BC1078C',
standardContractType: 'ERC1155',
chain,
method: 'balanceOf',
parameters: [
':userAddress',
'9541'
],
returnValueTest: {
comparator: '>',
value: '0'
}
}
];
2. 一括チェックで指定されたERC1155トークンを1つ以上所持
複数のトークンID(1, 2, 10003, 10004)のうち、いずれかを1つ以上持っているかを確認。
const accessControlConditions = [
{
contractAddress: '0x10daa9f4c0f985430fde4959adb2c791ef2ccf83',
standardContractType: 'ERC1155',
chain,
method: 'balanceOfBatch',
parameters: [
':userAddress,:userAddress,:userAddress,:userAddress',
'1,2,10003,10004'
],
returnValueTest: {
comparator: '>',
value: '0'
}
}
];
3. 特定のERC721トークン(NFT)を所持
ERC721トークン(コントラクトアドレス:0x89b597199dAc806Ceecfc091e56044D34E59985c
、トークンID:3112)の所有者であることを確認。
const accessControlConditions = [
{
contractAddress: '0x89b597199dAc806Ceecfc091e56044D34E59985c',
standardContractType: 'ERC721',
chain,
method: 'ownerOf',
parameters: [
'3112'
],
returnValueTest: {
comparator: '=',
value: ':userAddress'
}
}
];
4. ERC721コレクションのいずれかのNFTを所持
ERC721コレクション(コントラクトアドレス:0xA80617371A5f511Bf4c1dDf822E6040acaa63e71
)のいずれかのNFTを所持していることを確認。
const accessControlConditions = [
{
contractAddress: '0xA80617371A5f511Bf4c1dDf822E6040acaa63e71',
standardContractType: 'ERC721',
chain,
method: 'balanceOf',
parameters: [
':userAddress'
],
returnValueTest: {
comparator: '>',
value: '0'
}
}
];
5. ERC20トークンを所持
MakerDAOのERC20トークン(コントラクトアドレス:0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2
)を所持していることを確認。
const accessControlConditions = [
{
contractAddress: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2',
standardContractType: 'ERC20',
chain,
method: 'balanceOf',
parameters: [
':userAddress'
],
returnValueTest: {
comparator: '>',
value: '0'
}
}
];
6. 0.00001 ETH以上を所持
ETHの残高が0.00001 ETH(10,000,000,000,000 Wei)以上であることを確認。
const accessControlConditions = [
{
contractAddress: '',
standardContractType: '',
chain,
method: 'eth_getBalance',
parameters: [
':userAddress',
'latest'
],
returnValueTest: {
comparator: '>=',
value: '10000000000000'
}
}
];
7. DAOのメンバーである
特定のMolochDAO(コントラクトアドレス:0x50D8EB685a9F262B13F28958aBc9670F06F819d9
)のメンバーであることを確認。
const accessControlConditions = [
{
contractAddress: '0x50D8EB685a9F262B13F28958aBc9670F06F819d9',
standardContractType: 'MolochDAOv2.1',
chain,
method: 'members',
parameters: [
':userAddress',
],
returnValueTest: {
comparator: '=',
value: 'true'
}
}
];
8. 特定のウォレットアドレスを所持
ウォレットアドレスが 0x50e2dac5e78B5905CB09495547452cEE64426db2
であることを確認。
const accessControlConditions = [
{
contractAddress: '',
standardContractType: '',
chain,
method: '',
parameters: [
':userAddress',
],
returnValueTest: {
comparator: '=',
value: '0x50e2dac5e78B5905CB09495547452cEE64426db2'
}
}
];
9. Proof of Humanity に登録
Proof of Humanity(https://www.proofofhumanity.id/)に登録されていることを確認。
const accessControlConditions = [
{
contractAddress: "0xC5E9dDebb09Cd64DfaCab4011A0D5cEDaf7c9BDb",
standardContractType: "ProofOfHumanity",
chain: "ethereum",
method: "isRegistered",
parameters: [":userAddress"],
returnValueTest: {
comparator: "=",
value: "true"
}
}
];
EVMのアクセス制御条件の詳細は別記事で解説します。
Solana
comingsoon
Cosmos
comingsoon
Lit Action(Offchain)
Lit Action Conditionsを使用すると、特定のLit Actionが指定した条件を満たした場合にのみアクセスを許可することができます。Lit Actionsは、Lit Protocolネットワーク上で実行可能なJavaScriptコードで、カスタムのアクセス制御条件を作成するために利用できます。
以下は、気温が40度未満の場合にtrueを返すLit Actionを作成し、その条件を使用してドキュメントの復号を制御する例です。
1. Lit Actionコードの作成
次のコードは、気温が指定値以下であるかを判断します。このコードは外部APIにアクセスし、現在の天気予報を取得します:
const go = async (maxTemp) => {
const url = "https://api.weather.gov/gridpoints/LWX/97,71/forecast";
try {
const response = await fetch(url).then((res) => res.json());
const nearestForecast = response.properties.periods[0];
const temp = nearestForecast.temperature;
return temp < parseInt(maxTemp);
} catch (e) {
console.log(e);
}
return false;
};
このコードをIPFSに保存します。
2. Lit ActionのCID
上記のコードをIPFSにアップロードした際に、以下のCID(例)が生成されたとします:
QmcgbVu2sJSPpTeFhBd174FnmYmoVYvUFJeDkS7eYtwoFY
3. Lit Action Conditionsの設定
この例では、以下のようなAccess Control Conditionsを定義します:
var accessControlConditions = [
{
contractAddress: "ipfs://QmcgbVu2sJSPpTeFhBd174FnmYmoVYvUFJeDkS7eYtwoFY",
standardContractType: "LitAction",
chain: "ethereum",
method: "go",
parameters: ["40"], // 気温の閾値を文字列で指定
returnValueTest: {
comparator: "=",
value: "true", // Lit Actionがtrueを返すことが条件
},
},
];
この条件は、以下のロジックで動作します:
-
method: 実行するLit Actionの関数(
go
)。 -
parameters: 関数に渡すパラメータ(例:
40
)。 -
returnValueTest: 戻り値が
true
であることを確認。
4. 動作確認
- ユーザーがリソースへのアクセスを要求すると、Lit Protocolは指定されたLit Actionを実行します。
- Lit Actionの結果が
true
であれば、アクセスが許可されます。
戻り値オプション
Lit ActionのJavaScript関数は、任意の文字列を返すことが可能です。この戻り値を以下のような比較演算子で評価できます:
- =: 等しい
- !=: 等しくない
- contains: 指定した値を含む
- !contains: 指定した値を含まない
応用例
例えば、以下の条件を作成することも可能です:
- ユーザーのウォレットアドレスが特定のリストに含まれているかをチェック。
- 外部APIから取得した値が、事前定義された条件に一致するかを確認。
この仕組みを使うことで、外部データやリアルタイムデータに基づいた柔軟なアクセス制御が実現できます。
つまり、javascriptコードをIPFSに保存して、そのアドレスをACCに含めることで、あたかもオフチェーンのjsコードをコントラクトのように扱うことができるということですね。
このLit Actionについては別記事で深ぼっているので見てみてください。
Unified Access Control Conditions(統合アクセス制御条件)
最後に、複数のオンチェーンのアクセス条件を組み合わせて一つのアクセス制御条件を作る方法を紹介します。
概要
統合アクセス制御条件(Unified Access Control Conditions)は、以下の条件を1つの配列に組み合わせて使用することができます:
- EVM基本条件(EVM Basic Conditions)
- EVMカスタムコントラクト条件(EVM Custom Contract Conditions)
- Solana RPC条件
- Cosmos条件
unifiedAccessControlConditions
パラメータを使用して、LitNodeClient
のすべてのメソッドでこれらの条件を指定できます。
条件タイプ(conditionType)
各条件に以下の conditionType
フィールドを追加します:
-
evmBasic
: EVM基本条件(従来のアクセス制御条件) -
evmContract
: EVMカスタムコントラクト条件 -
solRpc
: Solana RPC条件 -
cosmos
: CosmosまたはKyve条件
ウォレットのAuthSigを渡す方法
LitNodeClient APIへのすべてのリクエストには AuthSig
が必要です。統合アクセス制御条件を使用する場合、複数のチェーンの AuthSig
を同時に渡すことができます。
// 各チェーンのAuthSigを取得
var solAuthSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "solana" });
var ethAuthSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "ethereum" });
var cosmosAuthSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "cosmos" });
var kyveAuthSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "kyve" });
// AuthSigをオブジェクト形式で渡す
await litNodeClient.encryptString({
unifiedAccessControlConditions,
authSig: {
solana: solAuthSig,
ethereum: ethAuthSig, // EVMチェーン用
cosmos: cosmosAuthSig,
kyve: kyveAuthSig,
},
dataToEncrypt: "秘密のデータ",
});
AuthSigについて詳しくはこちら
4種類の条件を組み合わせる例
以下の例では、次の条件を "または(or)" 演算子で組み合わせます。ユーザーは次のいずれかを満たす必要があります:
- Solanaで0.1 SOL以上を保有
- Ethereumで0.00001 ETH以上を保有
- PolygonでERC1155トークン(ID 8)を1つ以上保有
- Cosmosで1 ATOM以上を保有
var unifiedAccessControlConditions = [
{
conditionType: "solRpc",
method: "getBalance",
params: [":userAddress"],
chain: "solana",
pdaParams: [],
pdaInterface: { offset: 0, fields: {} },
pdaKey: "",
returnValueTest: {
key: "",
comparator: ">=",
value: "100000000", // 0.1 SOL
},
},
{ operator: "or" },
{
conditionType: "evmBasic",
contractAddress: "",
standardContractType: "",
chain: "ethereum",
method: "eth_getBalance",
parameters: [":userAddress", "latest"],
returnValueTest: {
comparator: ">=",
value: "10000000000000", // 0.00001 ETH
},
},
{ operator: "or" },
{
conditionType: "evmContract",
contractAddress: "0x7C7757a9675f06F3BE4618bB68732c4aB25D2e88",
functionName: "balanceOf",
functionParams: [":userAddress", "8"],
functionAbi: {
type: "function",
stateMutability: "view",
outputs: [
{ type: "uint256", name: "", internalType: "uint256" },
],
name: "balanceOf",
inputs: [
{ type: "address", name: "account", internalType: "address" },
{ type: "uint256", name: "id", internalType: "uint256" },
],
},
chain: "polygon",
returnValueTest: {
key: "",
comparator: ">",
value: "0", // 1つ以上保有
},
},
{ operator: "or" },
{
conditionType: "cosmos",
path: "/cosmos/bank/v1beta1/balances/:userAddress",
chain: "cosmos",
returnValueTest: {
key: "$.balances[0].amount",
comparator: ">=",
value: "1000000", // 1 ATOM
},
},
];
比較演算子のサポート
returnValueTest.comparator
フィールドでは以下の演算子を指定可能です:
-
>
(より大きい) -
<
(より小さい) -
>=
(以上) -
<=
(以下) -
=
(等しい) -
!=
(等しくない) -
contains
(指定された値を含む) -
!contains
(指定された値を含まない)
このように、Lit Protocolでは、条件のチェックにブールロジックを使用できます。"operator"
プロパティを "and"
または "or"
に設定して条件を組み合わせます。
基本的なブール条件
例えば、以下のようにDAOのメンバーであるか、0.00001 ETH以上保有しているかのどちらかをチェックする条件を設定できます:
const accessControlConditions = [
{
contractAddress: "0x50D8EB685a9F262B13F28958aBc9670F06F819d9",
standardContractType: "MolochDAOv2.1",
chain, // チェーン名を指定 (例: 'ethereum')
method: "members",
parameters: [":userAddress"],
returnValueTest: {
comparator: "=",
value: "true",
},
},
{ operator: "or" }, // OR条件
{
contractAddress: "",
standardContractType: "",
chain,
method: "eth_getBalance",
parameters: [":userAddress", "latest"],
returnValueTest: {
comparator: ">=",
value: "10000000000000", // 0.00001 ETH
},
},
];
ネストされたブール条件
ネストされた条件を使うと、さらに複雑な条件を定義できます。
例えば、DAOのメンバーであり、さらに以下のいずれかを満たす条件を設定する場合:
- 0.00001 ETH以上保有している
- ERC20トークンを10以上保有している
以下のように設定します:
const accessControlConditions = [
{
contractAddress: "0x50D8EB685a9F262B13F28958aBc9670F06F819d9",
standardContractType: "MolochDAOv2.1",
chain, // チェーン名を指定 (例: 'ethereum')
method: "members",
parameters: [":userAddress"],
returnValueTest: {
comparator: "=",
value: "true",
},
},
{ operator: "and" }, // AND条件
[
{
contractAddress: "",
standardContractType: "",
chain,
method: "eth_getBalance",
parameters: [":userAddress", "latest"],
returnValueTest: {
comparator: ">=",
value: "10000000000000", // 0.00001 ETH
},
},
{ operator: "or" }, // OR条件
{
contractAddress: "0xc0ad7861fe8848002a3d9530999dd29f6b6cae75",
standardContractType: "ERC20",
chain,
method: "balanceOf",
parameters: [":userAddress"],
returnValueTest: {
comparator: ">",
value: "10", // 10以上のトークン
},
},
],
];
説明
-
operator
:-
"and"
: 両方の条件を満たす必要があります。 -
"or"
: いずれかの条件を満たせば十分です。
-
-
ネスト:
- 条件を配列でまとめることで、ネストされたロジックを構築できます。
- 各条件の間に
"and"
や"or"
を挿入して組み合わせます。
長くなりましたが、以上がLitProtocolの暗号化とアクセス制御の全体像です!
Discussion