Skip to main content

rings_core/
session.rs

1#![warn(missing_docs)]
2//! Understanding Abstract Account and Session keypair in Rings Network
3//!
4//! Rings network offers a unique mechanism to bolster security and abstract the user's keypair through a feature known as session keypair.
5//! The fundamental concept behind session keypair is signing a generated keypair with a time period {ts, ttl} by user without access its private key in this program.
6//! This can be conceptualized as a contract stating, "I delegate to a keypair for the time period {ts, ttl}".
7//!
8//! In our terminology:
9//! - `I` is [Account].
10//! - `keypair` is [SessionSk].
11//! - The time period {ts, ttl} is in `SessionSk.session` field.
12//!
13//! The following is an example to build a [SessionSk] in Rust and use it to sign a message.
14//! It is not necessary to construct a secret_key in Rust.
15//! User may manually set account_type, account_entity, and session_sig, instead of provide secret key.
16//! ```
17//! use rings_core::dht::Did;
18//! use rings_core::session::SessionSkBuilder;
19//!
20//! // We are generate an ethereum account for example.
21//! // It's convenient because secp256k1 is also used in session_sk.
22//! let user_secret_key = rings_core::ecc::SecretKey::random();
23//! let user_secret_key_did: Did = user_secret_key.address().into();
24//!
25//! // The account type is "secp256k1".
26//! // The account entity is its address, also known as Did in rings network.
27//! let account_type = "secp256k1".to_string();
28//! let account_entity = user_secret_key_did.to_string();
29//!
30//! let builder = SessionSkBuilder::new(account_entity, account_type);
31//! let unsigned_proof = builder.unsigned_proof();
32//!
33//! // Sign the unsigned proof with user's secret key.
34//! let session_sig = user_secret_key.sign(&unsigned_proof).to_vec();
35//! let builder = builder.set_session_sig(session_sig);
36//!
37//! let session_sk = builder.build().unwrap();
38//!
39//! // Check session_sk is valid. (The verify_self is already called in build().)
40//! assert_eq!(session_sk.account_did(), user_secret_key_did);
41//! assert!(session_sk.session().verify_self().is_ok());
42//!
43//! // Sign a message with session_sk.
44//! let msg = "hello world".as_bytes();
45//! let msg_sig = session_sk.sign(msg).unwrap();
46//! let msg_session = session_sk.session();
47//!
48//! // Verify the message with session.
49//! assert_eq!(msg_session.account_did(), user_secret_key_did);
50//! assert!(msg_session.verify(msg, msg_sig).is_ok());
51//! ```
52//!
53//! [SessionSkBuilder], [SessionSk] is exported to wasm envirement.
54//! To build a [SessionSk] in javascript:
55//! ```js
56//!    // prepare auth & send to metamask for sign
57//!    let sessionBuilder = SessionSkBuilder.new(account, 'eip191')
58//!    let unsignedSession = sessionBuilder.unsigned_proof()
59//!    const { signed } = await sendMessage(
60//!      'sign-message',
61//!      {
62//!        auth: unsignedSession,
63//!      },
64//!      'popup'
65//!    )
66//!    const signature = new Uint8Array(hexToBytes(signed))
67//!    sessionBuilder = sessionBuilder.set_session_sig(signature)
68//!    let sessionSk: SessionSk = sessionBuilder.build()
69//! ```
70
71//!
72//! See [SessionSk] and [SessionSkBuilder] for details.
73
74use std::str::FromStr;
75
76use rings_derive::wasm_export;
77use serde::Deserialize;
78use serde::Serialize;
79
80use crate::consts::DEFAULT_SESSION_TTL_MS;
81use crate::dht::Did;
82use crate::ecc::keccak256;
83use crate::ecc::keys::AccountVerifier;
84use crate::ecc::keys::SignatureAlgorithm;
85use crate::ecc::keys::VerificationPublicKey;
86use crate::ecc::signers;
87use crate::ecc::PublicKey;
88use crate::ecc::SecretKey;
89use crate::error::Error;
90use crate::error::Result;
91use crate::utils;
92
93fn pack_session(session_id: Did, ts_ms: u128, ttl_ms: u64) -> String {
94    format!("{session_id}\n{ts_ms}\n{ttl_ms}")
95}
96
97/// SessionSkBuilder is used to build a [SessionSk].
98///
99/// Firstly, you need to provide the account's entity and type to [SessionSkBuilder::new] method.
100/// Then you can call `pack_session` to get the session dump for signing.
101/// After signing, you can call `sig` to set the signature back to builder.
102/// Finally, you can call `build` to get the [SessionSk].
103#[wasm_export]
104pub struct SessionSkBuilder {
105    sk: SecretKey,
106    /// Account of session.
107    account_entity: String,
108    /// Account of session.
109    account_type: String,
110    /// Session's lifetime
111    ttl_ms: u64,
112    /// Timestamp when session created
113    ts_ms: u128,
114    /// Signature of session
115    sig: Vec<u8>,
116}
117
118/// SessionSk holds the [Session] and its session private key.
119/// To prove that the message was sent by the [Account] of [Session],
120/// we need to attach session and the signature signed by sk to the payload.
121///
122/// SessionSk provide a `session` method to clone the session.
123/// SessionSk also provide `sign` method to sign a message.
124///
125/// To verify the session, use `verify_self()` method of [Session].
126/// To verify a message, use `verify(msg, sig)` method of [Session].
127#[wasm_export]
128#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
129pub struct SessionSk {
130    /// Session
131    session: Session,
132    /// The private key of session. Used for signing and decrypting.
133    sk: SecretKey,
134}
135
136/// Session is used to verify the message.
137/// It's serializable and can be attached to the message payload.
138///
139/// To verify the session is provided by the account, use session.verify_self().
140/// To verify the message, use session.verify(msg, sig).
141#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
142pub struct Session {
143    /// Did of session, this is hash of sessionPk
144    session_id: Did,
145    /// Account of session
146    account: Account,
147    /// Session's lifetime
148    ttl_ms: u64,
149    /// Timestamp when session created
150    ts_ms: u128,
151    /// Signature to verify that the session was signed by the account.
152    sig: Vec<u8>,
153}
154
155/// We will support as many protocols/algorithms as possible.
156/// Currently, it comprises Secp256k1, EIP191, BIP137, Ed25519, and BLS12-381.
157/// We welcome any issues and PRs for additional implementations.
158#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
159pub enum Account {
160    /// ecdsa
161    Secp256k1(Did),
162    /// ref: <https://eips.ethereum.org/EIPS/eip-191>
163    Secp256r1(PublicKey<33>),
164    /// ref: <https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API>
165    EIP191(Did),
166    /// bitcoin bip137 ref: <https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki>
167    BIP137(Did),
168    /// ed25519
169    Ed25519(PublicKey<33>),
170    /// bls12-381
171    Bls12381(PublicKey<48>),
172}
173
174impl TryFrom<(String, String)> for Account {
175    type Error = Error;
176
177    fn try_from((account_entity, account_type): (String, String)) -> Result<Self> {
178        match AccountVerifier::from_account_parts(&account_entity, &account_type)? {
179            AccountVerifier::Recoverable {
180                algorithm: SignatureAlgorithm::Secp256k1,
181                did,
182            } => Ok(Account::Secp256k1(did)),
183            AccountVerifier::Recoverable {
184                algorithm: SignatureAlgorithm::Eip191,
185                did,
186            } => Ok(Account::EIP191(did)),
187            AccountVerifier::Recoverable {
188                algorithm: SignatureAlgorithm::Bip137,
189                did,
190            } => Ok(Account::BIP137(did)),
191            AccountVerifier::PublicKey(VerificationPublicKey::Secp256r1(pk)) => {
192                Ok(Account::Secp256r1(pk))
193            }
194            AccountVerifier::PublicKey(VerificationPublicKey::Ed25519(pk)) => {
195                Ok(Account::Ed25519(pk))
196            }
197            AccountVerifier::PublicKey(VerificationPublicKey::Bls12381(pk)) => {
198                Ok(Account::Bls12381(pk))
199            }
200            _ => Err(Error::UnknownAccount),
201        }
202    }
203}
204
205impl Account {
206    fn account_verifier(&self) -> AccountVerifier {
207        match self {
208            Self::Secp256k1(did) => AccountVerifier::Recoverable {
209                algorithm: SignatureAlgorithm::Secp256k1,
210                did: *did,
211            },
212            Self::EIP191(did) => AccountVerifier::Recoverable {
213                algorithm: SignatureAlgorithm::Eip191,
214                did: *did,
215            },
216            Self::BIP137(did) => AccountVerifier::Recoverable {
217                algorithm: SignatureAlgorithm::Bip137,
218                did: *did,
219            },
220            Self::Secp256r1(pk) => {
221                AccountVerifier::PublicKey(VerificationPublicKey::Secp256r1(*pk))
222            }
223            Self::Ed25519(pk) => AccountVerifier::PublicKey(VerificationPublicKey::Ed25519(*pk)),
224            Self::Bls12381(pk) => AccountVerifier::PublicKey(VerificationPublicKey::Bls12381(*pk)),
225        }
226    }
227}
228
229// A SessionSk can be converted to a string using JSON and then encoded with base58.
230// To load the SessionSk from a string, use `SessionSk::from_str`.
231impl FromStr for SessionSk {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self> {
235        let s = base58_monero::decode_check(s).map_err(|_| Error::Decode)?;
236        let session_sk: SessionSk = serde_json::from_slice(&s).map_err(Error::Deserialize)?;
237        Ok(session_sk)
238    }
239}
240
241#[wasm_export]
242impl SessionSkBuilder {
243    /// Create a new SessionSkBuilder.
244    /// The "account_type" is lower case of [Account] variant.
245    /// The "account_entity" refers to the entity that is encapsulated by the [Account] variant, in string format.
246    pub fn new(account_entity: String, account_type: String) -> SessionSkBuilder {
247        let sk = SecretKey::random();
248        Self {
249            sk,
250            account_entity,
251            account_type,
252            ttl_ms: DEFAULT_SESSION_TTL_MS,
253            ts_ms: utils::get_epoch_ms(),
254            sig: vec![],
255        }
256    }
257
258    /// This is a helper method to let user know if the account params is valid.
259    pub fn validate_account(&self) -> bool {
260        Account::try_from((self.account_entity.clone(), self.account_type.clone()))
261            .map_err(|e| {
262                tracing::warn!("validate_account error: {:?}", e);
263                e
264            })
265            .is_ok()
266    }
267
268    /// Construct unsigned_info string for signing.
269    pub fn unsigned_proof(&self) -> String {
270        pack_session(self.sk.address().into(), self.ts_ms, self.ttl_ms)
271    }
272
273    /// Set the signature of session that signed by account.
274    pub fn set_session_sig(mut self, sig: Vec<u8>) -> Self {
275        self.sig = sig;
276        self
277    }
278
279    /// Set the lifetime of session.
280    pub fn set_ttl(mut self, ttl_ms: u64) -> Self {
281        self.ttl_ms = ttl_ms;
282        self
283    }
284
285    /// Build the [SessionSk].
286    pub fn build(self) -> Result<SessionSk> {
287        let account = Account::try_from((self.account_entity, self.account_type))?;
288        let session = Session {
289            session_id: self.sk.address().into(),
290            account,
291            ttl_ms: self.ttl_ms,
292            ts_ms: self.ts_ms,
293            sig: self.sig,
294        };
295
296        session.verify_self()?;
297
298        Ok(SessionSk {
299            session,
300            sk: self.sk,
301        })
302    }
303}
304
305impl Session {
306    /// Pack the session into a string for verification or public key recovery.
307    pub fn pack(&self) -> Vec<u8> {
308        pack_session(self.session_id, self.ts_ms, self.ttl_ms)
309            .as_bytes()
310            .to_vec()
311    }
312
313    /// Check session is expired or not.
314    pub fn is_expired(&self) -> bool {
315        let now = utils::get_epoch_ms();
316        now > self.ts_ms + self.ttl_ms as u128
317    }
318
319    /// Verify session.
320    pub fn verify_self(&self) -> Result<()> {
321        if self.is_expired() {
322            return Err(Error::SessionExpired);
323        }
324
325        let auth_bytes = self.pack();
326
327        if !self
328            .account
329            .account_verifier()
330            .verify(&auth_bytes, &self.sig)
331        {
332            return Err(Error::VerifySignatureFailed);
333        }
334
335        Ok(())
336    }
337
338    /// Verify message.
339    pub fn verify(&self, msg: &[u8], sig: impl AsRef<[u8]>) -> Result<()> {
340        self.verify_self()?;
341        if !signers::secp256k1::verify(msg, &self.session_id, sig) {
342            return Err(Error::VerifySignatureFailed);
343        }
344        Ok(())
345    }
346
347    /// Get legacy secp256k1-compatible account public key.
348    ///
349    /// Use `account_verification_pubkey` for typed account verification keys.
350    pub fn account_pubkey(&self) -> Result<PublicKey<33>> {
351        match self.account_verification_pubkey()? {
352            VerificationPublicKey::Secp256k1(pk)
353            | VerificationPublicKey::Eip191(pk)
354            | VerificationPublicKey::Bip137(pk) => Ok(pk),
355            VerificationPublicKey::Secp256r1(_)
356            | VerificationPublicKey::Ed25519(_)
357            | VerificationPublicKey::Bls12381(_) => Err(Error::UnknownAccount),
358        }
359    }
360
361    /// Get typed account verification public key from session proof.
362    pub fn account_verification_pubkey(&self) -> Result<VerificationPublicKey> {
363        self.account
364            .account_verifier()
365            .verification_key_from_signature(&self.pack(), &self.sig)
366    }
367
368    /// Get typed account verifier.
369    pub fn account_verifier(&self) -> AccountVerifier {
370        self.account.account_verifier()
371    }
372
373    /// Get account did.
374    pub fn account_did(&self) -> Did {
375        self.account.account_verifier().did()
376    }
377}
378
379impl SessionSk {
380    /// Generate Session with private key. Only use it for unittest.
381    /// To protect your private key, please use [SessionSkBuilder] to generate session.
382    pub fn new_with_seckey(key: &SecretKey) -> Result<Self> {
383        let account_entity = Did::from(key.address()).to_string();
384        let account_type = "secp256k1".to_string();
385
386        let mut builder = SessionSkBuilder::new(account_entity, account_type);
387
388        let sig = key.sign(&builder.unsigned_proof());
389        builder = builder.set_session_sig(sig.to_vec());
390
391        builder.build()
392    }
393
394    /// Get session from SessionSk.
395    pub fn session(&self) -> Session {
396        self.session.clone()
397    }
398
399    /// Sign message with session.
400    pub fn sign(&self, msg: &[u8]) -> Result<Vec<u8>> {
401        let key = self.sk;
402        let h = keccak256(msg);
403        Ok(signers::secp256k1::sign(key, &h).to_vec())
404    }
405
406    /// Get account did from session.
407    pub fn account_did(&self) -> Did {
408        self.session.account_did()
409    }
410
411    /// Get typed account verifier from session.
412    pub fn account_verifier(&self) -> AccountVerifier {
413        self.session.account_verifier()
414    }
415
416    /// Dump session_sk to string, allowing user to save it in a config file.
417    /// It can be restored using `SessionSk::from_str`.
418    pub fn dump(&self) -> Result<String> {
419        let s = serde_json::to_string(&self).map_err(|_| Error::SerializeError)?;
420        base58_monero::encode_check(s.as_bytes()).map_err(|_| Error::Encode)
421    }
422}
423
424#[cfg(test)]
425mod test {
426    use super::*;
427    use crate::ecc::keys::SigningSecretKey;
428
429    #[test]
430    pub fn test_session_verify() {
431        let key = SecretKey::random();
432        let sm = SessionSk::new_with_seckey(&key).unwrap();
433        let session = sm.session();
434        assert!(session.verify_self().is_ok());
435    }
436
437    #[test]
438    pub fn test_account_pubkey() {
439        let key = SecretKey::random();
440        let sm = SessionSk::new_with_seckey(&key).unwrap();
441        let session = sm.session();
442        let pubkey = session.account_pubkey().unwrap();
443        assert_eq!(key.pubkey(), pubkey);
444    }
445
446    #[test]
447    pub fn test_session_verify_secp256r1_account_key() {
448        let account_entity = "17a6afd392fcbe4ac9270a599a9c5732c4f838ce35ea2234d389d8f0c367f3f5dcab906352e27289002c7f2c96039ddce7c1b5aad8b87ba94984d4c8b4f95702";
449        let account_key = VerificationPublicKey::Secp256r1(
450            PublicKey::<33>::from_hex_string(account_entity).unwrap(),
451        );
452        let signing_key =
453            SecretKey::try_from("2544acda37415a476d42312969926dc48e529867036cec71922d4177ea9c1038")
454                .unwrap();
455        let mut builder =
456            SessionSkBuilder::new(account_entity.to_string(), "secp256r1".to_string());
457        let proof = builder.unsigned_proof();
458        let sig =
459            signers::secp256r1::sign(signing_key, &signers::secp256r1::hash(proof.as_bytes()))
460                .unwrap();
461        builder = builder.set_session_sig(sig.to_vec());
462
463        let session = builder.build().unwrap().session();
464        assert_eq!(session.account_verification_pubkey().unwrap(), account_key);
465        assert_eq!(session.account_did(), account_key.did());
466        assert!(session.verify_self().is_ok());
467        assert!(session.account_pubkey().is_err());
468    }
469
470    #[test]
471    pub fn test_session_rejects_invalid_secp256r1_account_key() {
472        let mut invalid_key = None;
473        for i in 0u8..=u8::MAX {
474            let mut key = [0u8; 33];
475            key[0] = 2;
476            key[32] = i;
477            let public_key = PublicKey(key);
478            let verifying_key = public_key.ct_try_into_secp256r1_pubkey();
479            if !bool::from(verifying_key.is_some()) || verifying_key.unwrap().is_err() {
480                invalid_key = Some(key);
481                break;
482            }
483        }
484        let account_entity = hex::encode(invalid_key.expect("at least one invalid P-256 x"));
485        let builder = SessionSkBuilder::new(account_entity, "secp256r1".to_string())
486            .set_session_sig(vec![0u8; 64]);
487
488        assert!(!builder.validate_account());
489        assert!(builder.build().is_err());
490    }
491
492    #[test]
493    pub fn test_session_verify_bls12381_account_key() {
494        let signing_key = SigningSecretKey::random_bls12381().unwrap();
495        let account_key = signing_key.public_key().unwrap();
496        let VerificationPublicKey::Bls12381(raw_account_key) = account_key else {
497            unreachable!("random_bls12381 returns a BLS verification key");
498        };
499        let account_entity = base58_monero::encode_check(&raw_account_key.0).unwrap();
500        let mut builder = SessionSkBuilder::new(account_entity, "bls12-381".to_string());
501        let proof = builder.unsigned_proof();
502        builder = builder.set_session_sig(signing_key.sign_raw(proof.as_bytes()).unwrap());
503
504        let session = builder.build().unwrap().session();
505        assert_eq!(
506            session.account_verification_pubkey().unwrap(),
507            VerificationPublicKey::Bls12381(raw_account_key)
508        );
509        assert_eq!(session.account_did(), account_key.did());
510        assert!(session.verify_self().is_ok());
511        assert!(session.account_pubkey().is_err());
512    }
513
514    #[test]
515    pub fn test_session_verify_ed25519_account_key() {
516        let signing_key = SigningSecretKey::random_ed25519();
517        let account_key = signing_key.public_key().unwrap();
518        let VerificationPublicKey::Ed25519(raw_account_key) = account_key else {
519            unreachable!("random_ed25519 returns an Ed25519 verification key");
520        };
521        let account_entity = raw_account_key.to_base58_string().unwrap();
522        let mut builder = SessionSkBuilder::new(account_entity, "ed25519".to_string());
523        let proof = builder.unsigned_proof();
524        builder = builder.set_session_sig(signing_key.sign_raw(proof.as_bytes()).unwrap());
525
526        let session = builder.build().unwrap().session();
527        assert_eq!(
528            session.account_verification_pubkey().unwrap(),
529            VerificationPublicKey::Ed25519(raw_account_key)
530        );
531        assert_eq!(session.account_did(), account_key.did());
532        assert!(session.verify_self().is_ok());
533        assert!(session.account_pubkey().is_err());
534    }
535
536    #[test]
537    pub fn test_dump_restore() {
538        let key = SecretKey::random();
539        let sm = SessionSk::new_with_seckey(&key).unwrap();
540        let dump = sm.dump().unwrap();
541        let sm2 = SessionSk::from_str(&dump).unwrap();
542        assert_eq!(sm, sm2);
543    }
544}