defuse_sep53/
lib.rs

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