dcl_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 dcl_crypto::authenticator::Authenticator;
82/// use dcl_crypto::account::Address;
83/// use dcl_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": "0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34",
92///            "signature": ""
93///        },
94///        {
95///            "type": "ECDSA_EPHEMERAL",
96///            "payload": "Decentraland Login\r\nEphemeral address: 0xB80549D339DCe9834271EcF5F1F1bb141C70AbC2\r\nExpiration: 2123-03-20T12:36:25.522Z",
97///            "signature": "0x76bf8d3c8ee6798bd488c4bc7ac1298d0ad78759669be39876e63ccfd9af81e31b8c6d8000b892ed2d17eb2f5a2b56fc3edbbf33c6089d3e5148d83cc70ce9001c"
98///        },
99///        {
100///            "type": "ECDSA_SIGNED_ENTITY",
101///            "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
102///            "signature": "0xd71fb5511f7d9116d171a12754b2c6f4c795240bee982511049a14aba57f18684b48a08413ab00176801d773eab0436fff5d0c978877b6d05f483ee2ae36efb41b"
103///        }
104///     ]"#).unwrap();
105///
106///     let address =  Address::try_from("0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34").unwrap();
107///     let owner =  chain.owner().unwrap();
108///     let result = authenticator.verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo").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
151    /// Validates a message and has correspond to an address.
152    ///
153    /// ```
154    /// use dcl_crypto::authenticator::Authenticator;
155    /// use dcl_crypto::account::{Address, PersonalSignature};
156    ///
157    /// # tokio_test::block_on(async {
158    ///     let address = Address::try_from("0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34").unwrap();
159    ///     let message = "Decentraland Login\nEphemeral address: 0xe94944439fAB988e5e14b128BbcF6D5502b05f9C\nExpiration: 2020-02-20T00:00:00.000Z";
160    ///     let hash = PersonalSignature::try_from("0x2d45e2a3e9e04614cf6bb822951b849458a78037733202d4bda12e60ef1ff4d266b02af7b72caa232c45052520fd440869672da2b0966b29fff21638e3d21ca01b").unwrap().to_vec();
161    ///
162    ///     let result = Authenticator::new().validate_personal(&address, &message, &hash).unwrap();
163    ///     assert_eq!(result, true);
164    /// # })
165    /// ```
166    pub fn validate_personal<M: AsRef<[u8]>>(
167        &self,
168        address: &Address,
169        message: M,
170        hash: &[u8],
171    ) -> Result<bool, RecoveryError> {
172        if hash.len() != 65 {
173            return Err(RecoveryError::InvalidSignature);
174        }
175
176        let signature = &hash[..64];
177        let recovery_id = &hash[64];
178
179        let recovery_number = (recovery_id - 27) as i32;
180        let h160 = recover(hash_message(message).as_bytes(), signature, recovery_number)?;
181
182        Ok(address == h160)
183    }
184
185    /// Verifies that and authlink is a signer and returns it as result. Otherwise, returns an error.
186    async fn verify_signer<'a>(
187        &self,
188        link: &'a AuthLink,
189        position: usize,
190    ) -> Result<&'a Address, AuthenticatorError> {
191        match link {
192            AuthLink::Signer { payload, .. } => Ok(payload),
193            _ => Err(AuthenticatorError::SignerExpected {
194                position,
195                found: link.kind().to_string(),
196            }),
197        }
198    }
199
200    /// Verifies:
201    /// - the authlink is an ephemeral link (personal or eip1654)
202    /// - the ephemeral link is not expired
203    /// - the ephemeral payload is valid
204    /// - the ephemeral signature corresponds to the authority
205    ///
206    /// returns the address defined in the ephemeral payload, otherwise returns an error.
207    async fn verify_ephemeral<'a, 'l, 'd>(
208        &self,
209        authority: &'a Address,
210        link: &'l AuthLink,
211        expiration: &'d DateTime<Utc>,
212        position: usize,
213    ) -> Result<&'l Address, AuthenticatorError> {
214        match link {
215            AuthLink::EcdsaPersonalEphemeral { payload, signature } => {
216                if payload.is_expired_at(expiration) {
217                    return Err(AuthenticatorError::ExpiredEntity {
218                        position,
219                        kind: link.kind().to_string(),
220                    });
221                }
222
223                let result = self
224                    .validate_personal(authority, payload.to_string(), signature.as_ref())
225                    .map_err(|err| AuthenticatorError::ValidationError {
226                        position,
227                        kind: link.kind().to_string(),
228                        message: err.to_string(),
229                    })?;
230
231                if !result {
232                    return Err(AuthenticatorError::ValidationError {
233                        position,
234                        kind: link.kind().to_string(),
235                        message: format!(
236                            "Signature {} couldn't be validated against address {}",
237                            signature, authority
238                        ),
239                    });
240                }
241
242                Ok(&payload.address)
243            }
244            AuthLink::EcdsaEip1654Ephemeral { payload, signature } => {
245                if payload.is_expired_at(expiration) {
246                    return Err(AuthenticatorError::ExpiredEntity {
247                        position,
248                        kind: link.kind().to_string(),
249                    });
250                }
251
252                let result = self
253                    .validate_eip1654(*authority, payload.to_string(), signature.to_vec())
254                    .await
255                    .map_err(|err| AuthenticatorError::ValidationError {
256                        position,
257                        kind: link.kind().to_string(),
258                        message: err.to_string(),
259                    })?;
260
261                if !result {
262                    return Err(AuthenticatorError::ValidationError {
263                        position,
264                        kind: link.kind().to_string(),
265                        message: format!(
266                            "Signature {} couldn't be validated against address {}",
267                            signature, authority
268                        ),
269                    });
270                }
271
272                Ok(&payload.address)
273            }
274            _ => Err(AuthenticatorError::EphemeralExpected {
275                position,
276                found: link.kind().to_string(),
277            }),
278        }
279    }
280
281    /// Verifies:
282    /// - the authlink is an ephemeral link (personal or eip1654)
283    /// - the ephemeral link is not expired
284    /// - the ephemeral signature corresponds to the authority
285    ///
286    /// returns the signed payload, otherwise returns an error.
287    async fn verify_signed_entity<'a>(
288        &self,
289        authority: &'a Address,
290        link: &'a AuthLink,
291        position: usize,
292    ) -> Result<&'a str, AuthenticatorError> {
293        match link {
294            AuthLink::EcdsaPersonalSignedEntity { payload, signature } => {
295                let result = self
296                    .validate_personal(authority, payload, signature.as_ref())
297                    .map_err(|err| AuthenticatorError::ValidationError {
298                        position,
299                        kind: link.kind().to_string(),
300                        message: err.to_string(),
301                    })?;
302
303                if !result {
304                    return Err(AuthenticatorError::ValidationError {
305                        position,
306                        kind: link.kind().to_string(),
307                        message: format!(
308                            "Signature {} couldn't be validated against address {}",
309                            signature, authority
310                        ),
311                    });
312                }
313
314                Ok(payload)
315            }
316            AuthLink::EcdsaEip1654SignedEntity { payload, signature } => {
317                let result = self
318                    .validate_eip1654(*authority, payload.to_string(), signature.to_vec())
319                    .await
320                    .map_err(|err| AuthenticatorError::ValidationError {
321                        position,
322                        kind: link.kind().to_string(),
323                        message: err.to_string(),
324                    })?;
325
326                if !result {
327                    return Err(AuthenticatorError::ValidationError {
328                        position,
329                        kind: link.kind().to_string(),
330                        message: format!(
331                            "Signature {} couldn't be validated against address {}",
332                            signature, authority
333                        ),
334                    });
335                }
336
337                Ok(payload)
338            }
339            _ => Err(AuthenticatorError::SignedEntityExpected {
340                position,
341                found: link.kind().to_string(),
342            }),
343        }
344    }
345
346    /// Verifies and authchain is valid, not expired at a given date and corresponds to the last_authority, otherwise, returns an error.
347    pub async fn verify_signature_at<'a>(
348        &self,
349        chain: &'a AuthChain,
350        last_authority: &str,
351        expiration: &DateTime<Utc>,
352    ) -> Result<&'a Address, AuthenticatorError> {
353        let owner = match chain.first() {
354            Some(link) => self.verify_signer(link, 0).await?,
355            None => return Err(AuthenticatorError::EmptyChain),
356        };
357
358        let len = chain.len();
359        let mut latest_authority = owner;
360        for (position, link) in chain.iter().enumerate().skip(1) {
361            // is not the last link
362            if position != len - 1 {
363                latest_authority = self
364                    .verify_ephemeral(latest_authority, link, expiration, position)
365                    .await?;
366
367                // is the last link
368            } else {
369                let signed_message = self
370                    .verify_signed_entity(latest_authority, link, position)
371                    .await?;
372
373                if signed_message == last_authority {
374                    return Ok(owner);
375                } else {
376                    return Err(AuthenticatorError::UnexpectedLastAuthority {
377                        position,
378                        found: signed_message.to_string(),
379                        expected: last_authority.to_string(),
380                    });
381                }
382            }
383        }
384
385        Err(AuthenticatorError::SignedEntityMissing)
386    }
387
388    /// Verifies and authchain is valid, not expired and corresponds to the last_authority, otherwise, returns an error.
389    pub async fn verify_signature<'a>(
390        &self,
391        chain: &'a AuthChain,
392        last_authority: &str,
393    ) -> Result<&'a Address, AuthenticatorError> {
394        let now = &Utc::now();
395        self.verify_signature_at(chain, last_authority, now).await
396    }
397
398    /// Creates a personal signature from a given identity and payload.
399    /// This method is intended to maintain parity with the [JS implementation](https://github.com/decentraland/decentraland-crypto/blob/680d7cceb52a75bfae38269005614e577f48561a/src/Authenticator.ts#L185).
400    pub fn create_signature<M: AsRef<str>>(
401        &self,
402        identity: &Identity,
403        payload: M,
404    ) -> PersonalSignature {
405        identity.create_signature(payload)
406    }
407
408    /// Creates an authchain from a given identity and payload.
409    /// This method is intended to maintain parity with the [JS implementation](https://github.com/decentraland/decentraland-crypto/blob/680d7cceb52a75bfae38269005614e577f48561a/src/Authenticator.ts#L171).
410    pub fn sign_payload<M: AsRef<str>>(&self, identity: &Identity, payload: M) -> AuthChain {
411        identity.sign_payload(payload)
412    }
413}
414
415#[cfg(test)]
416mod test {
417    use crate::account::{Account, Signer};
418
419    use super::*;
420    use std::env;
421
422    #[tokio::test]
423    async fn test_should_validate_personal_signature() {
424        let authenticator = Authenticator::new();
425        let chain = AuthChain::from_json(r#"[
426            {
427              "type": "SIGNER",
428              "payload": "0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34",
429              "signature": ""
430            },
431            {
432              "type": "ECDSA_EPHEMERAL",
433              "payload": "Decentraland Login\nEphemeral address: 0xB80549D339DCe9834271EcF5F1F1bb141C70AbC2\nExpiration: 2123-03-20T12:36:25.522Z",
434              "signature": "0x76bf8d3c8ee6798bd488c4bc7ac1298d0ad78759669be39876e63ccfd9af81e31b8c6d8000b892ed2d17eb2f5a2b56fc3edbbf33c6089d3e5148d83cc70ce9001c"
435            },
436            {
437              "type": "ECDSA_SIGNED_ENTITY",
438              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
439              "signature": "0xd71fb5511f7d9116d171a12754b2c6f4c795240bee982511049a14aba57f18684b48a08413ab00176801d773eab0436fff5d0c978877b6d05f483ee2ae36efb41b"
440            }
441          ]"#).unwrap();
442
443        let owner = authenticator
444            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
445            .await
446            .unwrap();
447        let expected = &Address::try_from("0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34").unwrap();
448        assert_eq!(owner, expected);
449    }
450
451    #[tokio::test]
452    async fn test_should_validate_eip_1654_signatures() {
453        let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
454        let transport = web3::transports::Http::new(&endpoint).unwrap();
455        let authenticator = Authenticator::with_transport(&transport);
456        let chain = AuthChain::from_json(r#"[
457            {
458              "type": "SIGNER",
459              "payload": "0x8C889222833F961FC991B31d15e25738c6732930",
460              "signature": ""
461            },
462            {
463              "type": "ECDSA_EIP_1654_EPHEMERAL",
464              "payload": "Decentraland Login\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\nExpiration: 2123-03-18T16:59:36.515Z",
465              "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
466            },
467            {
468              "type": "ECDSA_SIGNED_ENTITY",
469              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
470              "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
471            }
472          ]"#).unwrap();
473
474        let owner = authenticator
475            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
476            .await
477            .unwrap();
478        let expected = &Address::try_from("0x8C889222833F961FC991B31d15e25738c6732930").unwrap();
479        assert_eq!(owner, expected);
480    }
481
482    #[tokio::test]
483    async fn test_should_validate_simple_personal_signatures() {
484        let authenticator = Authenticator::new();
485        let signer = Address::try_from("0xeC6E6c0841a2bA474E92Bf42BaF76bFe80e8657C").unwrap();
486        let payload = "QmWyFNeHbxXaPtUnzKvDZPpKSa4d5anZEZEFJ8TC1WgcfU";
487        let signature = "0xaaafb0368c13c42e401e71162cb55a062b3b0a5389e0740e7dc34e623b12f0fd65e2fadac51ab5f0de8f69b1311f23f1f218753e8a957043a2a789ba721141f91c";
488        let chain = AuthChain::simple(signer, payload, signature).unwrap();
489
490        let owner = authenticator
491            .verify_signature(&chain, "QmWyFNeHbxXaPtUnzKvDZPpKSa4d5anZEZEFJ8TC1WgcfU")
492            .await
493            .unwrap();
494        let expected = &Address::try_from("0xeC6E6c0841a2bA474E92Bf42BaF76bFe80e8657C").unwrap();
495        assert_eq!(owner, expected);
496    }
497
498    #[tokio::test]
499    async fn test_should_validate_simple_eip_1654_signatures() {
500        let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
501        let transport = web3::transports::Http::new(&endpoint).unwrap();
502        let authenticator = Authenticator::with_transport(&transport);
503
504        let signer = Address::try_from("0x6b7d7e82c984a0F4489c722fd11906F017f57704").unwrap();
505        let payload = "QmNUd7Cyoo9CREGsACkvBrQSb3KjhWX379FVsdjTCGsTAz";
506        let signature = "0x7fba0fbe75d0b28a224ec49ad99f6025f9055880db9ed1a35bc527a372c54ebe2461406aa07097bc47017da4319e19e517c49952697f074bcdc702f36afa72b01c759138c6ca4675367458884eb9b820c51af60a79efe1904ebcf2c1950fc7a2c02f3595a82ea1cc9d67a680c2f9b34df6abf5b344e857773dfe4210c6f85405151b";
507        let chain = AuthChain::simple(signer, payload, signature).unwrap();
508
509        let owner = authenticator
510            .verify_signature(&chain, "QmNUd7Cyoo9CREGsACkvBrQSb3KjhWX379FVsdjTCGsTAz")
511            .await
512            .unwrap();
513        let expected = &Address::try_from("0x6b7d7e82c984a0F4489c722fd11906F017f57704").unwrap();
514        assert_eq!(owner, expected);
515    }
516
517    #[tokio::test]
518    async fn test_should_support_r_on_personal_signatures() {
519        let authenticator = Authenticator::new();
520        let chain = AuthChain::from_json(r#"[
521            {
522              "type": "SIGNER",
523              "payload": "0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34",
524              "signature": ""
525            },
526            {
527              "type": "ECDSA_EPHEMERAL",
528              "payload": "Decentraland Login\r\nEphemeral address: 0xB80549D339DCe9834271EcF5F1F1bb141C70AbC2\r\nExpiration: 2123-03-20T12:36:25.522Z",
529              "signature": "0x76bf8d3c8ee6798bd488c4bc7ac1298d0ad78759669be39876e63ccfd9af81e31b8c6d8000b892ed2d17eb2f5a2b56fc3edbbf33c6089d3e5148d83cc70ce9001c"
530            },
531            {
532              "type": "ECDSA_SIGNED_ENTITY",
533              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
534              "signature": "0xd71fb5511f7d9116d171a12754b2c6f4c795240bee982511049a14aba57f18684b48a08413ab00176801d773eab0436fff5d0c978877b6d05f483ee2ae36efb41b"
535            }
536          ]"#).unwrap();
537
538        let owner = authenticator
539            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
540            .await
541            .unwrap();
542        let expected = &Address::try_from("0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34").unwrap();
543        assert_eq!(owner, expected);
544    }
545
546    #[tokio::test]
547    async fn test_should_support_r_on_eip_1654_signatures() {
548        let endpoint = env::var("ETHEREUM_MAINNET_RPC").unwrap();
549        let transport = web3::transports::Http::new(&endpoint).unwrap();
550        let authenticator = Authenticator::with_transport(&transport);
551        let chain = AuthChain::from_json(r#"[
552            {
553              "type": "SIGNER",
554              "payload": "0x8C889222833F961FC991B31d15e25738c6732930",
555              "signature": ""
556            },
557            {
558              "type": "ECDSA_EIP_1654_EPHEMERAL",
559              "payload": "Decentraland Login\r\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\r\nExpiration: 2123-03-18T16:59:36.515Z",
560              "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
561            },
562            {
563              "type": "ECDSA_SIGNED_ENTITY",
564              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
565              "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
566            }
567          ]"#).unwrap();
568
569        let owner = authenticator
570            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
571            .await
572            .unwrap();
573        let expected = &Address::try_from("0x8C889222833F961FC991B31d15e25738c6732930").unwrap();
574        assert_eq!(owner, expected);
575    }
576
577    #[tokio::test]
578    async fn test_should_fail_it_tries_to_verify_a_eip_1654_signatures_without_a_transport() {
579        let chain = AuthChain::from_json(r#"[
580            {
581              "type": "SIGNER",
582              "payload": "0x8C889222833F961FC991B31d15e25738c6732930",
583              "signature": ""
584            },
585            {
586              "type": "ECDSA_EIP_1654_EPHEMERAL",
587              "payload": "Decentraland Login\r\nEphemeral address: 0x4A1b9FD363dE915145008C41FA217377B2C223F2\r\nExpiration: 2123-03-18T16:59:36.515Z",
588              "signature": "0x00050203596af90cecdbf9a768886e771178fd5561dd27ab005d000100019dde76f11e2c6aff01f6548f3046a9d0c569e13e79dec4218322068d3123e1162167fabd84dccfaabd350b93d2405f7b8a9cef4846b4d9a55d17838809a0e2591b020101c50adeadb7fe15bee45dcb820610cdedcd314eb0030102640dccefda3685e6c0dbeb70c1cf8018c27077eb00021cfbe892a1b29ac5e2fda1038c7965656be94aec57b658582f16447089bcf50b09df216a7e21d861cd7474723a7bfc70bf1caa55a962476cf78eb4b54471018b1b020103d9e87370ededc599df3bf9dd0e48586005f1a1bb"
589            },
590            {
591              "type": "ECDSA_SIGNED_ENTITY",
592              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
593              "signature": "0xb962b57accc8e12083769339888f82752d13f280012b2c7b2aa2722eae103aea7a623dc88605bf7036ec8c23b0bb8f036b52f5e4e30ee913f6f2a077d5e5e3e01b"
594            }
595          ]"#).unwrap();
596
597        let authenticator = Authenticator::new();
598        let result = authenticator
599            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
600            .await;
601
602        assert_eq!(result, Err(AuthenticatorError::ValidationError {
603            position: 1,
604            kind: "ECDSA_EIP_1654_EPHEMERAL".to_string(),
605            message: "rpc resolver not implemented".to_string()
606        }));
607    }
608
609    #[tokio::test]
610    async fn test_should_fail_if_ephemeral_is_expired() {
611        let authenticator = Authenticator::new();
612        let chain = AuthChain::from_json(r#"[
613            {
614              "type": "SIGNER",
615              "payload": "0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34",
616              "signature": ""
617            },
618            {
619              "type": "ECDSA_EPHEMERAL",
620              "payload": "Decentraland Login\nEphemeral address: 0xe94944439fAB988e5e14b128BbcF6D5502b05f9C\nExpiration: 2020-02-20T00:00:00.000Z",
621              "signature": "0x2d45e2a3e9e04614cf6bb822951b849458a78037733202d4bda12e60ef1ff4d266b02af7b72caa232c45052520fd440869672da2b0966b29fff21638e3d21ca01b"
622            },
623            {
624              "type": "ECDSA_SIGNED_ENTITY",
625              "payload": "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
626              "signature": "0x6ae9bbd2af56ea61db3afe188d78381f0cb3177376b12537a3cb01e5d242c3fc49955475615209f194d98f0c751a24f712ab1c0caa9f92fa222bd2e13e2efd611c"
627            }
628          ]"#).unwrap();
629
630        let result = authenticator
631            .verify_signature(&chain, "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo")
632            .await;
633
634        assert_eq!(
635            result,
636            Err(AuthenticatorError::ExpiredEntity {
637                position: 1,
638                kind: String::from("ECDSA_EPHEMERAL")
639            })
640        );
641
642        let time = DateTime::parse_from_rfc3339("2020-01-01T00:00:00.000Z")
643            .unwrap()
644            .with_timezone(&Utc);
645        let owner = authenticator
646            .verify_signature_at(
647                &chain,
648                "QmUsqJaHc5HQaBrojhBdjF4fr5MQc6CqhwZjqwhVRftNAo",
649                &time,
650            )
651            .await
652            .unwrap();
653
654        let expected = &Address::try_from("0x84452bbfa4ca14b7828e2f3bbd106a2bd495cd34").unwrap();
655        assert_eq!(owner, expected);
656    }
657
658    #[tokio::test]
659    async fn test_should_recover_address_from_signature() {
660        let account = Account::random();
661        let payload = "QmWyFNeHbxXaPtUnzKvDZPpKSa4d5anZEZEFJ8TC1WgcfU";
662        let signature = account.sign(payload);
663        let authenticator = Authenticator::new();
664        let result =
665            authenticator.validate_personal(&account.address(), payload, &signature.to_vec());
666
667        assert!(result.is_ok());
668        assert!(result.unwrap());
669
670        let chain = AuthChain::simple(account.address(), payload, signature.to_string()).unwrap();
671        let owner = authenticator
672            .verify_signature(&chain, payload)
673            .await
674            .unwrap();
675
676        assert_eq!(owner, &account.address());
677    }
678}