mtv_crypto/
authenticator.rs

1use chrono::{DateTime, Utc};
2use futures::future::BoxFuture;
3use thiserror::Error;
4use web3::{
5    signing::{hash_message, recover, RecoveryError},
6    Error as Web3Error, RequestId, Transport, Web3,
7};
8
9use crate::{
10    account::{Address, PersonalSignature},
11    chain::{AuthChain, AuthLink},
12    util::{rpc_call_is_valid_signature, RPCCallError},
13    Identity,
14};
15
16#[derive(Debug, Error, PartialEq)]
17pub enum AuthenticatorError {
18    #[error("malformed authchain: expecting at least 2 links, but is empty")]
19    EmptyChain,
20
21    #[error("malformed authchain: expecting SIGNER at position {position}, but found {found}")]
22    SignerExpected { position: usize, found: String },
23
24    #[error("malformed authchain: expecting ECDSA_EPHEMERAL or ECDSA_EIP_1654_EPHEMERAL at position {position}, but found {found}")]
25    EphemeralExpected { position: usize, found: String },
26
27    #[error("malformed authchain: expecting ECDSA_SIGNED_ENTITY or ECDSA_EIP_1654_SIGNED_ENTITY at position {position}, but found {found}")]
28    SignedEntityExpected { position: usize, found: String },
29
30    #[error("malformed authchain: expecting ECDSA_SIGNED_ENTITY or ECDSA_EIP_1654_SIGNED_ENTITY")]
31    SignedEntityMissing,
32
33    #[error("fail to validate {kind} at position {position}: {message}")]
34    ValidationError {
35        position: usize,
36        kind: String,
37        message: String,
38    },
39
40    #[error("unexpected authority at position {position}: expected {expected}, but found {found}")]
41    UnexpectedSigner {
42        position: usize,
43        expected: Address,
44        found: Address,
45    },
46
47    #[error(
48        "unexpected last authority at position {position}: expected {expected}, but found {found}"
49    )]
50    UnexpectedLastAuthority {
51        position: usize,
52        expected: String,
53        found: String,
54    },
55
56    #[error("expired entity {kind} at position {position}")]
57    ExpiredEntity { position: usize, kind: String },
58}
59
60#[derive(Debug, Clone)]
61pub struct WithoutTransport {}
62impl Transport for WithoutTransport {
63    type Out = BoxFuture<'static, Result<serde_json::Value, Web3Error>>;
64
65    fn prepare(
66        &self,
67        _method: &str,
68        _params: Vec<serde_json::Value>,
69    ) -> (usize, jsonrpc_core::types::request::Call) {
70        unimplemented!()
71    }
72
73    fn send(&self, _id: RequestId, _request: jsonrpc_core::Call) -> Self::Out {
74        unimplemented!()
75    }
76}
77
78/// Validates a message and has correspond to an address.
79///
80/// ```rust
81/// use mtv_crypto::authenticator::Authenticator;
82/// use mtv_crypto::account::Address;
83/// use mtv_crypto::chain::AuthChain;
84///
85/// # tokio_test::block_on(async {
86///     let authenticator = Authenticator::new();
87///
88///     let chain = AuthChain::from_json(r#"[
89///        {
90///            "type": "SIGNER",
91///            "payload": "0x13FE90239bfda363eC33a849b716616958c04f0F",
92///            "signature": ""
93///        },
94///        {
95///            "type": "ECDSA_EPHEMERAL",
96///            "payload": "Memetaverse Login\nEphemeral address: 0x68560651BD91509EB22b90f6F748422A26CA3425\nExpiration: 8020-07-29T01:23:42.610Z",
97///            "signature": "0xed50177b6d0607ef94e4dd507e99387624dfd480d781073ba7462240edfc930b64347f1e46d5db22bcd55f7e94c788415a0ac195928fb308b728d57c1c9b93b41c"
98///        },
99///        {
100///            "type": "ECDSA_SIGNED_ENTITY",
101///            "payload": "test",
102///            "signature": "0xc2542c9344635322aab27d0acf94c4d27fde0ac9d8ba4e760312703d6aeb519f5ed23c3291247375d74c4a7f057b75ec76ae8bdf25a499279044cc04bda159201c"
103///        }
104///     ]"#).unwrap();
105///
106///     let address =  Address::try_from("0x13fe90239bfda363ec33a849b716616958c04f0f").unwrap();
107///     let owner =  chain.owner().unwrap();
108///     let result = authenticator.verify_signature(&chain, "test").await.unwrap();
109///     assert_eq!(result, &address);
110///     assert_eq!(result, owner);
111/// # })
112/// ```
113pub struct Authenticator<T> {
114    transport: Option<T>,
115}
116
117impl Authenticator<()> {
118    pub fn new() -> Authenticator<WithoutTransport> {
119        Authenticator { transport: None }
120    }
121
122    pub fn with_transport<T: Transport>(transport: T) -> Authenticator<T> {
123        Authenticator {
124            transport: Some(transport),
125        }
126    }
127}
128
129impl Authenticator<WithoutTransport> {
130    pub fn add_transport<T: Transport>(&self, transport: T) -> Authenticator<T> {
131        Authenticator {
132            transport: Some(transport),
133        }
134    }
135}
136
137impl<T: Transport> Authenticator<T> {
138    async fn validate_eip1654(
139        &self,
140        address: Address,
141        message: String,
142        hash: Vec<u8>,
143    ) -> Result<bool, RPCCallError> {
144        if let Some(transport) = &self.transport {
145            rpc_call_is_valid_signature(&Web3::new(transport).eth(), address, message, hash).await
146        } else {
147            Err(RPCCallError::NotImplemented)
148        }
149    }
150    /// Validates a message and has correspond to an address.
151    ///
152    /// ```
153    /// use mtv_crypto::authenticator::Authenticator;
154    /// use mtv_crypto::account::{Address, PersonalSignature};
155    ///
156    /// # tokio_test::block_on(async {
157    ///     let address = Address::try_from("0x13FE90239bfda363eC33a849b716616958c04f0F").unwrap();
158    ///     let message = "Memetaverse Login\nEphemeral address: 0x68560651BD91509EB22b90f6F748422A26CA3425\nExpiration: 8020-07-28T11:11:08.989Z";
159    ///     let hash = PersonalSignature::try_from("0xaf237ccf4690d21c671c03a65aae48d1cbf94afdcfa3ed7569c20eb5769f2e273d4249c19e4a138acf063e5dff2b925e78a6c057fa864737ebaf65d9f1f464741c").unwrap().to_vec();
160    ///
161    ///     let result = Authenticator::new().validate_personal(&address, &message, &hash).unwrap();
162    ///     assert_eq!(result, true);
163    /// # })
164    /// ```
165    pub fn validate_personal<M: AsRef<[u8]>>(
166        &self,
167        address: &Address,
168        message: M,
169        hash: &[u8],
170    ) -> Result<bool, RecoveryError> {
171        if hash.len() != 65 {
172            return Err(RecoveryError::InvalidSignature);
173        }
174
175        let signature = &hash[..64];
176        let recovery_id = &hash[64];
177
178        let recovery_number = (recovery_id - 27) as i32;
179        let h160 = recover(hash_message(message).as_bytes(), signature, recovery_number)?;
180
181        Ok(address == h160)
182    }
183
184    /// Verifies that and authlink is a signer and returns it as result. Otherwise, returns an error.
185    async fn verify_signer<'a>(
186        &self,
187        link: &'a AuthLink,
188        position: usize,
189    ) -> Result<&'a Address, AuthenticatorError> {
190        match link {
191            AuthLink::Signer { payload, .. } => Ok(payload),
192            _ => Err(AuthenticatorError::SignerExpected {
193                position,
194                found: link.kind().to_string(),
195            }),
196        }
197    }
198
199    /// Verifies:
200    /// - the authlink is an ephemeral link (personal or eip1654)
201    /// - the ephemeral link is not expired
202    /// - the ephemeral payload is valid
203    /// - the ephemeral signature corresponds to the authority
204    ///
205    /// returns the address defined in the ephemeral payload, otherwise returns an error.
206    async fn verify_ephemeral<'a, 'l, 'd>(
207        &self,
208        authority: &'a Address,
209        link: &'l AuthLink,
210        expiration: &'d DateTime<Utc>,
211        position: usize,
212    ) -> Result<&'l Address, AuthenticatorError> {
213        match link {
214            AuthLink::EcdsaPersonalEphemeral { payload, signature } => {
215                if payload.is_expired_at(expiration) {
216                    return Err(AuthenticatorError::ExpiredEntity {
217                        position,
218                        kind: link.kind().to_string(),
219                    });
220                }
221
222                let result = self
223                    .validate_personal(authority, payload.to_string(), signature.as_ref())
224                    .map_err(|err| AuthenticatorError::ValidationError {
225                        position,
226                        kind: link.kind().to_string(),
227                        message: err.to_string(),
228                    })?;
229
230                if !result {
231                    return Err(AuthenticatorError::ValidationError {
232                        position,
233                        kind: link.kind().to_string(),
234                        message: format!(
235                            "Signature {} couldn't be validated against address {}",
236                            signature, authority
237                        ),
238                    });
239                }
240
241                Ok(&payload.address)
242            }
243            AuthLink::EcdsaEip1654Ephemeral { payload, signature } => {
244                if payload.is_expired_at(expiration) {
245                    return Err(AuthenticatorError::ExpiredEntity {
246                        position,
247                        kind: link.kind().to_string(),
248                    });
249                }
250
251                let result = self
252                    .validate_eip1654(*authority, payload.to_string(), signature.to_vec())
253                    .await
254                    .map_err(|err| AuthenticatorError::ValidationError {
255                        position,
256                        kind: link.kind().to_string(),
257                        message: err.to_string(),
258                    })?;
259
260                if !result {
261                    return Err(AuthenticatorError::ValidationError {
262                        position,
263                        kind: link.kind().to_string(),
264                        message: format!(
265                            "Signature {} couldn't be validated against address {}",
266                            signature, authority
267                        ),
268                    });
269                }
270
271                Ok(&payload.address)
272            }
273            _ => Err(AuthenticatorError::EphemeralExpected {
274                position,
275                found: link.kind().to_string(),
276            }),
277        }
278    }
279
280    /// Verifies:
281    /// - the authlink is an ephemeral link (personal or eip1654)
282    /// - the ephemeral link is not expired
283    /// - the ephemeral signature corresponds to the authority
284    ///
285    /// returns the signed payload, otherwise returns an error.
286    async fn verify_signed_entity<'a>(
287        &self,
288        authority: &'a Address,
289        link: &'a AuthLink,
290        position: usize,
291    ) -> Result<&'a str, AuthenticatorError> {
292        match link {
293            AuthLink::EcdsaPersonalSignedEntity { payload, signature } => {
294                let result = self
295                    .validate_personal(authority, payload, signature.as_ref())
296                    .map_err(|err| AuthenticatorError::ValidationError {
297                        position,
298                        kind: link.kind().to_string(),
299                        message: err.to_string(),
300                    })?;
301
302                if !result {
303                    return Err(AuthenticatorError::ValidationError {
304                        position,
305                        kind: link.kind().to_string(),
306                        message: format!(
307                            "Signature {} couldn't be validated against address {}",
308                            signature, authority
309                        ),
310                    });
311                }
312
313                Ok(payload)
314            }
315            AuthLink::EcdsaEip1654SignedEntity { payload, signature } => {
316                let result = self
317                    .validate_eip1654(*authority, payload.to_string(), signature.to_vec())
318                    .await
319                    .map_err(|err| AuthenticatorError::ValidationError {
320                        position,
321                        kind: link.kind().to_string(),
322                        message: err.to_string(),
323                    })?;
324
325                if !result {
326                    return Err(AuthenticatorError::ValidationError {
327                        position,
328                        kind: link.kind().to_string(),
329                        message: format!(
330                            "Signature {} couldn't be validated against address {}",
331                            signature, authority
332                        ),
333                    });
334                }
335
336                Ok(payload)
337            }
338            _ => Err(AuthenticatorError::SignedEntityExpected {
339                position,
340                found: link.kind().to_string(),
341            }),
342        }
343    }
344
345    /// Verifies and authchain is valid, not expired at a given date and corresponds to the last_authority, otherwise, returns an error.
346    pub async fn verify_signature_at<'a>(
347        &self,
348        chain: &'a AuthChain,
349        last_authority: &str,
350        expiration: &DateTime<Utc>,
351    ) -> Result<&'a Address, AuthenticatorError> {
352        let owner = match chain.first() {
353            Some(link) => self.verify_signer(link, 0).await?,
354            None => return Err(AuthenticatorError::EmptyChain),
355        };
356
357        let len = chain.len();
358        let mut latest_authority = owner;
359        for (position, link) in chain.iter().enumerate().skip(1) {
360            // is not the last link
361            if position != len - 1 {
362                latest_authority = self
363                    .verify_ephemeral(latest_authority, link, expiration, position)
364                    .await?;
365
366                // is the last link
367            } else {
368                let signed_message = self
369                    .verify_signed_entity(latest_authority, link, position)
370                    .await?;
371
372                if signed_message == last_authority {
373                    return Ok(owner);
374                } else {
375                    return Err(AuthenticatorError::UnexpectedLastAuthority {
376                        position,
377                        found: signed_message.to_string(),
378                        expected: last_authority.to_string(),
379                    });
380                }
381            }
382        }
383
384        Err(AuthenticatorError::SignedEntityMissing)
385    }
386
387    /// Verifies and authchain is valid, not expired and corresponds to the last_authority, otherwise, returns an error.
388    pub async fn verify_signature<'a>(
389        &self,
390        chain: &'a AuthChain,
391        last_authority: &str,
392    ) -> Result<&'a Address, AuthenticatorError> {
393        let now = &Utc::now();
394        self.verify_signature_at(chain, last_authority, now).await
395    }
396
397    /// Creates a personal signature from a given identity and payload.
398    /// This method is intended to maintain parity with the [JS implementation](https://github.com/memetaverseproject/memetaverse-crypto/blob/680d7cceb52a75bfae38269005614e577f48561a/src/Authenticator.ts#L185).
399    pub fn create_signature<M: AsRef<str>>(
400        &self,
401        identity: &Identity,
402        payload: M,
403    ) -> PersonalSignature {
404        identity.create_signature(payload)
405    }
406
407    /// Creates an authchain from a given identity and payload.
408    /// This method is intended to maintain parity with the [JS implementation](https://github.com/memetaverseproject/memetaverse-crypto/blob/680d7cceb52a75bfae38269005614e577f48561a/src/Authenticator.ts#L171).
409    pub fn sign_payload<M: AsRef<str>>(&self, identity: &Identity, payload: M) -> AuthChain {
410        identity.sign_payload(payload)
411    }
412}
413
414#[cfg(test)]
415mod test {
416    use crate::account::{Account, Signer};
417
418    use super::*;
419
420    #[tokio::test]
421    async fn test_should_validate_personal_signature() {
422        let authenticator = Authenticator::new();
423        let chain = AuthChain::from_json(r#"[
424                {
425                    "type": "SIGNER",
426                    "payload": "0x13FE90239bfda363eC33a849b716616958c04f0F",
427                    "signature": ""
428                },
429                {
430                    "type": "ECDSA_EPHEMERAL",
431                    "payload": "Memetaverse Login\nEphemeral address: 0x68560651BD91509EB22b90f6F748422A26CA3425\nExpiration: 8020-07-28T07:42:51.682Z",
432                    "signature": "0xea55d3fcf473eec78b240609e0f091f8c4f888fa03a4f4f42a1f27539dd355763677c56cbfb5f4cf77c9286d7782d39fbbf7eb4b9af9dab2f4d153707d497f591b"
433                },
434                {
435                    "type": "ECDSA_SIGNED_ENTITY",
436                    "payload": "test",
437                    "signature": "0xc2542c9344635322aab27d0acf94c4d27fde0ac9d8ba4e760312703d6aeb519f5ed23c3291247375d74c4a7f057b75ec76ae8bdf25a499279044cc04bda159201c"
438                }
439        ]"#).unwrap();
440
441        let owner = authenticator
442            .verify_signature(&chain, "test")
443            .await
444            .unwrap();
445        let expected = &Address::try_from("0x13fe90239bfda363ec33a849b716616958c04f0f").unwrap();
446        assert_eq!(owner, expected);
447    }
448
449    // #[tokio::test]
450    // async fn test_should_validate_eip_1654_signatures() {
451    //     let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
452    //     let transport = web3::transports::Http::new(&endpoint).unwrap();
453    //     let authenticator = Authenticator::with_transport(&transport);
454    //     let chain = AuthChain::from_json(r#"[
455    //         {
456    //           "type": "SIGNER",
457    //           "payload": "0x8C889222833F961FC991B31d15e25738c6732930",
458    //           "signature": ""
459    //         },
460    //         {
461    //           "type": "ECDSA_EIP_1654_EPHEMERAL",
462    //           "payload": "Memetaverse Login\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\nExpiration: 2123-03-18T16:59:36.515Z",
463    //           "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
464    //         },
465    //         {
466    //           "type": "ECDSA_SIGNED_ENTITY",
467    //           "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
468    //           "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
469    //         }
470    //       ]"#).unwrap();
471
472    //     let owner = authenticator
473    //         .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
474    //         .await
475    //         .unwrap();
476    //     let expected = &Address::try_from("0x8C889222833F961FC991B31d15e25738c6732930").unwrap();
477    //     assert_eq!(owner, expected);
478    // }
479
480    #[tokio::test]
481    async fn test_should_validate_simple_personal_signatures() {
482        let authenticator = Authenticator::new();
483        let signer = Address::try_from("0x68560651BD91509EB22b90f6F748422A26CA3425").unwrap();
484        let payload = "test";
485        let signature = "0xc2542c9344635322aab27d0acf94c4d27fde0ac9d8ba4e760312703d6aeb519f5ed23c3291247375d74c4a7f057b75ec76ae8bdf25a499279044cc04bda159201c";
486        let chain = AuthChain::simple(signer, payload, signature).unwrap();
487
488        let owner = authenticator
489            .verify_signature(&chain, "test")
490            .await
491            .unwrap();
492        let expected = &Address::try_from("0x68560651BD91509EB22b90f6F748422A26CA3425").unwrap();
493        assert_eq!(owner, expected);
494    }
495
496    // #[tokio::test]
497    // async fn test_should_validate_simple_eip_1654_signatures() {
498    //     let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
499    //     let transport = web3::transports::Http::new(&endpoint).unwrap();
500    //     let authenticator = Authenticator::with_transport(&transport);
501
502    //     let signer = Address::try_from("0x68560651BD91509EB22b90f6F748422A26CA3425").unwrap();
503    //     let payload = "test";
504    //     let signature = "0xed50177b6d0607ef94e4dd507e99387624dfd480d781073ba7462240edfc930b64347f1e46d5db22bcd55f7e94c788415a0ac195928fb308b728d57c1c9b93b41c";
505    //     let chain = AuthChain::simple(signer, payload, signature).unwrap();
506
507    //     let owner = authenticator
508    //         .verify_signature(&chain, "test")
509    //         .await
510    //         .unwrap();
511    //     let expected = &Address::try_from("0x68560651BD91509EB22b90f6F748422A26CA3425").unwrap();
512    //     assert_eq!(owner, expected);
513    // }
514
515    #[tokio::test]
516    async fn test_should_support_r_on_personal_signatures() {
517        let authenticator = Authenticator::new();
518        let chain = AuthChain::from_json(r#"[
519            {
520                "type": "SIGNER",
521                "payload": "0x13FE90239bfda363eC33a849b716616958c04f0F",
522                "signature": ""
523            },
524            {
525                "type": "ECDSA_EPHEMERAL",
526                "payload": "Memetaverse Login\nEphemeral address: 0x68560651BD91509EB22b90f6F748422A26CA3425\nExpiration: 8020-07-29T01:23:42.610Z",
527                "signature": "0xed50177b6d0607ef94e4dd507e99387624dfd480d781073ba7462240edfc930b64347f1e46d5db22bcd55f7e94c788415a0ac195928fb308b728d57c1c9b93b41c"
528            },
529            {
530                "type": "ECDSA_SIGNED_ENTITY",
531                "payload": "test",
532                "signature": "0xc2542c9344635322aab27d0acf94c4d27fde0ac9d8ba4e760312703d6aeb519f5ed23c3291247375d74c4a7f057b75ec76ae8bdf25a499279044cc04bda159201c"
533            }
534            ]"#).unwrap();
535
536        let owner = authenticator
537            .verify_signature(&chain, "test")
538            .await
539            .unwrap();
540        let expected = &Address::try_from("0x13FE90239bfda363eC33a849b716616958c04f0F").unwrap();
541        assert_eq!(owner, expected);
542    }
543
544    // #[tokio::test]
545    // async fn test_should_support_r_on_eip_1654_signatures() {
546    //     let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
547    //     let transport = web3::transports::Http::new(&endpoint).unwrap();
548    //     let authenticator = Authenticator::with_transport(&transport);
549    //     let chain = AuthChain::from_json(r#"[
550    //         {"type":"SIGNER","payload":"0x13FE90239bfda363eC33a849b716616958c04f0F","signature":""},
551    //         {
552    //           "type": "ECDSA_EIP_1654_EPHEMERAL",
553    //           "payload": "Memetaverse Login\r\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\r\nExpiration: 2123-03-18T16:59:36.515Z",
554    //           "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
555    //         },
556    //         {
557    //           "type": "ECDSA_SIGNED_ENTITY",
558    //           "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
559    //           "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
560    //         }
561    //       ]"#).unwrap();
562
563    //     let owner = authenticator
564    //         .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
565    //         .await
566    //         .unwrap();
567    //     let expected = &Address::try_from("0x8C889222833F961FC991B31d15e25738c6732930").unwrap();
568    //     assert_eq!(owner, expected);
569    // }
570
571    // #[tokio::test]
572    // async fn test_should_fail_it_tries_to_verify_a_eip_1654_signatures_without_a_transport() {
573    //     let chain = AuthChain::from_json(r#"[
574    //         {
575    //           "type": "SIGNER",
576    //           "payload": "0x8C889222833F961FC991B31d15e25738c6732930",
577    //           "signature": ""
578    //         },
579    //         {
580    //           "type": "ECDSA_EIP_1654_EPHEMERAL",
581    //           "payload": "Memetaverse Login\r\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\r\nExpiration: 2123-03-18T16:59:36.515Z",
582    //           "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
583    //         },
584    //         {
585    //           "type": "ECDSA_SIGNED_ENTITY",
586    //           "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
587    //           "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
588    //         }
589    //       ]"#).unwrap();
590
591    //     let authenticator = Authenticator::new();
592    //     let result = authenticator
593    //         .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
594    //         .await;
595
596    //     assert_eq!(result, Err(AuthenticatorError::ValidationError {
597    //         position: 1,
598    //         kind: "ECDSA_EIP_1654_EPHEMERAL".to_string(),
599    //         message: "rpc resolver not implemented".to_string()
600    //     }));
601    // }
602
603    #[tokio::test]
604    async fn test_should_fail_if_ephemeral_is_expired() {
605        let authenticator = Authenticator::new();
606        let chain = AuthChain::from_json(r#"[
607            {
608              "type": "SIGNER",
609              "payload": "0x13FE90239bfda363eC33a849b716616958c04f0F",
610              "signature": ""
611            },
612            {
613              "type": "ECDSA_EPHEMERAL",
614              "payload": "Memetaverse Login\nEphemeral address: 0x68560651BD91509EB22b90f6F748422A26CA3425\nExpiration: 2024-07-23T02:25:06.771Z",
615              "signature": "0xbde2dc9764a0f2aec6e320e0ad28c35c2582e88d31b66a0497cde58ac6d7ceb328f6123b2affeb1e4079f1fd178df711a41088054da174b5ccc80141c90db1c51b"
616            },
617            {
618              "type": "ECDSA_SIGNED_ENTITY",
619              "payload": "test",
620              "signature": "0xc2542c9344635322aab27d0acf94c4d27fde0ac9d8ba4e760312703d6aeb519f5ed23c3291247375d74c4a7f057b75ec76ae8bdf25a499279044cc04bda159201c"
621            }
622          ]"#).unwrap();
623
624        let result = authenticator
625            .verify_signature(&chain, "test")
626            .await;
627
628        assert_eq!(
629            result,
630            Err(AuthenticatorError::ExpiredEntity {
631                position: 1,
632                kind: String::from("ECDSA_EPHEMERAL")
633            })
634        );
635
636        let time = DateTime::parse_from_rfc3339("2020-01-01T00:00:00.000Z")
637            .unwrap()
638            .with_timezone(&Utc);
639        let owner = authenticator
640            .verify_signature_at(
641                &chain,
642                "test",
643                &time,
644            )
645            .await
646            .unwrap();
647
648        let expected = &Address::try_from("0x13FE90239bfda363eC33a849b716616958c04f0F").unwrap();
649        assert_eq!(owner, expected);
650    }
651
652    #[tokio::test]
653    async fn test_should_recover_address_from_signature() {
654        let account = Account::random();
655        let payload = "QmWyFNeHbxXaPtUnzKvDZPpKSa4d5anZEZEFJ8TC1WgcfU";
656        let signature = account.sign(payload);
657        let authenticator = Authenticator::new();
658        let result =
659            authenticator.validate_personal(&account.address(), payload, &signature.to_vec());
660
661        assert!(result.is_ok());
662        assert!(result.unwrap());
663
664        let chain = AuthChain::simple(account.address(), payload, signature.to_string()).unwrap();
665        let owner = authenticator
666            .verify_signature(&chain, payload)
667            .await
668            .unwrap();
669
670        assert_eq!(owner, &account.address());
671    }
672}