defuse_erc191/
lib.rs

1use defuse_crypto::{Curve, Secp256k1};
2use impl_tools::autoimpl;
3
4/// See [ERC-191](https://github.com/ethereum/ercs/blob/master/ERCS/erc-191.md)
5#[cfg_attr(
6    feature = "serde",
7    derive(::serde::Serialize, ::serde::Deserialize),
8    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
9)]
10#[derive(Debug, Clone)]
11pub struct Erc191Payload(pub String);
12
13impl Erc191Payload {
14    #[inline]
15    pub fn prehash(&self) -> Vec<u8> {
16        let data = self.0.as_bytes();
17        [
18            format!("\x19Ethereum Signed Message:\n{}", data.len()).as_bytes(),
19            data,
20        ]
21        .concat()
22    }
23}
24
25#[cfg(any(test, feature = "sha3", feature = "near-contract"))]
26impl defuse_crypto::Payload for Erc191Payload {
27    #[inline]
28    fn hash(&self) -> defuse_crypto::CryptoHash {
29        use defuse_digest::{Digest, Keccak256};
30        Keccak256::digest(self.prehash().as_slice()).into()
31    }
32}
33
34#[cfg_attr(
35    feature = "serde",
36    ::cfg_eval::cfg_eval,
37    ::serde_with::serde_as,
38    derive(::serde::Serialize, ::serde::Deserialize),
39    cfg_attr(feature = "abi", derive(::schemars::JsonSchema))
40)]
41#[autoimpl(Deref using self.payload)]
42#[derive(Debug, Clone)]
43pub struct SignedErc191Payload {
44    pub payload: Erc191Payload,
45
46    /// There is no public key member because the public key can be recovered
47    /// via `ecrecover()` knowing the data and the signature
48    #[cfg_attr(
49        feature = "serde",
50        serde_as(as = "defuse_crypto::serde::AsCurve<Secp256k1>")
51    )]
52    pub signature: <Secp256k1 as Curve>::Signature,
53}
54
55#[cfg(any(test, feature = "sha3", feature = "near-contract"))]
56impl defuse_crypto::Payload for SignedErc191Payload {
57    #[inline]
58    fn hash(&self) -> defuse_crypto::CryptoHash {
59        self.payload.hash()
60    }
61}
62
63#[cfg(any(test, feature = "near-contract"))]
64const _: () = {
65    use defuse_crypto::{Payload, SignedPayload, VerifiableCurve};
66    impl SignedPayload for SignedErc191Payload {
67        type PublicKey = <Secp256k1 as Curve>::PublicKey;
68
69        #[inline]
70        fn verify(&self) -> Option<Self::PublicKey> {
71            Secp256k1::verify(&self.signature, &self.payload.hash(), &())
72        }
73    }
74};
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use defuse_crypto::SignedPayload;
80    use hex_literal::hex;
81
82    const fn fix_v_in_signature(mut sig: [u8; 65]) -> [u8; 65] {
83        if *sig.last().unwrap() >= 27 {
84            // Ethereum only uses uncompressed keys, with corresponding value v=27/28
85            // https://bitcoin.stackexchange.com/a/38909/58790
86            *sig.last_mut().unwrap() -= 27;
87        }
88        sig
89    }
90
91    // Signature constructed in Metamask, using private key: a4b319a82adfc43584e4537fec97a80516e16673db382cd91eba97abbab8ca56
92    const REFERENCE_SIGNATURE: [u8; 65] = hex!(
93        "7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e471c"
94    );
95    const INVALID_REFERENCE_SIGNATURE: [u8; 65] = hex!(
96        "7900a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e471c"
97    );
98    const REFERENCE_MESSAGE: &str = "Hello world!";
99    const INVALID_REFERENCE_MESSAGE: &str = "Hello, NEAR!";
100
101    // Public key can be derived using `ethers_signers` crate:
102    // let wallet = LocalWallet::from_str(
103    //     "a4b319a82adfc43584e4537fec97a80516e16673db382cd91eba97abbab8ca56",
104    // )?;
105    // let signing_key = wallet.signer();
106    // let verifying_key = signing_key.verifying_key();
107    // let public_key = verifying_key.to_encoded_point(false);
108    // // Notice that we skip the first byte, 0x04
109    // println!("Public key: 0x{}", hex::encode(public_key.as_bytes()[1..]));
110    const REFERENCE_PUBKEY: [u8; 64] = hex!(
111        "85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"
112    );
113
114    #[test]
115    fn test_reference_signature_verification_works() {
116        assert_eq!(
117            SignedErc191Payload {
118                payload: Erc191Payload(REFERENCE_MESSAGE.to_string()),
119                signature: fix_v_in_signature(REFERENCE_SIGNATURE),
120            }
121            .verify(),
122            Some(REFERENCE_PUBKEY)
123        );
124    }
125
126    #[test]
127    fn test_invalid_reference_message_verification_fails() {
128        assert_ne!(
129            SignedErc191Payload {
130                payload: Erc191Payload(INVALID_REFERENCE_MESSAGE.to_string()),
131                signature: fix_v_in_signature(REFERENCE_SIGNATURE),
132            }
133            .verify(),
134            Some(REFERENCE_PUBKEY)
135        );
136    }
137
138    #[test]
139    fn test_invalid_reference_signature_verification_fails() {
140        assert_ne!(
141            SignedErc191Payload {
142                payload: Erc191Payload(REFERENCE_MESSAGE.to_string()),
143                signature: fix_v_in_signature(INVALID_REFERENCE_SIGNATURE),
144            }
145            .verify(),
146            Some(REFERENCE_PUBKEY)
147        );
148    }
149}