Skip to main content

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