1use crate::error::LitSdkError;
2use ed25519_dalek::SigningKey;
3use ethers::signers::{LocalWallet, Signer};
4use ethers::types::Address as EthAddress;
5use ethers::utils::{keccak256, to_checksum};
6use rand::rngs::OsRng;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct SessionKeyPair {
13 pub public_key: String,
14 pub secret_key: String,
15}
16
17pub fn generate_session_key_pair() -> SessionKeyPair {
18 let mut csprng = OsRng;
19 let signing_key = SigningKey::generate(&mut csprng);
20 let verifying_key = signing_key.verifying_key();
21
22 SessionKeyPair {
23 public_key: hex::encode(verifying_key.to_bytes()),
24 secret_key: hex::encode(signing_key.to_bytes()),
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct AuthData {
31 pub auth_method_id: String,
32 pub auth_method_type: u32,
33 pub access_token: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub public_key: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub metadata: Option<serde_json::Value>,
38}
39
40pub async fn create_eth_wallet_auth_data(
41 private_key_hex: &str,
42 nonce: &str,
43) -> Result<AuthData, LitSdkError> {
44 use chrono::{SecondsFormat, Utc};
45 use siwe::{Message, TimeStamp};
46
47 let wallet: LocalWallet = private_key_hex
48 .parse::<LocalWallet>()
49 .map_err(|e| LitSdkError::Config(format!("invalid private key: {e}")))?;
50 let checksum_address = to_checksum(&wallet.address(), None);
51
52 let issued_at: TimeStamp = Utc::now()
54 .to_rfc3339_opts(SecondsFormat::Millis, true)
55 .parse::<TimeStamp>()
56 .map_err(|e| LitSdkError::Config(format!("invalid issued_at timestamp: {e}")))?;
57 let expiration_time: TimeStamp = (Utc::now() + chrono::Duration::days(7))
58 .to_rfc3339_opts(SecondsFormat::Millis, true)
59 .parse::<TimeStamp>()
60 .map_err(|e| LitSdkError::Config(format!("invalid expiration timestamp: {e}")))?;
61
62 let message = Message {
63 domain: "localhost"
64 .parse::<http::uri::Authority>()
65 .map_err(|e| LitSdkError::Config(format!("invalid domain: {e}")))?,
66 address: wallet.address().0,
67 statement: Some("This is a test statement. You can put anything you want here.".into()),
68 uri: "https://localhost/login"
69 .parse::<iri_string::types::UriString>()
70 .map_err(|e| LitSdkError::Config(format!("invalid uri: {e}")))?,
71 version: siwe::Version::V1,
72 chain_id: 1,
73 nonce: nonce.to_string(),
74 issued_at,
75 expiration_time: Some(expiration_time),
76 not_before: None,
77 request_id: None,
78 resources: vec![],
79 };
80
81 let siwe_message = message.to_string();
82 let auth_sig = sign_siwe_with_eoa(private_key_hex, &siwe_message).await?;
83
84 let method_id_hash = keccak256(format!("{checksum_address}:lit").as_bytes());
85 let auth_method_id = format!("0x{}", hex::encode(method_id_hash));
86
87 Ok(AuthData {
88 auth_method_id,
89 auth_method_type: 1,
90 access_token: serde_json::to_string(&auth_sig)
91 .map_err(|e| LitSdkError::Config(format!("failed to serialize authSig: {e}")))?,
92 public_key: None,
93 metadata: None,
94 })
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct AuthSig {
100 pub sig: String,
101 pub derived_via: String,
102 pub signed_message: String,
103 pub address: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub algo: Option<String>,
106}
107
108#[derive(Debug, Clone)]
109pub struct AuthConfig {
110 pub capability_auth_sigs: Vec<AuthSig>,
111 pub expiration: String,
112 pub statement: String,
113 pub domain: String,
114 pub resources: Vec<ResourceAbilityRequest>,
115}
116
117#[derive(Debug, Clone, Default)]
118pub struct CustomAuthParams {
119 pub lit_action_code: Option<String>,
120 pub lit_action_ipfs_id: Option<String>,
121 pub js_params: Option<serde_json::Value>,
122}
123
124#[derive(Debug, Clone)]
125pub struct ResourceAbilityRequest {
126 pub ability: LitAbility,
127 pub resource_id: String,
128 pub data: Option<serde_json::Value>,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum LitAbility {
133 AccessControlConditionDecryption,
134 AccessControlConditionSigning,
135 PKPSigning,
136 PaymentDelegation,
137 LitActionExecution,
138 ResolvedAuthContext,
140}
141
142impl FromStr for LitAbility {
143 type Err = ();
144
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
146 match s {
147 "access-control-condition-decryption" => {
148 Ok(LitAbility::AccessControlConditionDecryption)
149 }
150 "access-control-condition-signing" => Ok(LitAbility::AccessControlConditionSigning),
151 "pkp-signing" => Ok(LitAbility::PKPSigning),
152 "lit-payment-delegation" => Ok(LitAbility::PaymentDelegation),
153 "lit-action-execution" => Ok(LitAbility::LitActionExecution),
154 "lit-resolved-auth-context" => Ok(LitAbility::ResolvedAuthContext),
155 _ => Err(()),
156 }
157 }
158}
159
160impl LitAbility {
161 pub fn as_str(&self) -> &'static str {
162 match self {
163 LitAbility::AccessControlConditionDecryption => "access-control-condition-decryption",
164 LitAbility::AccessControlConditionSigning => "access-control-condition-signing",
165 LitAbility::PKPSigning => "pkp-signing",
166 LitAbility::PaymentDelegation => "lit-payment-delegation",
167 LitAbility::LitActionExecution => "lit-action-execution",
168 LitAbility::ResolvedAuthContext => "lit-resolved-auth-context",
169 }
170 }
171
172 fn recap_namespace_and_ability(&self) -> (&'static str, &'static str) {
173 match self {
174 LitAbility::AccessControlConditionDecryption => ("Threshold", "Decryption"),
175 LitAbility::AccessControlConditionSigning => ("Threshold", "Signing"),
176 LitAbility::PKPSigning => ("Threshold", "Signing"),
177 LitAbility::PaymentDelegation => ("Auth", "Auth"),
178 LitAbility::LitActionExecution => ("Threshold", "Execution"),
179 LitAbility::ResolvedAuthContext => ("Auth", "Auth"),
180 }
181 }
182
183 pub fn resource_prefix(&self) -> &'static str {
184 match self {
185 LitAbility::AccessControlConditionDecryption
186 | LitAbility::AccessControlConditionSigning => "lit-accesscontrolcondition",
187 LitAbility::PKPSigning => "lit-pkp",
188 LitAbility::PaymentDelegation => "lit-paymentdelegation",
189 LitAbility::LitActionExecution => "lit-litaction",
190 LitAbility::ResolvedAuthContext => "lit-resolvedauthcontext",
191 }
192 }
193
194 pub fn resource_key(&self, resource_id: &str) -> String {
195 format!("{}://{}", self.resource_prefix(), resource_id)
196 }
197}
198
199pub fn create_siwe_message_with_resources(
201 wallet_address: &str,
202 session_public_key_hex: &str,
203 auth_config: &AuthConfig,
204 nonce: &str,
205) -> Result<String, LitSdkError> {
206 use chrono::{SecondsFormat, Utc};
207 use siwe::{Message, TimeStamp};
208 use siwe_recap::Capability;
209 use std::collections::BTreeMap;
210
211 let uri = format!("lit:session:{}", session_public_key_hex);
212
213 let eth_addr: EthAddress = wallet_address
214 .parse::<EthAddress>()
215 .map_err(|e| LitSdkError::Config(format!("invalid wallet address: {e}")))?;
216
217 let issued_at: TimeStamp = Utc::now()
218 .to_rfc3339_opts(SecondsFormat::Millis, true)
219 .parse::<TimeStamp>()
220 .map_err(|e| LitSdkError::Config(format!("invalid issued_at timestamp: {e}")))?;
221
222 let expiration_time: Option<TimeStamp> = Some(
223 auth_config
224 .expiration
225 .parse::<TimeStamp>()
226 .map_err(|e| LitSdkError::Config(format!("invalid expiration timestamp: {e}")))?,
227 );
228
229 let message = Message {
230 domain: auth_config
231 .domain
232 .parse::<http::uri::Authority>()
233 .map_err(|e| LitSdkError::Config(format!("invalid domain: {e}")))?,
234 address: eth_addr.0,
235 statement: Some(auth_config.statement.clone()),
236 uri: uri
237 .parse::<iri_string::types::UriString>()
238 .map_err(|e| LitSdkError::Config(format!("invalid session uri: {e}")))?,
239 version: siwe::Version::V1,
240 chain_id: 1,
241 nonce: nonce.to_string(),
242 issued_at,
243 expiration_time,
244 not_before: None,
245 request_id: None,
246 resources: vec![],
247 };
248
249 if auth_config.resources.is_empty() {
250 return Ok(message.to_string());
251 }
252
253 let mut cap = Capability::<serde_json::Value>::default();
254 for req in &auth_config.resources {
255 let (ns, ability) = req.ability.recap_namespace_and_ability();
256 let resource_key = req.ability.resource_key(&req.resource_id);
257
258 let nb_map: BTreeMap<String, serde_json::Value> = match &req.data {
259 Some(val) => val
260 .as_object()
261 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
262 .unwrap_or_default(),
263 None => BTreeMap::new(),
264 };
265
266 cap.with_action_convert(resource_key, format!("{}/{}", ns, ability), vec![nb_map])
267 .map_err(|e| LitSdkError::Config(format!("failed to add recap attenuation: {e}")))?;
268 }
269
270 let message = cap
271 .build_message(message)
272 .map_err(|e| LitSdkError::Config(e.to_string()))?;
273
274 Ok(message.to_string())
275}
276
277pub fn validate_delegation_auth_sig(
278 delegation_auth_sig: &AuthSig,
279 session_public_key_hex: &str,
280) -> Result<(), LitSdkError> {
281 let expected_session_key_uri = if session_public_key_hex.starts_with("lit:session:") {
282 session_public_key_hex.to_string()
283 } else {
284 format!("lit:session:{session_public_key_hex}")
285 };
286
287 let msg = siwe::Message::from_str(&delegation_auth_sig.signed_message)
288 .map_err(|e| LitSdkError::Crypto(format!("invalid delegation SIWE message: {e}")))?;
289
290 if let Some(exp) = msg.expiration_time {
291 let exp_str = exp.to_string();
292 let exp_dt = chrono::DateTime::parse_from_rfc3339(&exp_str)
293 .map_err(|e| LitSdkError::Crypto(format!("invalid delegation expiration: {e}")))?;
294 if exp_dt <= chrono::Utc::now() {
295 return Err(LitSdkError::Crypto(format!(
296 "delegation signature expired at {exp_str}"
297 )));
298 }
299 }
300
301 let uri = msg.uri.to_string();
302 if uri != expected_session_key_uri {
303 return Err(LitSdkError::Crypto(
304 "session key URI in delegation signature does not match".into(),
305 ));
306 }
307
308 Ok(())
309}
310
311fn base64url_decode_vec(input: &str) -> Result<Vec<u8>, LitSdkError> {
312 use base64ct::{Base64Url, Base64UrlUnpadded, Encoding};
313
314 Base64UrlUnpadded::decode_vec(input)
315 .or_else(|_| Base64Url::decode_vec(input))
316 .map_err(|e| LitSdkError::Crypto(format!("invalid base64url payload: {e}")))
317}
318
319fn resource_ability_requests_from_recap_urn(
320 urn: &str,
321) -> Result<Vec<ResourceAbilityRequest>, LitSdkError> {
322 let encoded = urn
323 .strip_prefix("urn:recap:")
324 .ok_or_else(|| LitSdkError::Crypto("invalid recap URN".into()))?;
325
326 let decoded = base64url_decode_vec(encoded)?;
327 let payload: serde_json::Value = serde_json::from_slice(&decoded)
328 .map_err(|e| LitSdkError::Crypto(format!("invalid recap JSON: {e}")))?;
329
330 let att = payload
331 .get("att")
332 .and_then(|v| v.as_object())
333 .ok_or_else(|| LitSdkError::Crypto("invalid recap attenuation payload".into()))?;
334
335 let mut out = Vec::new();
336
337 for (resource_key, ability_map) in att {
338 let Some(ability_map) = ability_map.as_object() else {
339 continue;
340 };
341
342 let (resource_prefix, resource_id) = resource_key
343 .split_once("://")
344 .unwrap_or((resource_key.as_str(), "*"));
345
346 for (ability_key, restrictions) in ability_map {
347 let (namespace, recap_ability) = ability_key
348 .split_once('/')
349 .unwrap_or((ability_key.as_str(), ""));
350
351 let ability = match (resource_prefix, namespace, recap_ability) {
352 ("lit-pkp", "Threshold", "Signing") => LitAbility::PKPSigning,
353 ("lit-accesscontrolcondition", "Threshold", "Signing") => {
354 LitAbility::AccessControlConditionSigning
355 }
356 ("lit-accesscontrolcondition", "Threshold", "Decryption") => {
357 LitAbility::AccessControlConditionDecryption
358 }
359 ("lit-litaction", "Threshold", "Execution") => LitAbility::LitActionExecution,
360 ("lit-paymentdelegation", "Auth", "Auth") => LitAbility::PaymentDelegation,
361 ("lit-resolvedauthcontext", "Auth", "Auth") => LitAbility::ResolvedAuthContext,
362 _ => continue,
363 };
364
365 let mut data = None;
366 if let Some(arr) = restrictions.as_array() {
367 if let Some(obj) = arr.iter().find(|v| v.is_object()) {
368 if obj.as_object().map(|o| !o.is_empty()).unwrap_or(false) {
369 data = Some(obj.clone());
370 }
371 }
372 }
373
374 out.push(ResourceAbilityRequest {
375 ability,
376 resource_id: resource_id.to_string(),
377 data,
378 });
379 }
380 }
381
382 Ok(out)
383}
384
385pub fn auth_config_from_delegation_auth_sig(
386 delegation_auth_sig: &AuthSig,
387) -> Result<AuthConfig, LitSdkError> {
388 let msg = siwe::Message::from_str(&delegation_auth_sig.signed_message)
389 .map_err(|e| LitSdkError::Crypto(format!("invalid SIWE message: {e}")))?;
390
391 let expiration = msg
392 .expiration_time
393 .map(|t| t.to_string())
394 .unwrap_or_else(|| (chrono::Utc::now() + chrono::Duration::hours(24)).to_rfc3339());
395
396 let mut resources: Vec<ResourceAbilityRequest> = Vec::new();
397 for uri in &msg.resources {
398 let s = uri.to_string();
399 if !s.starts_with("urn:recap:") {
400 continue;
401 }
402 if let Ok(mut decoded) = resource_ability_requests_from_recap_urn(&s) {
403 resources.append(&mut decoded);
404 }
405 }
406
407 if resources.is_empty() {
408 resources.push(ResourceAbilityRequest {
409 ability: LitAbility::PKPSigning,
410 resource_id: "*".into(),
411 data: None,
412 });
413 }
414
415 Ok(AuthConfig {
416 capability_auth_sigs: vec![],
417 expiration,
418 statement: msg.statement.unwrap_or_default(),
419 domain: msg.domain.to_string(),
420 resources,
421 })
422}
423
424pub fn pkp_eth_address_from_pubkey(pkp_public_key_hex: &str) -> Result<String, LitSdkError> {
425 let pkp_hex = pkp_public_key_hex.trim_start_matches("0x");
426 let pkp_bytes = hex::decode(pkp_hex)
427 .map_err(|e| LitSdkError::Config(format!("invalid pkp public key hex: {e}")))?;
428 if pkp_bytes.len() < 2 {
429 return Err(LitSdkError::Config("pkp public key too short".into()));
430 }
431 let hash = keccak256(&pkp_bytes[1..]);
432 let addr = EthAddress::from_slice(&hash[12..]);
433 Ok(to_checksum(&addr, None))
434}
435
436pub async fn sign_siwe_with_eoa(
438 private_key_hex: &str,
439 siwe_message: &str,
440) -> Result<AuthSig, LitSdkError> {
441 let wallet: LocalWallet = private_key_hex
442 .parse::<LocalWallet>()
443 .map_err(|e| LitSdkError::Config(format!("invalid private key: {e}")))?;
444 let address = ethers::utils::to_checksum(&wallet.address(), None);
445
446 let sig = wallet
447 .sign_message(siwe_message)
448 .await
449 .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
450
451 Ok(AuthSig {
452 sig: sig.to_string(),
453 derived_via: "web3.eth.personal.sign".into(),
454 signed_message: siwe_message.into(),
455 address,
456 algo: None,
457 })
458}
459
460#[derive(Debug, Clone)]
461pub struct AuthContext {
462 pub session_key_pair: SessionKeyPair,
463 pub auth_config: AuthConfig,
464 pub delegation_auth_sig: AuthSig,
465}