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}