1#![warn(missing_docs)]
2#![cfg_attr(docsrs, feature(doc_auto_cfg), feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4
5mod nonce;
6mod rfc3339;
7
8#[cfg(feature = "ethers")]
9mod eip1271;
10
11use ::core::{
12 convert::Infallible,
13 fmt::{self, Display, Formatter},
14 str::FromStr,
15};
16use hex::FromHex;
17use http::uri::{Authority, InvalidUri};
18use iri_string::types::UriString;
19use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
20use sha3::{Digest, Keccak256};
21use std::convert::{TryFrom, TryInto};
22use thiserror::Error;
23use time::OffsetDateTime;
24
25#[cfg(feature = "ethers")]
26use ethers::prelude::*;
27
28#[cfg(feature = "serde")]
29use serde::{
30 de::{self, Visitor},
31 Deserialize, Deserializer, Serialize, Serializer,
32};
33
34pub use nonce::generate_nonce;
35pub use rfc3339::TimeStamp;
36
37#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38pub enum Version {
40 V1 = 1,
42}
43
44impl FromStr for Version {
45 type Err = ParseError;
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 if s == "1" {
48 Ok(Self::V1)
49 } else {
50 Err(ParseError::Format("Bad Version"))
51 }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct Message {
75 pub domain: Authority,
77 pub address: [u8; 20],
79 pub statement: Option<String>,
81 pub uri: UriString,
83 pub version: Version,
85 pub chain_id: u64,
87 pub nonce: String,
89 pub issued_at: TimeStamp,
91 pub expiration_time: Option<TimeStamp>,
93 pub not_before: Option<TimeStamp>,
95 pub request_id: Option<String>,
97 pub resources: Vec<UriString>,
99}
100
101impl Display for Message {
102 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
103 writeln!(f, "{}{}", &self.domain, PREAMBLE)?;
104 writeln!(f, "{}", eip55(&self.address))?;
105 writeln!(f)?;
106 if let Some(statement) = &self.statement {
107 writeln!(f, "{}", statement)?;
108 }
109 writeln!(f)?;
110 writeln!(f, "{}{}", URI_TAG, &self.uri)?;
111 writeln!(f, "{}{}", VERSION_TAG, self.version as u64)?;
112 writeln!(f, "{}{}", CHAIN_TAG, &self.chain_id)?;
113 writeln!(f, "{}{}", NONCE_TAG, &self.nonce)?;
114 write!(f, "{}{}", IAT_TAG, &self.issued_at)?;
115 if let Some(exp) = &self.expiration_time {
116 write!(f, "\n{}{}", EXP_TAG, &exp)?
117 };
118 if let Some(nbf) = &self.not_before {
119 write!(f, "\n{}{}", NBF_TAG, &nbf)?
120 };
121 if let Some(rid) = &self.request_id {
122 write!(f, "\n{}{}", RID_TAG, rid)?
123 };
124 if !self.resources.is_empty() {
125 write!(f, "\n{}", RES_TAG)?;
126 for res in &self.resources {
127 write!(f, "\n- {}", res)?;
128 }
129 };
130 Ok(())
131 }
132}
133
134#[derive(Error, Debug)]
135pub enum ParseError {
137 #[error("Invalid Domain: {0}")]
138 Domain(#[from] InvalidUri),
140 #[error("Formatting Error: {0}")]
141 Format(&'static str),
143 #[error("Invalid Address: {0}")]
144 Address(#[from] hex::FromHexError),
146 #[error("Invalid URI: {0}")]
147 Uri(#[from] iri_string::validate::Error),
149 #[error("Invalid Timestamp: {0}")]
150 TimeStamp(#[from] time::Error),
152 #[error(transparent)]
153 ParseIntError(#[from] std::num::ParseIntError),
155 #[error(transparent)]
156 Never(#[from] Infallible),
158}
159
160fn tagged<'a>(tag: &'static str, line: Option<&'a str>) -> Result<&'a str, ParseError> {
161 line.and_then(|l| l.strip_prefix(tag))
162 .ok_or(ParseError::Format(tag))
163}
164
165fn parse_line<S: FromStr<Err = E>, E: Into<ParseError>>(
166 tag: &'static str,
167 line: Option<&str>,
168) -> Result<S, ParseError> {
169 tagged(tag, line).and_then(|s| S::from_str(s).map_err(|e| e.into()))
170}
171
172fn tag_optional<'a>(
173 tag: &'static str,
174 line: Option<&'a str>,
175) -> Result<Option<&'a str>, ParseError> {
176 match tagged(tag, line).map(Some) {
177 Err(ParseError::Format(t)) if t == tag => Ok(None),
178 r => r,
179 }
180}
181
182impl FromStr for Message {
183 type Err = ParseError;
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 let mut lines = s.split('\n');
186 let domain = lines
187 .next()
188 .and_then(|preamble| preamble.strip_suffix(PREAMBLE))
189 .map(Authority::from_str)
190 .ok_or(ParseError::Format("Missing Preamble Line"))??;
191 let address = tagged(ADDR_TAG, lines.next())
192 .and_then(|a| {
193 if is_checksum(a) {
194 Ok(a)
195 } else {
196 Err(ParseError::Format("Address is not in EIP-55 format"))
197 }
198 })
199 .and_then(|a| <[u8; 20]>::from_hex(a).map_err(|e| e.into()))?;
200
201 lines.next();
203 let statement = match lines.next() {
204 None => return Err(ParseError::Format("No lines found after address")),
205 Some("") => None,
206 Some(s) => {
207 lines.next();
208 Some(s.to_string())
209 }
210 };
211
212 let uri = parse_line(URI_TAG, lines.next())?;
213 let version = parse_line(VERSION_TAG, lines.next())?;
214 let chain_id = parse_line(CHAIN_TAG, lines.next())?;
215 let nonce = parse_line(NONCE_TAG, lines.next()).and_then(|nonce: String| {
216 if nonce.len() < 8 {
217 Err(ParseError::Format("Nonce must be longer than 8 characters"))
218 } else {
219 Ok(nonce)
220 }
221 })?;
222 let issued_at = tagged(IAT_TAG, lines.next())?.parse()?;
223
224 let mut line = lines.next();
225 let expiration_time = match tag_optional(EXP_TAG, line)? {
226 Some(exp) => {
227 line = lines.next();
228 Some(exp.parse()?)
229 }
230 None => None,
231 };
232 let not_before = match tag_optional(NBF_TAG, line)? {
233 Some(nbf) => {
234 line = lines.next();
235 Some(nbf.parse()?)
236 }
237 None => None,
238 };
239
240 let request_id = match tag_optional(RID_TAG, line)? {
241 Some(rid) => {
242 line = lines.next();
243 Some(rid.into())
244 }
245 None => None,
246 };
247
248 let resources = match line {
249 Some(RES_TAG) => lines.map(|s| parse_line("- ", Some(s))).collect(),
250 Some(_) => Err(ParseError::Format("Unexpected Content")),
251 None => Ok(vec![]),
252 }?;
253
254 Ok(Message {
255 domain,
256 address,
257 statement,
258 uri,
259 version,
260 chain_id,
261 nonce,
262 issued_at,
263 expiration_time,
264 not_before,
265 request_id,
266 resources,
267 })
268 }
269}
270
271#[cfg(feature = "serde")]
272impl Serialize for Message {
273 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
274 where
275 S: Serializer,
276 {
277 serializer.serialize_str(self.to_string().as_str())
278 }
279}
280
281#[cfg(feature = "serde")]
282struct MessageVisitor;
283
284#[cfg(feature = "serde")]
285impl<'de> Visitor<'de> for MessageVisitor {
286 type Value = Message;
287
288 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
289 formatter.write_str("an EIP-4361 formatted message")
290 }
291
292 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
293 where
294 E: de::Error,
295 {
296 match Message::from_str(value) {
297 Ok(message) => Ok(message),
298 Err(error) => Err(E::custom(format!("error parsing message: {}", error))),
299 }
300 }
301}
302
303#[cfg(feature = "serde")]
304impl<'de> Deserialize<'de> for Message {
305 fn deserialize<D>(deserializer: D) -> Result<Message, D::Error>
306 where
307 D: Deserializer<'de>,
308 {
309 deserializer.deserialize_str(MessageVisitor)
310 }
311}
312
313macro_rules! typed_builder_doc {
315 ($struct:item) => {
316 #[cfg(feature = "typed-builder")]
317 mod tb {
318 use super::*;
319 #[derive(typed_builder::TypedBuilder)]
320 #[builder(doc)]
321 #[cfg_attr(docsrs, doc(cfg(all())))]
322 $struct
323 }
324
325 #[cfg(not(feature = "typed-builder"))]
326 mod tb {
327 use super::*;
328 #[cfg_attr(docsrs, doc(cfg(all())))]
329 $struct
330 }
331
332 pub use tb::*;
333 }
334}
335
336typed_builder_doc! {
337 pub struct VerificationOpts {
339 pub domain: Option<Authority>,
341 pub nonce: Option<String>,
343 pub timestamp: Option<OffsetDateTime>,
345 #[cfg(feature = "ethers")]
346 pub rpc_provider: Option<Provider<Http>>,
348 }
349}
350
351#[allow(clippy::derivable_impls)]
354impl Default for VerificationOpts {
355 fn default() -> Self {
356 Self {
357 domain: None,
358 nonce: None,
359 timestamp: None,
360 #[cfg(feature = "ethers")]
361 rpc_provider: None,
362 }
363 }
364}
365
366#[derive(Error, Debug)]
367pub enum VerificationError {
369 #[error(transparent)]
370 Crypto(#[from] k256::ecdsa::Error),
372 #[error(transparent)]
373 Serialization(#[from] fmt::Error),
375 #[error("Recovered key does not match address or contract wallet support is not enabled.")]
376 Signer,
378 #[error("Message is not currently valid")]
379 Time,
381 #[error("Message domain does not match")]
382 DomainMismatch,
384 #[error("Message nonce does not match")]
385 NonceMismatch,
387 #[cfg(feature = "ethers")]
388 #[error("Contract wallet query failed: {0}")]
390 ContractCall(String),
392 #[error("The signature is not 65 bytes long. It might mean that it is a EIP1271 signature and you have the `ethers` feature disabled or configured a provider.")]
393 SignatureLength,
395}
396
397pub fn is_checksum(address: &str) -> bool {
399 match <[u8; 20]>::from_hex(address) {
400 Ok(s) => {
401 let sum = eip55(&s);
402 let sum = sum.trim_start_matches("0x");
403 sum == address
404 }
405 Err(_) => false,
406 }
407}
408
409impl Message {
410 pub fn verify_eip191(&self, sig: &[u8; 65]) -> Result<Vec<u8>, VerificationError> {
435 let prehash = self.eip191_hash()?;
436 let signature: Signature = Signature::from_slice(&sig[..64])?;
437 let recovery_id = RecoveryId::try_from(&sig[64] % 27)?;
438
439 let pk: VerifyingKey =
440 VerifyingKey::recover_from_prehash(&prehash, &signature, recovery_id)?;
441
442 let recovered_address = Keccak256::default()
443 .chain_update(&pk.to_encoded_point(false).as_bytes()[1..])
444 .finalize();
445
446 let recovered_address: &[u8] = &recovered_address[12..];
447
448 if recovered_address != self.address {
449 Err(VerificationError::Signer)
450 } else {
451 Ok(pk.to_sec1_bytes().to_vec())
452 }
453 }
454
455 #[cfg(feature = "ethers")]
456 pub async fn verify_eip1271(
467 &self,
468 sig: &[u8],
469 provider: &Provider<Http>,
470 ) -> Result<bool, VerificationError> {
471 let hash = Keccak256::new_with_prefix(self.eip191_bytes().unwrap()).finalize();
472 eip1271::verify_eip1271(self.address, hash.as_ref(), sig, provider).await
473 }
474
475 pub async fn verify(
514 &self,
515 sig: &[u8],
516 opts: &VerificationOpts,
517 ) -> Result<(), VerificationError> {
518 match (
519 opts.timestamp
520 .as_ref()
521 .map(|t| self.valid_at(t))
522 .unwrap_or_else(|| self.valid_now()),
523 opts.domain.as_ref(),
524 opts.nonce.as_ref(),
525 ) {
526 (false, _, _) => return Err(VerificationError::Time),
527 (_, Some(d), _) if *d != self.domain => return Err(VerificationError::DomainMismatch),
528 (_, _, Some(n)) if *n != self.nonce => return Err(VerificationError::NonceMismatch),
529 _ => (),
530 };
531
532 let res = if sig.len() == 65 {
533 self.verify_eip191(sig.try_into().unwrap())
534 } else {
535 Err(VerificationError::SignatureLength)
536 };
537
538 #[cfg(feature = "ethers")]
539 if let Err(e) = res {
540 if let Some(provider) = &opts.rpc_provider {
541 if self.verify_eip1271(sig, provider).await? {
542 return Ok(());
543 }
544 }
545 return Err(e);
546 }
547 res.map(|_| ())
548 }
549
550 pub fn valid_now(&self) -> bool {
574 self.valid_at(&OffsetDateTime::now_utc())
575 }
576
577 pub fn valid_at(&self, t: &OffsetDateTime) -> bool {
601 self.not_before.as_ref().map(|nbf| nbf < t).unwrap_or(true)
602 && self
603 .expiration_time
604 .as_ref()
605 .map(|exp| exp >= t)
606 .unwrap_or(true)
607 }
608
609 pub fn eip191_bytes(&self) -> Result<Vec<u8>, fmt::Error> {
629 let s = self.to_string();
630 Ok(format!("\x19Ethereum Signed Message:\n{}{}", s.as_bytes().len(), s).into())
631 }
632
633 pub fn eip191_hash(&self) -> Result<[u8; 32], fmt::Error> {
653 Ok(Keccak256::default()
654 .chain_update(self.eip191_bytes()?)
655 .finalize()
656 .into())
657 }
658}
659
660pub fn eip55(addr: &[u8; 20]) -> String {
662 let addr_str = hex::encode(addr);
663 let hash = Keccak256::digest(addr_str.as_bytes());
664 "0x".chars()
665 .chain(addr_str.chars().enumerate().map(|(i, c)| {
666 match (c, hash[i >> 1] & if i % 2 == 0 { 128 } else { 8 } != 0) {
667 ('a'..='f' | 'A'..='F', true) => c.to_ascii_uppercase(),
668 _ => c.to_ascii_lowercase(),
669 }
670 }))
671 .collect()
672}
673
674const PREAMBLE: &str = " wants you to sign in with your Ethereum account:";
675const ADDR_TAG: &str = "0x";
676const URI_TAG: &str = "URI: ";
677const VERSION_TAG: &str = "Version: ";
678const CHAIN_TAG: &str = "Chain ID: ";
679const NONCE_TAG: &str = "Nonce: ";
680const IAT_TAG: &str = "Issued At: ";
681const EXP_TAG: &str = "Expiration Time: ";
682const NBF_TAG: &str = "Not Before: ";
683const RID_TAG: &str = "Request ID: ";
684const RES_TAG: &str = "Resources:";
685
686#[cfg(test)]
687mod tests {
688 use time::format_description::well_known::Rfc3339;
689
690 use super::*;
691 use std::convert::TryInto;
692
693 #[test]
694 fn parsing() {
695 let message = r#"service.org wants you to sign in with your Ethereum account:
6970xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
698
699I accept the ServiceOrg Terms of Service: https://service.org/tos
700
701URI: https://service.org/login
702Version: 1
703Chain ID: 1
704Nonce: 32891756
705Issued At: 2021-09-30T16:25:24Z
706Resources:
707- ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/
708- https://example.com/my-web2-claim.json"#;
709
710 assert!(Message::from_str(message).is_ok());
711
712 assert_eq!(message, &Message::from_str(message).unwrap().to_string());
713
714 assert!(Message::from_str(
716 r#"service.org wants you to sign in with your Ethereum account:
7170xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
718
719I accept the ServiceOrg Terms of Service: https://service.org/tos
720
721URI: https://service.org/login
722Version: 1
723Nonce: 32891756
724Chain ID: 1
725Issued At: 2021-09-30T16:25:24Z
726Resources:
727- ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/
728- https://example.com/my-web2-claim.json"#,
729 )
730 .is_err());
731
732 let message = r#"service.org wants you to sign in with your Ethereum account:
7340xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
735
736
737URI: https://service.org/login
738Version: 1
739Chain ID: 1
740Nonce: 32891756
741Issued At: 2021-09-30T16:25:24Z
742Resources:
743- ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/
744- https://example.com/my-web2-claim.json"#;
745
746 assert!(Message::from_str(message).is_ok());
747
748 assert_eq!(message, &Message::from_str(message).unwrap().to_string());
749 }
750
751 #[tokio::test]
752 async fn verification() {
753 let message = Message::from_str(
754 r#"localhost:4361 wants you to sign in with your Ethereum account:
7550x6Da01670d8fc844e736095918bbE11fE8D564163
756
757SIWE Notepad Example
758
759URI: http://localhost:4361
760Version: 1
761Chain ID: 1
762Nonce: kEWepMt9knR6lWJ6A
763Issued At: 2021-12-07T18:28:18.807Z"#,
764 )
765 .unwrap();
766 let correct = <[u8; 65]>::from_hex(r#"6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c"#).unwrap();
767
768 let verify_result = message.verify_eip191(&correct);
769 dbg!(&verify_result);
770 assert!(verify_result.is_ok());
771
772 let incorrect = <[u8; 65]>::from_hex(r#"7228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c"#).unwrap();
773 assert!(message.verify_eip191(&incorrect).is_err());
774 }
775
776 #[tokio::test]
777 async fn verification1() {
778 let message = Message::from_str(r#"localhost wants you to sign in with your Ethereum account:
7790x4b60ffAf6fD681AbcC270Faf4472011A4A14724C
780
781Allow localhost to access your orbit using their temporary session key: did:key:z6Mktud6LcDFb3heS7FFWoJhiCafmUPkCAgpvJLv5E6fgBJg#z6Mktud6LcDFb3heS7FFWoJhiCafmUPkCAgpvJLv5E6fgBJg
782
783URI: did:key:z6Mktud6LcDFb3heS7FFWoJhiCafmUPkCAgpvJLv5E6fgBJg#z6Mktud6LcDFb3heS7FFWoJhiCafmUPkCAgpvJLv5E6fgBJg
784Version: 1
785Chain ID: 1
786Nonce: PPrtjztx2lYqWbqNs
787Issued At: 2021-12-20T12:29:25.907Z
788Expiration Time: 2021-12-20T12:44:25.906Z
789Resources:
790- kepler://bafk2bzacecn2cdbtzho72x4c62fcxvcqj23padh47s5jyyrv42mtca3yrhlpa#put
791- kepler://bafk2bzacecn2cdbtzho72x4c62fcxvcqj23padh47s5jyyrv42mtca3yrhlpa#del
792- kepler://bafk2bzacecn2cdbtzho72x4c62fcxvcqj23padh47s5jyyrv42mtca3yrhlpa#get
793- kepler://bafk2bzacecn2cdbtzho72x4c62fcxvcqj23padh47s5jyyrv42mtca3yrhlpa#list"#).unwrap();
794 let correct = <[u8; 65]>::from_hex(r#"20c0da863b3dbfbb2acc0fb3b9ec6daefa38f3f20c997c283c4818ebeca96878787f84fccc25c4087ccb31ebd782ae1d2f74be076a49c0a8604419e41507e9381c"#).unwrap();
795 assert!(message.verify_eip191(&correct).is_ok());
796 let incorrect = <[u8; 65]>::from_hex(r#"30c0da863b3dbfbb2acc0fb3b9ec6daefa38f3f20c997c283c4818ebeca96878787f84fccc25c4087ccb31ebd782ae1d2f74be076a49c0a8604419e41507e9381c"#).unwrap();
797 assert!(message.verify_eip191(&incorrect).is_err());
798 }
799
800 const PARSING_POSITIVE: &str = include_str!("../tests/siwe/test/parsing_positive.json");
801 const PARSING_NEGATIVE: &str = include_str!("../tests/siwe/test/parsing_negative.json");
802 const VERIFICATION_POSITIVE: &str =
803 include_str!("../tests/siwe/test/verification_positive.json");
804 const VERIFICATION_NEGATIVE: &str =
805 include_str!("../tests/siwe/test/verification_negative.json");
806 #[cfg(feature = "ethers")]
807 const VERIFICATION_EIP1271: &str = include_str!("../tests/siwe/test/eip1271.json");
808
809 fn fields_to_message(fields: &serde_json::Value) -> anyhow::Result<Message> {
810 let fields = fields.as_object().unwrap();
811 Ok(Message {
812 domain: fields["domain"].as_str().unwrap().try_into().unwrap(),
813 address: <[u8; 20]>::from_hex(
814 fields["address"]
815 .as_str()
816 .unwrap()
817 .strip_prefix("0x")
818 .unwrap(),
819 )
820 .unwrap(),
821 statement: fields
822 .get("statement")
823 .map(|s| s.as_str().unwrap().try_into().unwrap()),
824 uri: fields["uri"].as_str().unwrap().try_into().unwrap(),
825 version: <Version as std::str::FromStr>::from_str(fields["version"].as_str().unwrap())
826 .unwrap(),
827 chain_id: fields["chainId"].as_u64().unwrap(),
828 nonce: fields["nonce"].as_str().unwrap().try_into().unwrap(),
829 issued_at: <TimeStamp as std::str::FromStr>::from_str(
830 fields["issuedAt"].as_str().unwrap(),
831 )?,
832 expiration_time: match fields.get("expirationTime") {
833 Some(e) => Some(<TimeStamp as std::str::FromStr>::from_str(
834 e.as_str().unwrap(),
835 )?),
836 None => None,
837 },
838 not_before: if let Some(not_before) = fields.get("notBefore") {
839 Some(<TimeStamp as std::str::FromStr>::from_str(
840 not_before.as_str().unwrap(),
841 )?)
842 } else {
843 None
844 },
845 request_id: fields
846 .get("requestId")
847 .map(|e| e.as_str().unwrap().to_string()),
848 resources: fields
849 .get("resources")
850 .map(|e| {
851 e.as_array()
852 .unwrap()
853 .iter()
854 .map(|r| {
855 <UriString as std::str::FromStr>::from_str(r.as_str().unwrap()).unwrap()
856 })
857 .collect()
858 })
859 .unwrap_or_default(),
860 })
861 }
862
863 #[test]
864 fn parsing_positive() {
865 let tests: serde_json::Value = serde_json::from_str(PARSING_POSITIVE).unwrap();
866 for (test_name, test) in tests.as_object().unwrap() {
867 print!("{} -> ", test_name);
868 let parsed_message = Message::from_str(test["message"].as_str().unwrap()).unwrap();
869 let fields = &test["fields"];
870 let expected_message = fields_to_message(fields).unwrap();
871 assert!(parsed_message == expected_message);
872 println!("✅")
873 }
874 }
875
876 #[test]
877 fn parsing_negative() {
878 let tests: serde_json::Value = serde_json::from_str(PARSING_NEGATIVE).unwrap();
879 for (test_name, test) in tests.as_object().unwrap() {
880 print!("{} -> ", test_name);
881 assert!(Message::from_str(test.as_str().unwrap()).is_err());
882 println!("✅")
883 }
884 }
885
886 #[tokio::test]
887 async fn verification_positive() {
888 let tests: serde_json::Value = serde_json::from_str(VERIFICATION_POSITIVE).unwrap();
889 for (test_name, test) in tests.as_object().unwrap() {
890 print!("{} -> ", test_name);
891 let fields = &test;
892 let message = fields_to_message(fields).unwrap();
893 let signature = <[u8; 65]>::from_hex(
894 fields.as_object().unwrap()["signature"]
895 .as_str()
896 .unwrap()
897 .strip_prefix("0x")
898 .unwrap(),
899 )
900 .unwrap();
901 let timestamp = fields
902 .as_object()
903 .unwrap()
904 .get("time")
905 .and_then(|timestamp| {
906 OffsetDateTime::parse(timestamp.as_str().unwrap(), &Rfc3339).ok()
907 });
908 let opts = VerificationOpts {
909 timestamp,
910 ..Default::default()
911 };
912 assert!(message.verify(&signature, &opts).await.is_ok());
913 println!("✅")
914 }
915 }
916
917 #[cfg(feature = "ethers")]
918 #[tokio::test]
919 async fn verification_eip1271() {
920 let tests: serde_json::Value = serde_json::from_str(VERIFICATION_EIP1271).unwrap();
921 for (test_name, test) in tests.as_object().unwrap() {
922 print!("{} -> ", test_name);
923 let message = Message::from_str(test["message"].as_str().unwrap()).unwrap();
924 let signature = <Vec<u8>>::from_hex(
925 test["signature"]
926 .as_str()
927 .unwrap()
928 .strip_prefix("0x")
929 .unwrap(),
930 )
931 .unwrap();
932 let opts = VerificationOpts {
933 rpc_provider: Some("https://eth.llamarpc.com".try_into().unwrap()),
934 ..Default::default()
935 };
936 assert!(message.verify(&signature, &opts).await.is_ok());
937 println!("✅")
938 }
939 }
940
941 #[tokio::test]
942 async fn verification_negative() {
943 let tests: serde_json::Value = serde_json::from_str(VERIFICATION_NEGATIVE).unwrap();
944 for (test_name, test) in tests.as_object().unwrap() {
945 print!("{} -> ", test_name);
946 let fields = &test;
947 let message = fields_to_message(fields);
948 let signature = <Vec<u8>>::from_hex(
949 fields.as_object().unwrap()["signature"]
950 .as_str()
951 .unwrap()
952 .strip_prefix("0x")
953 .unwrap(),
954 );
955 let domain_binding =
956 fields
957 .as_object()
958 .unwrap()
959 .get("domainBinding")
960 .and_then(|domain_binding| {
961 Authority::from_str(domain_binding.as_str().unwrap()).ok()
962 });
963 let match_nonce = fields
964 .as_object()
965 .unwrap()
966 .get("matchNonce")
967 .and_then(|match_nonce| match_nonce.as_str())
968 .map(|n| n.to_string());
969 let timestamp = fields
970 .as_object()
971 .unwrap()
972 .get("time")
973 .and_then(|timestamp| {
974 OffsetDateTime::parse(timestamp.as_str().unwrap(), &Rfc3339).ok()
975 });
976 #[allow(clippy::needless_update)]
977 let opts = VerificationOpts {
978 domain: domain_binding,
979 nonce: match_nonce,
980 timestamp,
981 ..Default::default()
982 };
983 assert!(
984 message.is_err()
985 || signature.is_err()
986 || message
987 .unwrap()
988 .verify(&signature.unwrap(), &opts,)
989 .await
990 .is_err()
991 );
992 println!("✅")
993 }
994 }
995
996 const VALID_CASES: &[&str] = &[
997 "0x52908400098527886E0F7030069857D2E4169EE7",
1000 "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
1001 "0xde709f2102306220921060314715629080e2fb77",
1003 "0x27b1fdb04752bbc536007a920d24acb045561c26",
1004 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1005 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
1006 "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1007 "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1008 ];
1009
1010 const INVALID_CASES: &[&str] = &[
1011 "0xD1220a0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1013 "0xdbF03B407c01e7cD3CBea99509d93f8DDDC8C6FB",
1014 "0xfb6916095ca1df60bB79Ce92cE3Ea74c37c5D359",
1015 "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed",
1016 "0xCF5609B003B2776699EEA1233F7C82D5695CC9AA",
1018 "0x000000000000000000000000000000000000dEAD",
1020 ];
1021
1022 #[test]
1023 fn test_is_checksum() {
1024 for case in VALID_CASES {
1025 let c = case.trim_start_matches("0x");
1026 assert!(is_checksum(c))
1027 }
1028
1029 for case in INVALID_CASES {
1030 let c = case.trim_start_matches("0x");
1031 assert!(!is_checksum(c))
1032 }
1033 }
1034
1035 #[test]
1036 fn eip55_test() {
1037 assert!(test_eip55(
1040 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1041 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
1042 ));
1043 assert!(test_eip55(
1044 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
1045 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"
1046 ));
1047 assert!(test_eip55(
1048 "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1049 "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"
1050 ));
1051 assert!(test_eip55(
1052 "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1053 "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
1054 ));
1055
1056 assert!(test_eip55(
1057 "0x52908400098527886E0F7030069857D2E4169EE7",
1058 "0x52908400098527886E0F7030069857D2E4169EE7",
1059 ));
1060 assert!(test_eip55(
1061 "0x8617e340b3d01fa5f11f306f4090fd50e238070d",
1062 "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
1063 ));
1064 assert!(test_eip55(
1065 "0xde709f2102306220921060314715629080e2fb77",
1066 "0xde709f2102306220921060314715629080e2fb77",
1067 ));
1068 assert!(test_eip55(
1069 "0x27b1fdb04752bbc536007a920d24acb045561c26",
1070 "0x27b1fdb04752bbc536007a920d24acb045561c26"
1071 ));
1072 assert!(test_eip55(
1073 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1074 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1075 ));
1076 assert!(test_eip55(
1077 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
1078 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"
1079 ));
1080 assert!(test_eip55(
1081 "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1082 "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1083 ));
1084 assert!(test_eip55(
1085 "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1086 "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
1087 ));
1088 }
1089
1090 fn test_eip55(addr: &str, checksum: &str) -> bool {
1091 let unprefixed = addr.strip_prefix("0x").unwrap();
1092 eip55(&<[u8; 20]>::from_hex(unprefixed).unwrap()) == checksum
1093 && eip55(&<[u8; 20]>::from_hex(unprefixed.to_lowercase()).unwrap()) == checksum
1094 && eip55(&<[u8; 20]>::from_hex(unprefixed.to_uppercase()).unwrap()) == checksum
1095 }
1096}