Skip to main content

wowlab_parsers/parsers/
crypto.rs

1//! Cryptographic utilities for node authentication.
2
3use 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/// Errors produced by the node-authentication crypto helpers.
13#[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/// Encoded input that failed a node-authentication crypto operation.
44#[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    /// Encoded input associated with this failure, when one input is responsible.
88    #[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    /// Whether this failure came from decoding an encoded input.
102    #[must_use]
103    pub const fn is_invalid_base64(&self) -> bool {
104        matches!(self.0, CryptoErrorKind::InvalidBase64 { .. })
105    }
106
107    /// Decoded byte length for a structurally valid base64 value of the wrong size.
108    #[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    /// Whether signature verification failed for otherwise valid inputs.
121    #[must_use]
122    pub const fn is_verification_failed(&self) -> bool {
123        matches!(self.0, CryptoErrorKind::VerificationFailed(_))
124    }
125}
126
127/// Ed25519 signing keypair used by nodes to authenticate with the control plane.
128#[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    /// Reconstruct a keypair from a raw Ed25519 private key.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`CryptoError`] when `bytes` has the wrong Ed25519 private-key length.
150    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
202/// Build a keypair from a base64-encoded private key.
203///
204/// # Errors
205///
206/// Returns [`CryptoError`] when the input is invalid base64 or has the wrong key length.
207pub 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
219/// Verify a signature against a public key.
220///
221/// # Errors
222///
223/// Returns [`CryptoError`] when the key or signature is malformed, or verification fails.
224pub 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
258/// Verify a signature using base64-encoded inputs.
259///
260/// # Errors
261///
262/// Returns [`CryptoError`] when either encoded value is invalid or verification fails.
263pub 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/// Compute SHA256 hash of data and return as hex string.
287#[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/// Build the message to sign for a node request.
295// docref:start hosted-compute-sign-message
296#[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// docref:end hosted-compute-sign-message
308
309#[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}