siwe/
lib.rs

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)]
38/// EIP-4361 version.
39pub enum Version {
40    /// V1
41    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/// EIP-4361 message.
56///
57/// # Example
58/// ```
59/// # use siwe::Message;
60/// #
61/// let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
62/// 0x6Da01670d8fc844e736095918bbE11fE8D564163
63///
64/// SIWE Notepad Example
65///
66/// URI: http://localhost:4361
67/// Version: 1
68/// Chain ID: 1
69/// Nonce: kEWepMt9knR6lWJ6A
70/// Issued At: 2021-12-07T18:28:18.807Z"#;
71/// let message: Message = msg.parse().unwrap();
72/// ```
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct Message {
75    /// The RFC 3986 authority that is requesting the signing.
76    pub domain: Authority,
77    /// The Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable.
78    pub address: [u8; 20],
79    /// A human-readable ASCII assertion that the user will sign, and it must not contain '\n' (the byte 0x0a).
80    pub statement: Option<String>,
81    /// An RFC 3986 URI referring to the resource that is the subject of the signing (as in the subject of a claim).
82    pub uri: UriString,
83    /// The current version of the message, which MUST be 1 for this specification.
84    pub version: Version,
85    /// The EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts MUST be resolved.
86    pub chain_id: u64,
87    /// A randomized token typically chosen by the relying party and used to prevent replay attacks, at least 8 alphanumeric characters.
88    pub nonce: String,
89    /// The ISO 8601 datetime string of the current time.
90    pub issued_at: TimeStamp,
91    /// The ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid.
92    pub expiration_time: Option<TimeStamp>,
93    /// The ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid.
94    pub not_before: Option<TimeStamp>,
95    /// An system-specific identifier that may be used to uniquely refer to the sign-in request.
96    pub request_id: Option<String>,
97    /// A list of information or references to information the user wishes to have resolved as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by "\n- " where \n is the byte 0x0a.
98    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)]
135/// Errors raised during parsing/deserialization.
136pub enum ParseError {
137    #[error("Invalid Domain: {0}")]
138    /// Domain field is non-conformant.
139    Domain(#[from] InvalidUri),
140    #[error("Formatting Error: {0}")]
141    /// Catch-all for all other parsing errors.
142    Format(&'static str),
143    #[error("Invalid Address: {0}")]
144    /// Address field is non-conformant.
145    Address(#[from] hex::FromHexError),
146    #[error("Invalid URI: {0}")]
147    /// URI field is non-conformant.
148    Uri(#[from] iri_string::validate::Error),
149    #[error("Invalid Timestamp: {0}")]
150    /// Timestamp is non-conformant.
151    TimeStamp(#[from] time::Error),
152    #[error(transparent)]
153    /// Chain ID is non-conformant.
154    ParseIntError(#[from] std::num::ParseIntError),
155    #[error(transparent)]
156    /// Infallible variant.
157    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        // Skip the new line:
202        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
313// Fixes the documentation to show the typed builder impl as behind a feature flag.
314macro_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    /// Verification options and configuration
338    pub struct VerificationOpts {
339        /// Expected domain field.
340        pub domain: Option<Authority>,
341        /// Expected nonce field.
342        pub nonce: Option<String>,
343        /// Datetime for which the message should be valid at.
344        pub timestamp: Option<OffsetDateTime>,
345        #[cfg(feature = "ethers")]
346        /// RPC Provider used for on-chain checks. Necessary for contract wallets signatures.
347        pub rpc_provider: Option<Provider<Http>>,
348    }
349}
350
351// Non-derived implementation needed, otherwise the implementation is marked as being behind the
352// typed-builder feature flag.
353#[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)]
367/// Reasons for the verification of a signed message to fail.
368pub enum VerificationError {
369    #[error(transparent)]
370    /// Signature is not a valid k256 signature (it can be returned if the contract wallet verification failed or is not enabled).
371    Crypto(#[from] k256::ecdsa::Error),
372    #[error(transparent)]
373    /// Message failed to be serialized.
374    Serialization(#[from] fmt::Error),
375    #[error("Recovered key does not match address or contract wallet support is not enabled.")]
376    /// Catch-all for invalid signature (it can be returned if contract wallet support is not enabled).
377    Signer,
378    #[error("Message is not currently valid")]
379    /// Message is not currently valid.
380    Time,
381    #[error("Message domain does not match")]
382    /// Expected message domain does not match.
383    DomainMismatch,
384    #[error("Message nonce does not match")]
385    /// Expected message nonce does not match.
386    NonceMismatch,
387    #[cfg(feature = "ethers")]
388    // Using a String because the original type requires a lifetime.
389    #[error("Contract wallet query failed: {0}")]
390    /// Contract wallet verification failed unexpectedly.
391    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    /// 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.
394    SignatureLength,
395}
396
397/// Takes an UNPREFIXED eth address and returns whether it is in checksum format or not.
398pub 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    /// Verify the integrity of the message by matching its signature.
411    ///
412    /// # Arguments
413    /// - `sig` - Signature of the message signed by the wallet
414    ///
415    /// # Example
416    /// ```
417    /// # use siwe::Message;
418    /// # use hex::FromHex;
419    /// #
420    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
421    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
422    /// #
423    /// # SIWE Notepad Example
424    /// #
425    /// # URI: http://localhost:4361
426    /// # Version: 1
427    /// # Chain ID: 1
428    /// # Nonce: kEWepMt9knR6lWJ6A
429    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
430    /// # let message: Message = msg.parse().unwrap();
431    /// let signature = <[u8; 65]>::from_hex(r#"6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c"#).unwrap();
432    /// let signer: Vec<u8> = message.verify_eip191(&signature).unwrap();
433    /// ```
434    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    /// Verify the integrity of a, potentially, EIP-1271 signed message.
457    ///
458    /// # Arguments
459    /// - `sig` - Signature of the message signed by the wallet.
460    /// - `provider` - Provider used to query the chain.
461    ///
462    /// # Example (find a provider at https://ethereumnodes.com/)
463    /// ```ignore
464    /// let is_valid: bool = message.verify_eip1271(&signature, "https://provider.example.com/".try_into().unwrap())?;
465    /// ```
466    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    /// Validates time constraints and integrity of the object by matching it's signature.
476    ///
477    /// # Arguments
478    /// - `sig` - Signature of the message signed by the wallet
479    /// - `opts` - Verification options and configuration
480    ///
481    /// # Example
482    /// ```
483    /// # use hex::FromHex;
484    /// # use siwe::{Message, TimeStamp, VerificationOpts};
485    /// # use std::str::FromStr;
486    /// # use time::{format_description::well_known::Rfc3339, OffsetDateTime};
487    /// #
488    /// # #[tokio::main]
489    /// # async fn main() {
490    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
491    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
492    /// #
493    /// # SIWE Notepad Example
494    /// #
495    /// # URI: http://localhost:4361
496    /// # Version: 1
497    /// # Chain ID: 1
498    /// # Nonce: kEWepMt9knR6lWJ6A
499    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
500    /// # let message: Message = msg.parse().unwrap();
501    /// let signature = <[u8; 65]>::from_hex(r#"6228b3ecd7bf2df018183aeab6b6f1db1e9f4e3cbe24560404112e25363540eb679934908143224d746bbb5e1aa65ab435684081f4dbb74a0fec57f98f40f5051c"#).unwrap();
502    ///
503    /// let verification_opts = VerificationOpts {
504    ///     domain: Some("localhost:4361".parse().unwrap()),
505    ///     nonce: Some("kEWepMt9knR6lWJ6A".into()),
506    ///     timestamp: Some(OffsetDateTime::parse("2021-12-08T00:00:00Z", &Rfc3339).unwrap()),
507    ///     ..Default::default()
508    /// };
509    ///
510    /// message.verify(&signature, &verification_opts).await.unwrap();
511    /// # }
512    /// ```
513    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    /// Validates the time constraints of the message at current time.
551    ///
552    /// # Example
553    /// ```
554    /// # use siwe::Message;
555    /// # use time::OffsetDateTime;
556    /// #
557    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
558    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
559    /// #
560    /// # SIWE Notepad Example
561    /// #
562    /// # URI: http://localhost:4361
563    /// # Version: 1
564    /// # Chain ID: 1
565    /// # Nonce: kEWepMt9knR6lWJ6A
566    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
567    /// # let message: Message = msg.parse().unwrap();
568    /// assert!(message.valid_now());
569    ///
570    /// // equivalent to
571    /// assert!(message.valid_at(&OffsetDateTime::now_utc()));
572    /// ```
573    pub fn valid_now(&self) -> bool {
574        self.valid_at(&OffsetDateTime::now_utc())
575    }
576
577    /// Validates the time constraints of the message at a specific point in time.
578    ///
579    /// # Arguments
580    /// - `t` - timestamp to use when validating time constraints
581    ///
582    /// # Example
583    /// ```
584    /// # use siwe::Message;
585    /// # use time::OffsetDateTime;
586    /// #
587    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
588    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
589    /// #
590    /// # SIWE Notepad Example
591    /// #
592    /// # URI: http://localhost:4361
593    /// # Version: 1
594    /// # Chain ID: 1
595    /// # Nonce: kEWepMt9knR6lWJ6A
596    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
597    /// # let message: Message = msg.parse().unwrap();
598    /// assert!(message.valid_at(&OffsetDateTime::now_utc()));
599    /// ```
600    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    /// Produces EIP-191 Personal-Signature pre-hash signing input
610    ///
611    /// # Example
612    /// ```
613    /// # use siwe::Message;
614    /// #
615    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
616    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
617    /// #
618    /// # SIWE Notepad Example
619    /// #
620    /// # URI: http://localhost:4361
621    /// # Version: 1
622    /// # Chain ID: 1
623    /// # Nonce: kEWepMt9knR6lWJ6A
624    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
625    /// # let message: Message = msg.parse().unwrap();
626    /// let eip191_bytes: Vec<u8> = message.eip191_bytes().unwrap();
627    /// ```
628    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    /// Produces EIP-191 Personal-Signature Hashed signing-input
634    ///
635    /// # Example
636    /// ```
637    /// # use siwe::Message;
638    /// #
639    /// # let msg = r#"localhost:4361 wants you to sign in with your Ethereum account:
640    /// # 0x6Da01670d8fc844e736095918bbE11fE8D564163
641    /// #
642    /// # SIWE Notepad Example
643    /// #
644    /// # URI: http://localhost:4361
645    /// # Version: 1
646    /// # Chain ID: 1
647    /// # Nonce: kEWepMt9knR6lWJ6A
648    /// # Issued At: 2021-12-07T18:28:18.807Z"#;
649    /// # let message: Message = msg.parse().unwrap();
650    /// let eip191_hash: [u8; 32] = message.eip191_hash().unwrap();
651    /// ```
652    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
660/// Takes an eth address and returns it as a checksum formatted string.
661pub 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        // correct order
696        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        // incorrect order
715        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        //  no statement
733        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        // From the spec:
998        // All caps
999        "0x52908400098527886E0F7030069857D2E4169EE7",
1000        "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
1001        // All Lower
1002        "0xde709f2102306220921060314715629080e2fb77",
1003        "0x27b1fdb04752bbc536007a920d24acb045561c26",
1004        "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1005        "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
1006        "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1007        "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1008    ];
1009
1010    const INVALID_CASES: &[&str] = &[
1011        // From eip55 Crate:
1012        "0xD1220a0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1013        "0xdbF03B407c01e7cD3CBea99509d93f8DDDC8C6FB",
1014        "0xfb6916095ca1df60bB79Ce92cE3Ea74c37c5D359",
1015        "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed",
1016        // FROM SO QUESTION:
1017        "0xCF5609B003B2776699EEA1233F7C82D5695CC9AA",
1018        // From eip55 Crate Issue
1019        "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        // vectors from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
1038
1039        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}