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
78pub 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 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 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 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 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 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 if position != len - 1 {
363 latest_authority = self
364 .verify_ephemeral(latest_authority, link, expiration, position)
365 .await?;
366
367 } 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 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 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 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}