Skip to main content

defuse_erc191/
lib.rs

1//! [ERC-191](https://eips.ethereum.org/EIPS/eip-191) Signed Data Standard
2
3use defuse_crypto::{Curve, RecoverableCurve, secp256k1::Secp256k1};
4use defuse_digest::{Digest, sha3::Keccak256};
5
6/// [ERC-191](https://eips.ethereum.org/EIPS/eip-191) Signed Data Standard
7pub struct Erc191;
8
9impl Erc191 {
10    /// Try to recover public key which signed given message according to
11    /// [ERC-191](https://eips.ethereum.org/EIPS/eip-191) and produced given
12    /// signature and recovery id.
13    #[must_use = "check recovered public key"]
14    #[inline]
15    pub fn recover(
16        msg: impl AsRef<[u8]>,
17        signature: &<Secp256k1 as Curve>::Signature,
18        recovery_id: <Secp256k1 as RecoverableCurve>::RecoveryId,
19    ) -> Option<<Secp256k1 as Curve>::PublicKey> {
20        Secp256k1::recover(&Self::prehash(msg), signature, recovery_id)
21    }
22
23    /// Derive prehash for signing according to following schema:
24    ///
25    /// ```text
26    /// 0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign>
27    /// ```
28    #[inline]
29    pub fn prehash(msg: impl AsRef<[u8]>) -> [u8; 32] {
30        let msg = msg.as_ref();
31
32        Keccak256::new_with_prefix(b"\x19Ethereum Signed Message:\n")
33            // `len(message)` is the non-zero-padded ascii-decimal encoding of the number of bytes in message.
34            .chain_update(msg.len().to_string())
35            // <data to sign>
36            .chain_update(msg)
37            .finalize()
38            .into()
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use defuse_crypto::secp256k1::{Secp256k1RecoverableSignature, Secp256k1UncompressedPublicKey};
45    use hex_literal::hex;
46    use rstest::rstest;
47
48    use super::*;
49
50    #[rstest]
51    #[case(
52        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
53        "Hello world!",
54        hex!("7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e4701"),
55    )]
56    fn recover_ok(
57        #[case] public_key: impl Into<Secp256k1UncompressedPublicKey>,
58        #[case] msg: impl AsRef<[u8]>,
59        #[case] signature: impl Into<Secp256k1RecoverableSignature>,
60    ) {
61        let (signature, recovery_id) = signature.into().try_into().unwrap();
62
63        assert_eq!(
64            Erc191::recover(msg, &signature, recovery_id),
65            Some(public_key.into().try_into().unwrap()),
66            "invalid recovered public key",
67        );
68    }
69}