ic_siwe/
login.rs

1use std::fmt;
2
3use candid::{CandidType, Principal};
4use serde::Deserialize;
5use serde_bytes::ByteBuf;
6use simple_asn1::ASN1EncodeErr;
7
8use crate::{
9    delegation::{
10        create_delegation, create_delegation_hash, create_user_canister_pubkey, generate_seed,
11        DelegationError,
12    },
13    eth::{recover_eth_address, EthAddress, EthError, EthSignature},
14    hash,
15    rand::generate_nonce,
16    settings::Settings,
17    signature_map::SignatureMap,
18    siwe::{SiweMessage, SiweMessageError},
19    time::get_current_time,
20    with_settings, SIWE_MESSAGES,
21};
22
23const MAX_SIGS_TO_PRUNE: usize = 10;
24
25/// This function is the first step of the user login process. It validates the provided Ethereum address,
26/// creates a SIWE message, saves it for future use, and returns it.
27///
28/// # Parameters
29/// * `address`: A [`crate::eth::EthAddress`] representing the user's Ethereum address. This address is
30///   validated and used to create the SIWE message.
31///
32/// # Returns
33/// A `Result` that, on success, contains a [`crate::siwe::SiweMessage`] and its `nonce`. The `nonce` is used in
34/// the login function to prevent replay and ddos attacks.
35///
36/// # Example
37/// ```ignore
38/// use ic_siwe::{
39///   login::prepare_login,
40///   eth::EthAddress
41/// };
42///
43/// let address = EthAddress::new("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").unwrap();
44/// let (message, nonce) = prepare_login(&address).unwrap();
45/// ```
46pub fn prepare_login(address: &EthAddress) -> Result<(SiweMessage, String), EthError> {
47    let nonce = generate_nonce();
48    let message = SiweMessage::new(address, &nonce);
49
50    // Save the SIWE message for use in the login call
51    SIWE_MESSAGES.with_borrow_mut(|siwe_messages| {
52        siwe_messages.insert(message.clone(), address, &nonce);
53    });
54
55    Ok((message, nonce))
56}
57/// Login details are returned after a successful login. They contain the expiration time of the
58/// delegation and the user canister public key.
59#[derive(Clone, Debug, CandidType, Deserialize)]
60pub struct LoginDetails {
61    /// The session expiration time in nanoseconds since the UNIX epoch. This is the time at which
62    /// the delegation will no longer be valid.
63    pub expiration: u64,
64
65    /// The user canister public key. This key is used to derive the user principal.
66    pub user_canister_pubkey: ByteBuf,
67}
68
69pub enum LoginError {
70    EthError(EthError),
71    SiweMessageError(SiweMessageError),
72    AddressMismatch,
73    DelegationError(DelegationError),
74    ASN1EncodeErr(ASN1EncodeErr),
75}
76
77impl From<EthError> for LoginError {
78    fn from(err: EthError) -> Self {
79        LoginError::EthError(err)
80    }
81}
82
83impl From<SiweMessageError> for LoginError {
84    fn from(err: SiweMessageError) -> Self {
85        LoginError::SiweMessageError(err)
86    }
87}
88
89impl From<DelegationError> for LoginError {
90    fn from(err: DelegationError) -> Self {
91        LoginError::DelegationError(err)
92    }
93}
94
95impl From<ASN1EncodeErr> for LoginError {
96    fn from(err: ASN1EncodeErr) -> Self {
97        LoginError::ASN1EncodeErr(err)
98    }
99}
100
101impl fmt::Display for LoginError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            LoginError::EthError(e) => write!(f, "{}", e),
105            LoginError::SiweMessageError(e) => write!(f, "{}", e),
106            LoginError::AddressMismatch => write!(f, "Recovered address does not match"),
107            LoginError::DelegationError(e) => write!(f, "{}", e),
108            LoginError::ASN1EncodeErr(e) => write!(f, "{}", e),
109        }
110    }
111}
112
113/// Handles the second step of the user login process. It verifies the signature against the SIWE message,
114/// creates a delegation for the session, adds it to the signature map, and returns login details
115///
116/// # Parameters
117/// * `signature`: The SIWE message signature to verify.
118/// * `address`: The Ethereum address used to sign the SIWE message.
119/// * `session_key`: A unique session key to be used for the delegation.
120/// * `signature_map`: A mutable reference to `SignatureMap` to which the delegation hash will be added
121///   after successful validation.
122/// * `canister_id`: The principal of the canister performing the login.
123/// * `nonce`: The nonce generated during the `prepare_login` call.
124///
125/// # Returns
126/// A `Result` that, on success, contains the [LoginDetails] with session expiration and user canister
127/// public key, or an error string on failure.
128pub fn login(
129    signature: &EthSignature,
130    address: &EthAddress,
131    session_key: ByteBuf,
132    signature_map: &mut SignatureMap,
133    canister_id: &Principal,
134    nonce: &str,
135) -> Result<LoginDetails, LoginError> {
136    // Remove expired SIWE messages from the state before proceeding. The init settings determines
137    // the time to live for SIWE messages.
138    SIWE_MESSAGES.with_borrow_mut(|siwe_messages| {
139        // Prune any expired SIWE messages from the state.
140        siwe_messages.prune_expired();
141
142        // Get the previously created SIWE message for current address. If it has expired or does not
143        // exist, return an error.
144        let message = siwe_messages.get(address, nonce)?;
145        let message_string: String = message.clone().into();
146
147        // Verify the supplied signature against the SIWE message and recover the Ethereum address
148        // used to sign the message.
149        let result = match recover_eth_address(&message_string, signature) {
150            Ok(recovered_address) => {
151                if recovered_address != address.as_str() {
152                    Err(LoginError::AddressMismatch)
153                } else {
154                    Ok(())
155                }
156            }
157            Err(e) => Err(LoginError::EthError(e)),
158        };
159
160        // Ensure the SIWE message is removed from the state both on success and on failure.
161        siwe_messages.remove(address, nonce);
162
163        // Handle the result of the signature verification.
164        result?;
165
166        // The delegation is valid for the duration of the session as defined in the settings.
167        let expiration = with_settings!(|settings: &Settings| {
168            message
169                .issued_at
170                .saturating_add(settings.session_expires_in)
171        });
172
173        // The seed is what uniquely identifies the delegation. It is derived from the salt, the
174        // Ethereum address and the SIWE message URI.
175        let seed = generate_seed(address);
176
177        // Before adding the signature to the signature map, prune any expired signatures.
178        signature_map.prune_expired(get_current_time(), MAX_SIGS_TO_PRUNE);
179
180        // Create the delegation and add its hash to the signature map. The seed is used as the map key.
181        let delegation = create_delegation(session_key, expiration)?;
182        let delegation_hash = create_delegation_hash(&delegation);
183        signature_map.put(hash::hash_bytes(seed), delegation_hash);
184
185        // Create the user canister public key from the seed. From this key, the client can derive the
186        // user principal.
187        let user_canister_pubkey = create_user_canister_pubkey(canister_id, seed.to_vec())?;
188
189        Ok(LoginDetails {
190            expiration,
191            user_canister_pubkey: ByteBuf::from(user_canister_pubkey),
192        })
193    })
194}