1pub use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
11
12use ed25519_dalek::Signer;
13use sha2::{Digest, Sha256};
14
15#[derive(Debug, thiserror::Error)]
17pub enum CryptoError {
18 #[error("invalid hex: {0}")]
20 Hex(#[from] hex::FromHexError),
21 #[error("signing key must be 32 bytes, got {0}")]
23 KeyLength(usize),
24}
25
26pub fn generate_keypair() -> (SigningKey, VerifyingKey) {
28 let mut csprng = rand::rngs::OsRng;
29 let signing_key = SigningKey::generate(&mut csprng);
30 let verifying_key = signing_key.verifying_key();
31 (signing_key, verifying_key)
32}
33
34pub fn sign(
38 signing_key: &SigningKey,
39 payload: &[u8],
40 timestamp: i64,
41) -> Signature {
42 let digest = canonical_digest(payload, timestamp);
43 signing_key.sign(&digest)
44}
45
46pub fn verify(
62 verifying_key: &VerifyingKey,
63 payload: &[u8],
64 timestamp: i64,
65 signature: &Signature,
66) -> bool {
67 let digest = canonical_digest(payload, timestamp);
68 verifying_key.verify_strict(&digest, signature).is_ok()
69}
70
71pub fn signing_key_from_bytes(bytes: &[u8; 32]) -> SigningKey {
73 SigningKey::from_bytes(bytes)
74}
75
76pub fn signing_key_from_hex(hex_str: &str) -> Result<SigningKey, CryptoError> {
78 let bytes = hex::decode(hex_str.trim())?;
79 if bytes.len() != 32 {
80 return Err(CryptoError::KeyLength(bytes.len()));
81 }
82 let mut key_bytes = [0u8; 32];
83 key_bytes.copy_from_slice(&bytes);
84 Ok(SigningKey::from_bytes(&key_bytes))
85}
86
87pub fn signing_key_to_hex(key: &SigningKey) -> String {
89 hex::encode(key.to_bytes())
90}
91
92fn canonical_digest(payload: &[u8], timestamp: i64) -> Vec<u8> {
94 let mut hasher = Sha256::new();
95 hasher.update(payload);
96 hasher.update(timestamp.to_le_bytes());
97 hasher.finalize().to_vec()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn sign_and_verify_succeeds() {
106 let (signing_key, verifying_key) = generate_keypair();
107 let payload = b"hello agora";
108 let timestamp = 1234567890i64;
109
110 let signature = sign(&signing_key, payload, timestamp);
111 assert!(verify(&verifying_key, payload, timestamp, &signature));
112 }
113
114 #[test]
115 fn verify_fails_with_wrong_payload() {
116 let (signing_key, verifying_key) = generate_keypair();
117 let timestamp = 1234567890i64;
118
119 let signature = sign(&signing_key, b"correct payload", timestamp);
120 assert!(!verify(
121 &verifying_key,
122 b"wrong payload",
123 timestamp,
124 &signature
125 ));
126 }
127
128 #[test]
129 fn verify_fails_with_wrong_timestamp() {
130 let (signing_key, verifying_key) = generate_keypair();
131 let payload = b"hello agora";
132
133 let signature = sign(&signing_key, payload, 1000);
134 assert!(!verify(&verifying_key, payload, 2000, &signature));
135 }
136
137 #[test]
138 fn verify_fails_with_wrong_key() {
139 let (signing_key, _) = generate_keypair();
140 let (_, wrong_verifying_key) = generate_keypair();
141 let payload = b"hello agora";
142 let timestamp = 1234567890i64;
143
144 let signature = sign(&signing_key, payload, timestamp);
145 assert!(!verify(
146 &wrong_verifying_key,
147 payload,
148 timestamp,
149 &signature
150 ));
151 }
152
153 #[test]
154 fn different_keypairs_produce_different_signatures() {
155 let (key_a, _) = generate_keypair();
156 let (key_b, _) = generate_keypair();
157 let payload = b"same payload";
158 let timestamp = 1234567890i64;
159
160 let sig_a = sign(&key_a, payload, timestamp);
161 let sig_b = sign(&key_b, payload, timestamp);
162 assert_ne!(sig_a.to_bytes(), sig_b.to_bytes());
163 }
164
165 #[test]
166 fn hex_roundtrip() {
167 let (signing_key, _) = generate_keypair();
168 let hex_str = signing_key_to_hex(&signing_key);
169 let recovered = signing_key_from_hex(&hex_str).unwrap();
170 assert_eq!(signing_key.to_bytes(), recovered.to_bytes());
171 }
172
173 #[test]
174 fn hex_wrong_length() {
175 let result = signing_key_from_hex("abcd");
176 assert!(matches!(result, Err(CryptoError::KeyLength(2))));
177 }
178
179 #[test]
180 fn hex_invalid() {
181 let result = signing_key_from_hex("not-hex!");
182 assert!(matches!(result, Err(CryptoError::Hex(_))));
183 }
184
185 #[test]
198 fn identity_element_forgery_is_rejected() {
199 const BASEPOINT_COMPRESSED: [u8; 32] = [
204 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
205 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
206 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
207 ];
208 const SCALAR_ONE_LE: [u8; 32] = [
210 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
211 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
212 ];
213
214 let zero_vk = VerifyingKey::from_bytes(&[0u8; 32]).expect(
215 "all-zero bytes still decode as a valid curve point (identity)",
216 );
217
218 let mut sig_bytes = [0u8; 64];
219 sig_bytes[..32].copy_from_slice(&BASEPOINT_COMPRESSED);
220 sig_bytes[32..].copy_from_slice(&SCALAR_ONE_LE);
221 let forged_sig = Signature::from_bytes(&sig_bytes);
222
223 let timestamp = 1_700_000_000i64;
228 for i in 0..100 {
229 let payload = format!("attack payload {i}").into_bytes();
230 let digest = canonical_digest(&payload, timestamp);
231 let result = zero_vk.verify_strict(&digest, &forged_sig);
232 assert!(
233 result.is_err(),
234 "identity-element forgery should be rejected for payload {i}, but verify_strict accepted it"
235 );
236 }
237 }
238}