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 const SIGNATURE_WINDOW_SECS: i64 = 300;
37
38pub const fn timestamp_in_window(timestamp: i64, now: i64) -> bool {
41 now.abs_diff(timestamp) <= SIGNATURE_WINDOW_SECS as u64
42}
43
44pub fn sign(
48 signing_key: &SigningKey,
49 payload: &[u8],
50 timestamp: i64,
51) -> Signature {
52 let digest = canonical_digest(payload, timestamp);
53 signing_key.sign(&digest)
54}
55
56pub fn verify(
72 verifying_key: &VerifyingKey,
73 payload: &[u8],
74 timestamp: i64,
75 signature: &Signature,
76) -> bool {
77 let digest = canonical_digest(payload, timestamp);
78 verifying_key.verify_strict(&digest, signature).is_ok()
79}
80
81pub fn signing_key_from_bytes(bytes: &[u8; 32]) -> SigningKey {
83 SigningKey::from_bytes(bytes)
84}
85
86pub fn signing_key_from_hex(hex_str: &str) -> Result<SigningKey, CryptoError> {
88 let bytes = hex::decode(hex_str.trim())?;
89 if bytes.len() != 32 {
90 return Err(CryptoError::KeyLength(bytes.len()));
91 }
92 let mut key_bytes = [0u8; 32];
93 key_bytes.copy_from_slice(&bytes);
94 Ok(SigningKey::from_bytes(&key_bytes))
95}
96
97pub fn signing_key_to_hex(key: &SigningKey) -> String {
99 hex::encode(key.to_bytes())
100}
101
102fn canonical_digest(payload: &[u8], timestamp: i64) -> Vec<u8> {
104 let mut hasher = Sha256::new();
105 hasher.update(payload);
106 hasher.update(timestamp.to_le_bytes());
107 hasher.finalize().to_vec()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn sign_and_verify_succeeds() {
116 let (signing_key, verifying_key) = generate_keypair();
117 let payload = b"hello agora";
118 let timestamp = 1234567890i64;
119
120 let signature = sign(&signing_key, payload, timestamp);
121 assert!(verify(&verifying_key, payload, timestamp, &signature));
122 }
123
124 #[test]
125 fn verify_fails_with_wrong_payload() {
126 let (signing_key, verifying_key) = generate_keypair();
127 let timestamp = 1234567890i64;
128
129 let signature = sign(&signing_key, b"correct payload", timestamp);
130 assert!(!verify(
131 &verifying_key,
132 b"wrong payload",
133 timestamp,
134 &signature
135 ));
136 }
137
138 #[test]
139 fn verify_fails_with_wrong_timestamp() {
140 let (signing_key, verifying_key) = generate_keypair();
141 let payload = b"hello agora";
142
143 let signature = sign(&signing_key, payload, 1000);
144 assert!(!verify(&verifying_key, payload, 2000, &signature));
145 }
146
147 #[test]
148 fn verify_fails_with_wrong_key() {
149 let (signing_key, _) = generate_keypair();
150 let (_, wrong_verifying_key) = generate_keypair();
151 let payload = b"hello agora";
152 let timestamp = 1234567890i64;
153
154 let signature = sign(&signing_key, payload, timestamp);
155 assert!(!verify(
156 &wrong_verifying_key,
157 payload,
158 timestamp,
159 &signature
160 ));
161 }
162
163 #[test]
164 fn different_keypairs_produce_different_signatures() {
165 let (key_a, _) = generate_keypair();
166 let (key_b, _) = generate_keypair();
167 let payload = b"same payload";
168 let timestamp = 1234567890i64;
169
170 let sig_a = sign(&key_a, payload, timestamp);
171 let sig_b = sign(&key_b, payload, timestamp);
172 assert_ne!(sig_a.to_bytes(), sig_b.to_bytes());
173 }
174
175 #[test]
176 fn hex_roundtrip() {
177 let (signing_key, _) = generate_keypair();
178 let hex_str = signing_key_to_hex(&signing_key);
179 let recovered = signing_key_from_hex(&hex_str).unwrap();
180 assert_eq!(signing_key.to_bytes(), recovered.to_bytes());
181 }
182
183 #[test]
184 fn hex_wrong_length() {
185 let result = signing_key_from_hex("abcd");
186 assert!(matches!(result, Err(CryptoError::KeyLength(2))));
187 }
188
189 #[test]
190 fn hex_invalid() {
191 let result = signing_key_from_hex("not-hex!");
192 assert!(matches!(result, Err(CryptoError::Hex(_))));
193 }
194
195 #[test]
208 fn identity_element_forgery_is_rejected() {
209 const BASEPOINT_COMPRESSED: [u8; 32] = [
214 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
215 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
216 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
217 ];
218 const SCALAR_ONE_LE: [u8; 32] = [
220 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
221 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
222 ];
223
224 let zero_vk = VerifyingKey::from_bytes(&[0u8; 32]).expect(
225 "all-zero bytes still decode as a valid curve point (identity)",
226 );
227
228 let mut sig_bytes = [0u8; 64];
229 sig_bytes[..32].copy_from_slice(&BASEPOINT_COMPRESSED);
230 sig_bytes[32..].copy_from_slice(&SCALAR_ONE_LE);
231 let forged_sig = Signature::from_bytes(&sig_bytes);
232
233 let timestamp = 1_700_000_000i64;
238 for i in 0..100 {
239 let payload = format!("attack payload {i}").into_bytes();
240 let digest = canonical_digest(&payload, timestamp);
241 let result = zero_vk.verify_strict(&digest, &forged_sig);
242 assert!(
243 result.is_err(),
244 "identity-element forgery should be rejected for payload {i}, but verify_strict accepted it"
245 );
246 }
247 }
248}