defuse_sep53/
lib.rs

1use defuse_crypto::{CryptoHash, Curve, Ed25519, Payload, SignedPayload, serde::AsCurve};
2use impl_tools::autoimpl;
3use near_sdk::{env, near};
4use serde_with::serde_as;
5
6/// See [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md)
7#[near(serializers = [json])]
8#[serde(rename_all = "snake_case")]
9#[derive(Debug, Clone)]
10pub struct Sep53Payload {
11    pub payload: String,
12}
13
14impl Sep53Payload {
15    #[inline]
16    pub const fn new(payload: String) -> Self {
17        Self { payload }
18    }
19
20    #[inline]
21    pub fn prehash(&self) -> Vec<u8> {
22        [b"Stellar Signed Message:\n", self.payload.as_bytes()].concat()
23    }
24}
25
26impl Payload for Sep53Payload {
27    #[inline]
28    fn hash(&self) -> CryptoHash {
29        env::sha256_array(self.prehash())
30    }
31}
32
33#[near(serializers = [json])]
34#[autoimpl(Deref using self.payload)]
35#[derive(Debug, Clone)]
36pub struct SignedSep53Payload {
37    #[serde(flatten)]
38    pub payload: Sep53Payload,
39
40    #[serde_as(as = "AsCurve<Ed25519>")]
41    pub public_key: <Ed25519 as Curve>::PublicKey,
42    #[serde_as(as = "AsCurve<Ed25519>")]
43    pub signature: <Ed25519 as Curve>::Signature,
44}
45
46impl Payload for SignedSep53Payload {
47    #[inline]
48    fn hash(&self) -> CryptoHash {
49        self.payload.hash()
50    }
51}
52
53impl SignedPayload for SignedSep53Payload {
54    type PublicKey = <Ed25519 as Curve>::PublicKey;
55
56    #[inline]
57    fn verify(&self) -> Option<Self::PublicKey> {
58        Ed25519::verify(&self.signature, &self.hash(), &self.public_key)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use crate::{Sep53Payload, SignedSep53Payload};
65    use base64::{Engine, engine::general_purpose::STANDARD};
66    use defuse_crypto::{Payload, SignedPayload};
67    use defuse_test_utils::random::{CryptoRng, Rng, gen_random_string, random_bytes, rng};
68    use defuse_test_utils::tamper::{tamper_bytes, tamper_string};
69    use ed25519_dalek::Verifier;
70    use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
71    use near_sdk::base64;
72    use rstest::rstest;
73    use stellar_strkey::Strkey;
74
75    #[test]
76    fn reference_test_vectors() {
77        // 1) Decode the StrKey seed -> raw 32 bytes
78        let seed = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW";
79        let raw_key = match Strkey::from_string(seed).unwrap() {
80            Strkey::PrivateKeyEd25519(pk) => pk.0,
81            _ => panic!("expected an Ed25519 seed"),
82        };
83
84        // 2) Build SigningKey + VerifyingKey
85        let mut signing_key = SigningKey::from_bytes(&raw_key);
86        let verifying_key = signing_key.verifying_key();
87
88        let vectors = [
89            (
90                "Hello, World!",
91                "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==",
92            ),
93            (
94                "こんにちは、世界!",
95                "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==",
96            ),
97            // One test vector is dropped because it's binary data, and that's not supported
98        ];
99
100        // Verify with dalek
101        for (msg, expected_b64) in vectors {
102            let mut payload = "Stellar Signed Message:\n".to_string();
103            payload += msg;
104
105            let hash = near_sdk::env::sha256_array(payload.as_bytes());
106            let sig = signing_key.sign(hash.as_ref());
107            let actual_b64 = STANDARD.encode(sig.to_bytes());
108
109            assert_eq!(actual_b64, *expected_b64);
110            assert!(verifying_key.verify(hash.as_ref(), &sig).is_ok());
111        }
112
113        // Verify with our abstraction
114        for (msg, expected_sig_b64) in vectors {
115            let payload = Sep53Payload::new(msg.to_string());
116
117            let hash = payload.hash();
118            let secret_key = near_crypto::SecretKey::ED25519(near_crypto::ED25519SecretKey(
119                signing_key
120                    .as_bytes()
121                    .iter()
122                    .chain(verifying_key.as_bytes())
123                    .copied()
124                    .collect::<Vec<_>>()
125                    .try_into()
126                    .unwrap(),
127            ));
128            let generic_sig = secret_key.sign(hash.as_ref());
129            let sig = match generic_sig {
130                near_crypto::Signature::ED25519(signature) => signature,
131                near_crypto::Signature::SECP256K1(_) => unreachable!(),
132            };
133
134            let actual_sig_b64 = STANDARD.encode(sig.to_bytes());
135
136            assert_eq!(actual_sig_b64, *expected_sig_b64);
137            assert!(generic_sig.verify(hash.as_ref(), &secret_key.public_key()));
138
139            let signed_payload = SignedSep53Payload {
140                payload,
141                public_key: verifying_key.as_bytes().to_owned(),
142                signature: sig.to_bytes(),
143            };
144
145            assert_eq!(
146                signed_payload.verify(),
147                Some(verifying_key.as_bytes().to_owned())
148            );
149        }
150    }
151
152    /// Decode our test seed into a NEAR ED25519 secret + public key
153    fn make_ed25519_key(rng: &mut (impl Rng + CryptoRng)) -> near_crypto::SecretKey {
154        // We have to use dalek because near interface doesn't support making keys from bytes
155        // so we start from dalek, generate a random key, then use it in a new near_crypto key
156        let key_len = ed25519_dalek::SECRET_KEY_LENGTH;
157        let bytes = random_bytes(key_len..=key_len, rng);
158        let signing_key = SigningKey::from_bytes(&bytes.try_into().unwrap());
159        let verifying_key = signing_key.verifying_key();
160
161        near_crypto::SecretKey::ED25519(near_crypto::ED25519SecretKey(
162            signing_key
163                .as_bytes()
164                .iter()
165                .chain(verifying_key.as_bytes())
166                .copied()
167                .collect::<Vec<_>>()
168                .try_into()
169                .unwrap(),
170        ))
171    }
172
173    #[rstest]
174    fn tampered_message_fails(mut rng: impl Rng + CryptoRng) {
175        let sk = make_ed25519_key(&mut rng);
176        let pk = sk.public_key();
177
178        let msg = gen_random_string(&mut rng, 100..1000);
179
180        // sign the “good” message
181        let payload = Sep53Payload::new(msg.clone());
182        let hash = payload.hash();
183        let sig = match sk.sign(hash.as_ref()) {
184            near_crypto::Signature::ED25519(signature) => signature,
185            near_crypto::Signature::SECP256K1(_) => unreachable!(),
186        };
187
188        {
189            let signed_good = SignedSep53Payload {
190                payload,
191                public_key: pk.key_data().try_into().unwrap(),
192                signature: sig.to_bytes(),
193            };
194            assert!(signed_good.verify().is_some());
195        }
196
197        // tamper with the message, and expect failure
198        {
199            let tempered_message = tamper_string(&mut rng, &msg);
200
201            // verify with a tampered message
202            let bad_payload = Sep53Payload::new(tempered_message);
203            let signed_bad = SignedSep53Payload {
204                payload: bad_payload,
205                public_key: pk.key_data().try_into().unwrap(),
206                signature: sig.to_bytes(),
207            };
208            assert_eq!(signed_bad.verify(), None);
209        }
210    }
211
212    #[rstest]
213    fn tampered_signature_fails(mut rng: impl Rng + CryptoRng) {
214        let sk = make_ed25519_key(&mut rng);
215        let pk = sk.public_key();
216
217        let msg = gen_random_string(&mut rng, 100..1000);
218
219        // sign the canonical payload
220        let payload = Sep53Payload::new(msg);
221        let hash = payload.hash();
222        let sig = match sk.sign(hash.as_ref()) {
223            near_crypto::Signature::ED25519(signature) => signature,
224            near_crypto::Signature::SECP256K1(_) => unreachable!(),
225        };
226
227        {
228            let signed_good = SignedSep53Payload {
229                payload: payload.clone(),
230                public_key: pk.key_data().try_into().unwrap(),
231                signature: sig.into(),
232            };
233            assert!(signed_good.verify().is_some());
234        }
235
236        // tamper with the signature, and expect failure
237        {
238            let bad_bytes = tamper_bytes(&mut rng, sig.to_bytes().as_ref(), false);
239
240            let signed_bad = SignedSep53Payload {
241                payload,
242                public_key: pk.key_data().try_into().unwrap(),
243                signature: bad_bytes.try_into().unwrap(),
244            };
245            assert!(signed_bad.verify().is_none());
246        }
247    }
248}