defuse_sep53/
lib.rs

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