1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as BASE64};
4use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
5use sha2::{Digest, Sha256};
6
7const ED25519_PRIVATE_KEY_LEN: usize = 32;
8const ED25519_PUBLIC_KEY_LEN: usize = 32;
9const ED25519_SIGNATURE_LEN: usize = 64;
10
11wowlab_engine_macros::define_error! {
12#[derive(Debug)]
14#[non_exhaustive]
15pub struct CryptoError(CryptoErrorKind);
16
17#[derive(Debug, thiserror::Error)]
18enum CryptoErrorKind {
19 #[error("invalid {input} base64: {source}")]
20 InvalidBase64 {
21 input: CryptoInput,
22 #[source]
23 source: base64::DecodeError,
24 },
25
26 #[error("invalid private key length: expected 32 bytes, got {0}")]
27 InvalidPrivateKeyLength(usize),
28
29 #[error("invalid public key length: expected 32 bytes, got {0}")]
30 InvalidPublicKeyLength(usize),
31
32 #[error("invalid public key")]
33 InvalidPublicKey(#[source] ed25519_dalek::SignatureError),
34
35 #[error("invalid signature length: expected 64 bytes, got {0}")]
36 InvalidSignatureLength(usize),
37
38 #[error("signature verification failed")]
39 VerificationFailed(#[source] ed25519_dalek::SignatureError),
40}
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45#[non_exhaustive]
46pub enum CryptoInput {
47 PrivateKey,
48 PublicKey,
49 Signature,
50}
51
52impl std::fmt::Display for CryptoInput {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(match self {
55 Self::PrivateKey => "private key",
56 Self::PublicKey => "public key",
57 Self::Signature => "signature",
58 })
59 }
60}
61
62impl CryptoError {
63 const fn invalid_base64(input: CryptoInput, source: base64::DecodeError) -> Self {
64 Self(CryptoErrorKind::InvalidBase64 { input, source })
65 }
66
67 const fn invalid_private_key_length(actual: usize) -> Self {
68 Self(CryptoErrorKind::InvalidPrivateKeyLength(actual))
69 }
70
71 const fn invalid_public_key_length(actual: usize) -> Self {
72 Self(CryptoErrorKind::InvalidPublicKeyLength(actual))
73 }
74
75 const fn invalid_public_key(source: ed25519_dalek::SignatureError) -> Self {
76 Self(CryptoErrorKind::InvalidPublicKey(source))
77 }
78
79 const fn invalid_signature_length(actual: usize) -> Self {
80 Self(CryptoErrorKind::InvalidSignatureLength(actual))
81 }
82
83 const fn verification_failed(source: ed25519_dalek::SignatureError) -> Self {
84 Self(CryptoErrorKind::VerificationFailed(source))
85 }
86
87 #[must_use]
89 pub const fn input(&self) -> Option<CryptoInput> {
90 match self.0 {
91 CryptoErrorKind::InvalidBase64 { input, .. } => Some(input),
92 CryptoErrorKind::InvalidPrivateKeyLength(_) => Some(CryptoInput::PrivateKey),
93 CryptoErrorKind::InvalidPublicKeyLength(_) | CryptoErrorKind::InvalidPublicKey(_) => {
94 Some(CryptoInput::PublicKey)
95 }
96 CryptoErrorKind::InvalidSignatureLength(_) => Some(CryptoInput::Signature),
97 CryptoErrorKind::VerificationFailed(_) => None,
98 }
99 }
100
101 #[must_use]
103 pub const fn is_invalid_base64(&self) -> bool {
104 matches!(self.0, CryptoErrorKind::InvalidBase64 { .. })
105 }
106
107 #[must_use]
109 pub const fn invalid_length(&self) -> Option<usize> {
110 match self.0 {
111 CryptoErrorKind::InvalidPrivateKeyLength(actual)
112 | CryptoErrorKind::InvalidPublicKeyLength(actual)
113 | CryptoErrorKind::InvalidSignatureLength(actual) => Some(actual),
114 CryptoErrorKind::InvalidBase64 { .. }
115 | CryptoErrorKind::InvalidPublicKey(_)
116 | CryptoErrorKind::VerificationFailed(_) => None,
117 }
118 }
119
120 #[must_use]
122 pub const fn is_verification_failed(&self) -> bool {
123 matches!(self.0, CryptoErrorKind::VerificationFailed(_))
124 }
125}
126
127#[derive(Clone)]
129pub struct NodeKeypair {
130 signing_key: SigningKey,
131}
132
133impl NodeKeypair {
134 #[must_use]
135 pub fn generate() -> Self {
136 use rand::Rng;
137 let mut key_bytes = [0u8; ED25519_PRIVATE_KEY_LEN];
138
139 rand::rng().fill_bytes(&mut key_bytes);
140 let signing_key = SigningKey::from_bytes(&key_bytes);
141
142 Self { signing_key }
143 }
144
145 pub fn from_private_key(bytes: &[u8]) -> Result<Self, CryptoError> {
151 if bytes.len() != ED25519_PRIVATE_KEY_LEN {
152 return Err(CryptoError::invalid_private_key_length(bytes.len()));
153 }
154
155 let mut key_bytes = [0u8; ED25519_PRIVATE_KEY_LEN];
156
157 key_bytes.copy_from_slice(bytes);
158 let signing_key = SigningKey::from_bytes(&key_bytes);
159
160 Ok(Self { signing_key })
161 }
162
163 #[must_use]
164 pub fn private_key_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_LEN] {
165 self.signing_key.to_bytes()
166 }
167
168 #[must_use]
169 pub fn public_key_bytes(&self) -> [u8; ED25519_PUBLIC_KEY_LEN] {
170 self.signing_key.verifying_key().to_bytes()
171 }
172
173 #[must_use]
174 pub fn private_key_base64(&self) -> String {
175 BASE64.encode(self.private_key_bytes())
176 }
177
178 #[must_use]
179 pub fn public_key_base64(&self) -> String {
180 BASE64.encode(self.public_key_bytes())
181 }
182
183 #[must_use]
184 pub fn sign(&self, message: &[u8]) -> [u8; ED25519_SIGNATURE_LEN] {
185 self.signing_key.sign(message).to_bytes()
186 }
187
188 #[must_use]
189 pub fn sign_base64(&self, message: &[u8]) -> String {
190 BASE64.encode(self.sign(message))
191 }
192}
193
194impl std::fmt::Debug for NodeKeypair {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("NodeKeypair")
197 .field("public_key", &self.public_key_base64())
198 .finish_non_exhaustive()
199 }
200}
201
202pub fn keypair_from_base64(private_key_base64: &str) -> Result<NodeKeypair, CryptoError> {
208 let bytes = BASE64
209 .decode(private_key_base64.as_bytes())
210 .map_err(|source| {
211 tracing::warn!(err = %source, "base64 decode of private key failed");
212
213 CryptoError::invalid_base64(CryptoInput::PrivateKey, source)
214 })?;
215
216 NodeKeypair::from_private_key(&bytes)
217}
218
219pub fn verify_signature(
225 public_key: &[u8],
226 message: &[u8],
227 signature: &[u8],
228) -> Result<(), CryptoError> {
229 if public_key.len() != ED25519_PUBLIC_KEY_LEN {
230 return Err(CryptoError::invalid_public_key_length(public_key.len()));
231 }
232
233 if signature.len() != ED25519_SIGNATURE_LEN {
234 return Err(CryptoError::invalid_signature_length(signature.len()));
235 }
236
237 let mut pk_bytes = [0u8; ED25519_PUBLIC_KEY_LEN];
238
239 pk_bytes.copy_from_slice(public_key);
240 let verifying_key = VerifyingKey::from_bytes(&pk_bytes).map_err(|source| {
241 tracing::warn!(err = %source, "ed25519 public key parse failed");
242
243 CryptoError::invalid_public_key(source)
244 })?;
245
246 let mut sig_bytes = [0u8; ED25519_SIGNATURE_LEN];
247
248 sig_bytes.copy_from_slice(signature);
249 let sig = Signature::from_bytes(&sig_bytes);
250
251 verifying_key.verify(message, &sig).map_err(|source| {
252 tracing::warn!(err = %source, "ed25519 signature verification failed");
253
254 CryptoError::verification_failed(source)
255 })
256}
257
258pub fn verify_signature_base64(
264 public_key_base64: &str,
265 message: &[u8],
266 signature_base64: &str,
267) -> Result<(), CryptoError> {
268 let public_key = BASE64
269 .decode(public_key_base64.as_bytes())
270 .map_err(|source| {
271 tracing::warn!(err = %source, "base64 decode of public key failed");
272
273 CryptoError::invalid_base64(CryptoInput::PublicKey, source)
274 })?;
275 let signature = BASE64
276 .decode(signature_base64.as_bytes())
277 .map_err(|source| {
278 tracing::warn!(err = %source, "base64 decode of signature failed");
279
280 CryptoError::invalid_base64(CryptoInput::Signature, source)
281 })?;
282
283 verify_signature(&public_key, message, &signature)
284}
285
286#[must_use]
288pub fn sha256_hex(data: &[u8]) -> String {
289 let hash = Sha256::digest(data);
290
291 data_encoding::HEXLOWER.encode(&hash)
292}
293
294#[must_use]
297pub fn build_sign_message(
298 timestamp: u64,
299 method: &str,
300 host: &str,
301 path: &str,
302 body: &[u8],
303) -> String {
304 let body_hash = sha256_hex(body);
305 format!("{timestamp}\0{method}\0{host}\0{path}\0{body_hash}")
306}
307#[cfg(test)]
310mod crypto_tests {
311 use googletest::prelude::*;
312
313 use super::*;
314
315 #[gtest]
316 fn test_keypair_generation() -> Result<()> {
317 let kp1 = NodeKeypair::generate();
318 let kp2 = NodeKeypair::generate();
319
320 verify_that!(kp1.private_key_bytes(), not(eq(kp2.private_key_bytes())))?;
321 verify_that!(kp1.public_key_bytes(), not(eq(kp2.public_key_bytes())))?;
322
323 verify_that!(kp1.private_key_bytes().len(), eq(32))?;
324 verify_that!(kp1.public_key_bytes().len(), eq(32))?;
325
326 Ok(())
327 }
328
329 #[gtest]
330 fn test_keypair_from_private_key() -> Result<()> {
331 let original = NodeKeypair::generate();
332 let restored = NodeKeypair::from_private_key(&original.private_key_bytes()).or_fail()?;
333
334 verify_that!(
335 original.private_key_bytes(),
336 eq(restored.private_key_bytes())
337 )?;
338 verify_that!(original.public_key_bytes(), eq(restored.public_key_bytes()))?;
339
340 Ok(())
341 }
342
343 #[gtest]
344 fn test_keypair_from_base64() -> Result<()> {
345 let original = NodeKeypair::generate();
346 let restored = keypair_from_base64(&original.private_key_base64()).or_fail()?;
347
348 verify_that!(
349 original.private_key_bytes(),
350 eq(restored.private_key_bytes())
351 )?;
352 verify_that!(original.public_key_bytes(), eq(restored.public_key_bytes()))?;
353
354 Ok(())
355 }
356
357 #[gtest]
358 fn test_sign_and_verify() -> Result<()> {
359 let kp = NodeKeypair::generate();
360 let message = b"test message to sign";
361
362 let signature = kp.sign(message);
363
364 verify_that!(
365 verify_signature(&kp.public_key_bytes(), message, &signature),
366 ok(anything())
367 )?;
368
369 Ok(())
370 }
371
372 #[gtest]
373 fn test_sign_and_verify_base64() -> Result<()> {
374 let kp = NodeKeypair::generate();
375 let message = b"test message to sign";
376
377 let signature = kp.sign_base64(message);
378
379 verify_that!(
380 verify_signature_base64(&kp.public_key_base64(), message, &signature),
381 ok(anything())
382 )?;
383
384 Ok(())
385 }
386
387 #[gtest]
388 fn test_verify_wrong_message() -> Result<()> {
389 let kp = NodeKeypair::generate();
390 let message = b"original message";
391 let wrong_message = b"different message";
392
393 let signature = kp.sign(message);
394
395 let error = verify_signature(&kp.public_key_bytes(), wrong_message, &signature)
396 .expect_err("the signature must not verify against another message");
397
398 verify_that!(error.is_verification_failed(), eq(true))?;
399
400 Ok(())
401 }
402
403 #[gtest]
404 fn test_verify_wrong_key() -> Result<()> {
405 let kp1 = NodeKeypair::generate();
406 let kp2 = NodeKeypair::generate();
407 let message = b"test message";
408
409 let signature = kp1.sign(message);
410
411 let error = verify_signature(&kp2.public_key_bytes(), message, &signature)
412 .expect_err("the signature must not verify against another key");
413
414 verify_that!(error.is_verification_failed(), eq(true))?;
415
416 Ok(())
417 }
418
419 #[gtest]
420 fn test_verify_tampered_signature() -> Result<()> {
421 let kp = NodeKeypair::generate();
422 let message = b"test message";
423
424 let mut signature = kp.sign(message);
425
426 signature[0] ^= 0xFF;
427
428 let result = verify_signature(&kp.public_key_bytes(), message, &signature);
429
430 verify_that!(result, err(anything()))?;
431
432 Ok(())
433 }
434
435 #[gtest]
436 fn test_sha256_hex() -> Result<()> {
437 let data = b"hello world";
438 let hash = sha256_hex(data);
439
440 verify_that!(
441 hash,
442 eq("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
443 )?;
444
445 Ok(())
446 }
447
448 #[gtest]
449 fn test_build_sign_message() -> Result<()> {
450 let timestamp = 1_234_567_890_u64;
451 let method = "POST";
452 let host = "sentinel.wowlab.gg";
453 let path = "/nodes/register";
454 let body = b"{}";
455
456 let message = build_sign_message(timestamp, method, host, path, body);
457
458 let parts: Vec<&str> = message.split('\0').collect();
459
460 verify_that!(parts.len(), eq(5))?;
461 verify_that!(parts[0], eq("1234567890"))?;
462 verify_that!(parts[1], eq("POST"))?;
463 verify_that!(parts[2], eq("sentinel.wowlab.gg"))?;
464 verify_that!(parts[3], eq("/nodes/register"))?;
465 verify_that!(parts[4], eq(sha256_hex(body)))?;
466
467 Ok(())
468 }
469
470 #[gtest]
471 fn test_full_signing_flow() -> Result<()> {
472 let kp = NodeKeypair::generate();
473
474 let timestamp = 1_700_000_000_u64;
475 let method = "POST";
476 let host = "sentinel.wowlab.gg";
477 let path = "/chunks/complete";
478 let body = br#"{"chunkId":"abc"}"#;
479
480 let message = build_sign_message(timestamp, method, host, path, body);
481
482 let signature = kp.sign_base64(message.as_bytes());
483
484 verify_that!(
485 verify_signature_base64(&kp.public_key_base64(), message.as_bytes(), &signature),
486 ok(anything())
487 )?;
488
489 Ok(())
490 }
491}
492
493#[cfg(test)]
494mod crypto_input_tests {
495 use googletest::prelude::*;
496
497 use super::*;
498
499 #[gtest]
500 fn test_invalid_key_lengths() -> Result<()> {
501 let error = NodeKeypair::from_private_key(&[0u8; 16]).unwrap_err();
502
503 verify_that!(
504 error.to_string(),
505 eq("invalid private key length: expected 32 bytes, got 16")
506 )?;
507
508 let error = NodeKeypair::from_private_key(&[0u8; 64]).unwrap_err();
509
510 verify_that!(
511 error.to_string(),
512 eq("invalid private key length: expected 32 bytes, got 64")
513 )?;
514
515 let error = verify_signature(&[0u8; 16], b"msg", &[0u8; 64]).unwrap_err();
516
517 verify_that!(
518 error.to_string(),
519 eq("invalid public key length: expected 32 bytes, got 16")
520 )?;
521
522 let kp = NodeKeypair::generate();
523 let error = verify_signature(&kp.public_key_bytes(), b"msg", &[0u8; 32]).unwrap_err();
524
525 verify_that!(
526 error.to_string(),
527 eq("invalid signature length: expected 64 bytes, got 32")
528 )?;
529
530 Ok(())
531 }
532
533 #[gtest]
534 fn test_keypair_from_base64_decode_failure() -> Result<()> {
535 let error = keypair_from_base64("!!!not base64!!!").unwrap_err();
536
537 verify_that!(
538 error.to_string(),
539 eq("invalid private key base64: Invalid symbol 33, offset 0.")
540 )?;
541
542 Ok(())
543 }
544
545 #[gtest]
546 fn test_verify_signature_base64_public_key_decode_failure() -> Result<()> {
547 let error = verify_signature_base64("!!!bad", b"m", "!!!bad").unwrap_err();
548
549 verify_that!(
550 error.to_string(),
551 eq("invalid public key base64: Invalid symbol 33, offset 0.")
552 )?;
553
554 Ok(())
555 }
556
557 #[gtest]
558 fn test_verify_signature_structurally_invalid_public_key() -> Result<()> {
559 let error = verify_signature(&[0x02u8; 32], b"msg", &[0u8; 64]).unwrap_err();
560
561 verify_that!(error.to_string(), eq("invalid public key"))?;
562
563 Ok(())
564 }
565}