ironshield_types/
crypto.rs

1//! # Cryptographic operations for IronShield challenges
2//!
3//! This module provides Ed25519 signature generation and verification for IronShield challenges,
4//! including key management from environment variables and challenge signing/verification.
5//!
6//! ## Key Format Support
7//!
8//! The key loading functions support multiple formats with automatic detection:
9//! - **Raw Ed25519 Keys**: Base64-encoded 32-byte Ed25519 keys (legacy format)
10//! - **PGP Format**: Base64-encoded PGP keys (without ASCII armor headers)
11//!
12//! For PGP keys, a simple heuristic scans the binary data to find valid Ed25519 key material.
13//! This approach is simpler and more reliable than using complex PGP parsing libraries.
14//!
15//! ## Features
16//!
17//! ### Key Management
18//! * `load_private_key_from_env()`:            Load Ed25519 private key from environment
19//!                                             (multiple formats)
20//! * `load_public_key_from_env()`:             Load Ed25519 public key from environment
21//!                                             (multiple formats)
22//! * `generate_test_keypair()`:                Generate keypair for testing.
23//!
24//! ### Challenge Signing
25//! * `sign_challenge()`:                       Sign challenges with environment private key
26//! * `IronShieldChallenge::create_signed()`:   Create and sign challenges in one step
27//!
28//! ### Challenge Verification
29//! * `verify_challenge_signature()`:           Verify using environment public key
30//! * `verify_challenge_signature_with_key()`:  Verify using provided public key
31//! * `validate_challenge()`:                   Comprehensive challenge validation
32//!                                             (signature + expiration)
33//!
34//! ## Environment Variables
35//!
36//! The following environment variables are used for key storage:
37//! * `IRONSHIELD_PRIVATE_KEY`:                 Base64-encoded private key (PGP or raw Ed25519)
38//! * `IRONSHIELD_PUBLIC_KEY`:                  Base64-encoded public key (PGP or raw Ed25519)
39//!
40//! ## Examples
41//!
42//! ### Basic Usage with Raw Keys
43//! ```no_run
44//! use ironshield_types::{load_private_key_from_env, generate_test_keypair};
45//!
46//! // Generate test keys
47//! let (private_b64, public_b64) = generate_test_keypair();
48//! std::env::set_var("IRONSHIELD_PRIVATE_KEY", private_b64);
49//! std::env::set_var("IRONSHIELD_PUBLIC_KEY", public_b64);
50//!
51//! // Load keys from environment
52//! let signing_key = load_private_key_from_env().unwrap();
53//! ```
54//!
55//! ### Using with PGP Keys
56//! For PGP keys stored in Cloudflare Secrets Store (base64-encoded without armor):
57//! ```bash
58//! # Store PGP keys in Cloudflare Secrets Store
59//! wrangler secrets-store secret create STORE_ID \
60//!   --name IRONSHIELD_PRIVATE_KEY \
61//!   --value "LS0tLS1CRUdJTi..." \  # Base64 PGP data without headers
62//!   --scopes workers
63//! ```
64
65use base64::{
66    Engine,
67    engine::general_purpose::STANDARD
68};
69use ed25519_dalek::{
70    Signature,
71    Signer,
72    Verifier,
73    SigningKey,
74    VerifyingKey,
75    PUBLIC_KEY_LENGTH,
76    SECRET_KEY_LENGTH
77};
78use rand::rngs::OsRng;
79
80use crate::IronShieldChallenge;
81
82use std::env;
83
84/// Debug logging helper that works across different compilation targets
85macro_rules! debug_log {
86    ($($arg:tt)*) => {
87        #[cfg(all(target_arch = "wasm32", feature = "wasm-logging"))]
88        {
89            let msg = format!($($arg)*);
90            web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&msg));
91        }
92        #[cfg(not(target_arch = "wasm32"))]
93        eprintln!($($arg)*);
94        #[cfg(all(target_arch = "wasm32", not(feature = "wasm-logging")))]
95        {
96            // No-op for WASM without logging feature
97            let _ = format!($($arg)*);
98        }
99    };
100}
101
102#[derive(Debug, Clone)]
103pub enum CryptoError {
104    MissingEnvironmentVariable(String),
105    InvalidKeyFormat(String),
106    SigningFailed(String),
107    VerificationFailed(String),
108    Base64DecodingFailed(String),
109    PgpParsingFailed(String),
110}
111
112impl std::fmt::Display for CryptoError {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            CryptoError::MissingEnvironmentVariable(var) => write!(f, "Missing environment variable: {}", var),
116            CryptoError::InvalidKeyFormat(msg) => write!(f, "Invalid key format: {}", msg),
117            CryptoError::SigningFailed(msg) => write!(f, "Signing failed: {}", msg),
118            CryptoError::VerificationFailed(msg) => write!(f, "Verification failed: {}", msg),
119            CryptoError::Base64DecodingFailed(msg) => write!(f, "Base64 decoding failed: {}", msg),
120            CryptoError::PgpParsingFailed(msg) => write!(f, "PGP parsing failed: {}", msg),
121        }
122    }
123}
124
125impl std::error::Error for CryptoError {}
126
127/// Parse key data with simple heuristic approach (handles PGP and raw Ed25519)
128///
129/// This function attempts to extract Ed25519 key material from various formats:
130/// 1. PGP armored text (base64 with possible line breaks)
131/// 2. Raw base64-encoded Ed25519 keys (32 bytes)
132///
133/// # Arguments
134/// * `key_data`:   Key data as string (PGP armored or raw base64)
135/// * `is_private`: Whether this is a private key (for validation)
136///
137/// # Returns
138/// * `Result<[u8; 32], CryptoError>`: The 32-byte Ed25519 key
139fn parse_key_simple(key_data: &str, is_private: bool) -> Result<[u8; 32], CryptoError> {
140    // Clean the key data by removing all whitespace, line breaks, and common PGP formatting
141    let cleaned_data = key_data
142        .chars()
143        .filter(|c| !c.is_whitespace()) // Remove all whitespace including \n, \r, \t, spaces
144        .collect::<String>();
145
146    debug_log!("🔑 Parsing key data: {} chars → {} chars after cleaning", key_data.len(), cleaned_data.len());
147
148    // Check for any invalid base64 characters
149    let invalid_chars: Vec<char> = cleaned_data
150        .chars()
151        .filter(|&c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
152        .collect();
153
154    if !invalid_chars.is_empty() {
155        debug_log!("🔧 Fixing {} invalid base64 characters", invalid_chars.len());
156
157        // Try to fix common issues
158        let fixed_data = cleaned_data
159            .chars()
160            .filter(|&c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
161            .collect::<String>();
162
163        debug_log!("🔧 Fixed data length: {}", fixed_data.len());
164
165        // Try to decode the fixed data
166        match STANDARD.decode(&fixed_data) {
167            Ok(key_bytes) => {
168                debug_log!("✅ Fixed data decoded to {} bytes", key_bytes.len());
169                return try_extract_ed25519_key(&key_bytes, is_private);
170            }
171            Err(e) => {
172                debug_log!("⚠️ Fixed data decode failed: {}", e);
173            }
174        }
175    }
176
177    // Try to decode as base64
178    let key_bytes = match STANDARD.decode(&cleaned_data) {
179        Ok(bytes) => {
180            debug_log!("✅ Base64 decoded to {} bytes", bytes.len());
181            bytes
182        }
183        Err(e) => {
184            debug_log!("⚠️ Base64 decode failed: {}", e);
185
186            // Try removing trailing characters that might be corrupted
187            let mut test_data = cleaned_data.clone();
188            while !test_data.is_empty() {
189                if let Ok(bytes) = STANDARD.decode(&test_data) {
190                    debug_log!("✅ Successful decode after trimming to {} chars → {} bytes", test_data.len(), bytes.len());
191                    return try_extract_ed25519_key(&bytes, is_private);
192                }
193                test_data.pop();
194            }
195
196            return Err(CryptoError::Base64DecodingFailed(format!("Failed to decode cleaned key data: {}", e)));
197        }
198    };
199
200    try_extract_ed25519_key(&key_bytes, is_private)
201}
202
203/// Extract Ed25519 key material from decoded bytes
204fn try_extract_ed25519_key(key_bytes: &[u8], is_private: bool) -> Result<[u8; 32], CryptoError> {
205    debug_log!("🔑 Extracting Ed25519 key from {} bytes", key_bytes.len());
206
207    // If it's exactly 32 bytes, it might be a raw Ed25519 key
208    if key_bytes.len() == 32 {
209        let mut key_array = [0u8; 32];
210        key_array.copy_from_slice(&key_bytes);
211
212        // Validate the key
213        if is_private {
214            let _signing_key = SigningKey::from_bytes(&key_array);
215            debug_log!("✅ Raw Ed25519 private key validated");
216        } else {
217            let _verifying_key = VerifyingKey::from_bytes(&key_array)
218                .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid raw public key: {}", e)))?;
219            debug_log!("✅ Raw Ed25519 public key validated");
220        }
221
222        return Ok(key_array);
223    }
224
225    // For larger data (PGP format), use multiple sophisticated key extraction strategies
226    if key_bytes.len() >= 32 {
227        debug_log!("🔍 Scanning PGP data for Ed25519 key...");
228
229        // Strategy 1: Look for Ed25519 algorithm identifier (0x16 = 22 decimal)
230        // Ed25519 keys in PGP often have specific patterns
231        for window_start in 0..key_bytes.len().saturating_sub(32) {
232            let potential_key = &key_bytes[window_start..window_start + 32];
233
234            // Skip obviously invalid keys (all zeros, all 0xFF, or patterns that don't make sense)
235            if potential_key == &[0u8; 32] || potential_key == &[0xFFu8; 32] {
236                continue;
237            }
238
239            // For Ed25519, check if this looks like valid key material
240            let mut key_array = [0u8; 32];
241            key_array.copy_from_slice(potential_key);
242
243            if is_private {
244                // For private keys, try to create a SigningKey and derive the public key
245                let signing_key = SigningKey::from_bytes(&key_array);
246                let derived_public = signing_key.verifying_key();
247
248                // Additional validation: check if the derived public key appears elsewhere in the PGP data
249                let public_bytes = derived_public.to_bytes();
250
251                // Look for the derived public key in the remaining PGP data
252                let search_start = window_start + 32;
253                if search_start < key_bytes.len() {
254                    let remaining_data = &key_bytes[search_start..];
255                    if remaining_data.windows(32).any(|window| window == public_bytes) {
256                        debug_log!("✅ Private key found at offset {} (with matching public key)", window_start);
257                        return Ok(key_array);
258                    }
259                }
260
261                // Even if we don't find the public key, if this is at a reasonable offset, it might be valid
262                if window_start >= 20 && window_start <= 200 {
263                    debug_log!("✅ Private key found at offset {}", window_start);
264                    return Ok(key_array);
265                }
266            } else {
267                // For public keys, try to create a VerifyingKey
268                if let Ok(_verifying_key) = VerifyingKey::from_bytes(&key_array) {
269                    // Additional validation: public keys should appear after some PGP header data
270                    if window_start >= 10 && window_start <= 100 {
271                        debug_log!("✅ Public key found at offset {}", window_start);
272                        return Ok(key_array);
273                    }
274                }
275            }
276        }
277
278        // Strategy 2: Look for specific PGP packet patterns
279        for (i, &byte) in key_bytes.iter().enumerate() {
280            if byte == 0x16 && i + 33 < key_bytes.len() { // Algorithm 22 (Ed25519) + 32 bytes key
281                let key_start = i + 1;
282                if key_start + 32 <= key_bytes.len() {
283                    let potential_key = &key_bytes[key_start..key_start + 32];
284                    let mut key_array = [0u8; 32];
285                    key_array.copy_from_slice(potential_key);
286
287                    // Validate this key
288                    if is_private {
289                        let _signing_key = SigningKey::from_bytes(&key_array);
290                        debug_log!("✅ Private key found via algorithm ID at offset {}", key_start);
291                        return Ok(key_array);
292                    } else {
293                        if let Ok(_verifying_key) = VerifyingKey::from_bytes(&key_array) {
294                            debug_log!("✅ Public key found via algorithm ID at offset {}", key_start);
295                            return Ok(key_array);
296                        }
297                    }
298                }
299            }
300        }
301
302        // Strategy 3: Look for keys at common PGP offsets
303        let common_offsets = [
304            32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100,
305            104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160
306        ];
307
308        for &offset in &common_offsets {
309            if offset + 32 <= key_bytes.len() {
310                let potential_key = &key_bytes[offset..offset + 32];
311
312                // Skip obviously invalid patterns
313                if potential_key == &[0u8; 32] || potential_key == &[0xFFu8; 32] {
314                    continue;
315                }
316
317                let mut key_array = [0u8; 32];
318                key_array.copy_from_slice(potential_key);
319
320                if is_private {
321                    let _signing_key = SigningKey::from_bytes(&key_array);
322                    debug_log!("✅ Private key found at common offset {}", offset);
323                    return Ok(key_array);
324                } else {
325                    if let Ok(_verifying_key) = VerifyingKey::from_bytes(&key_array) {
326                        debug_log!("✅ Public key found at common offset {}", offset);
327                        return Ok(key_array);
328                    }
329                }
330            }
331        }
332    }
333
334    Err(CryptoError::PgpParsingFailed(format!(
335        "Could not find valid Ed25519 key material in {} bytes of PGP data using multiple strategies",
336        key_bytes.len()
337    )))
338}
339
340/// Loads the private key from the IRONSHIELD_PRIVATE_KEY environment variable
341///
342/// The environment variable should contain a base64-encoded PGP private key (without armor headers).
343/// For backward compatibility, raw base64-encoded Ed25519 keys (32 bytes) are also supported.
344///
345/// # Returns
346/// * `Result<SigningKey, CryptoError>`: The Ed25519 signing key or an error
347///
348/// # Environment Variables
349/// * `IRONSHIELD_PRIVATE_KEY`:          Base64-encoded PGP private key data
350///                                      (without -----BEGIN/END----- lines)
351///                                      or raw base64-encoded Ed25519 private
352///                                      key (legacy format)
353pub fn load_private_key_from_env() -> Result<SigningKey, CryptoError> {
354    let key_str: String = env::var("IRONSHIELD_PRIVATE_KEY")
355        .map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PRIVATE_KEY".to_string()))?;
356
357    // Try PGP format first
358    match parse_key_simple(&key_str, true) {
359        Ok(key_array) => {
360            let signing_key: SigningKey = SigningKey::from_bytes(&key_array);
361            return Ok(signing_key);
362        }
363        Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
364            // Fall back to raw base64 format
365        }
366        Err(e) => return Err(e), // Return other errors immediately
367    }
368
369    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
370    let key_bytes: Vec<u8> = STANDARD.decode(key_str.trim())
371        .map_err(|e| CryptoError::Base64DecodingFailed(format!("Private key (legacy fallback): {}", e)))?;
372
373    // Verify length for raw Ed25519 key
374    if key_bytes.len() != SECRET_KEY_LENGTH {
375        return Err(CryptoError::InvalidKeyFormat(
376            format!("Private key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
377                   SECRET_KEY_LENGTH, key_bytes.len())
378        ));
379    }
380
381    // Create signing key from raw bytes
382    let key_array: [u8; SECRET_KEY_LENGTH] = key_bytes.try_into()
383        .map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert private key bytes".to_string()))?;
384
385    let signing_key: SigningKey = SigningKey::from_bytes(&key_array);
386    Ok(signing_key)
387}
388
389/// Loads the public key from the IRONSHIELD_PUBLIC_KEY environment variable
390///
391/// The environment variable should contain a base64-encoded PGP public key (without armor headers).
392/// For backward compatibility, raw base64-encoded Ed25519 keys (32 bytes) are also supported.
393///
394/// # Returns
395/// * `Result<VerifyingKey, CryptoError>`: The Ed25519 verifying key or an error
396///
397/// # Environment Variables
398/// * `IRONSHIELD_PUBLIC_KEY`: Base64-encoded PGP public key data
399///                            (without -----BEGIN/END----- lines)
400///                            or raw base64-encoded Ed25519 public key
401///                            (legacy format)
402pub fn load_public_key_from_env() -> Result<VerifyingKey, CryptoError> {
403    let key_str: String = env::var("IRONSHIELD_PUBLIC_KEY")
404        .map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PUBLIC_KEY".to_string()))?;
405
406    // Try PGP format first
407    match parse_key_simple(&key_str, false) {
408        Ok(key_array) => {
409            let verifying_key: VerifyingKey = VerifyingKey::from_bytes(&key_array)
410                .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
411            return Ok(verifying_key);
412        }
413        Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
414            // Fall back to raw base64 format
415        }
416        Err(e) => return Err(e), // Return other errors immediately
417    }
418
419    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
420    let key_bytes: Vec<u8> = STANDARD.decode(key_str.trim())
421        .map_err(|e| CryptoError::Base64DecodingFailed(format!("Public key (legacy fallback): {}", e)))?;
422
423    // Verify length for raw Ed25519 key
424    if key_bytes.len() != PUBLIC_KEY_LENGTH {
425        return Err(CryptoError::InvalidKeyFormat(
426            format!("Public key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
427                   PUBLIC_KEY_LENGTH, key_bytes.len())
428        ));
429    }
430
431    // Create verifying key from raw bytes
432    let key_array: [u8; PUBLIC_KEY_LENGTH] = key_bytes.try_into()
433        .map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert public key bytes".to_string()))?;
434
435    let verifying_key: VerifyingKey = VerifyingKey::from_bytes(&key_array)
436        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
437
438    Ok(verifying_key)
439}
440
441/// Creates a message to be signed from challenge data components
442///
443/// This function creates a canonical representation of the challenge data for signing.
444/// It takes individual challenge components rather than a complete challenge object,
445/// allowing it to be used during challenge creation.
446///
447/// # Arguments
448/// * `random_nonce`:    The random nonce string
449/// * `created_time`:    The challenge creation timestamp
450/// * `expiration_time`: The challenge expiration timestamp
451/// * `website_id`:      The website identifier
452/// * `challenge_param`: The challenge parameter bytes
453/// * `public_key`:      The public key bytes
454///
455/// # Returns
456/// * `String`: Canonical string representation for signing
457pub fn create_signing_message(
458    random_nonce: &str,
459    created_time: i64,
460    expiration_time: i64,
461    website_id: &str,
462    challenge_param: &[u8; 32],
463    public_key: &[u8; 32]
464) -> String {
465    format!(
466        "{}|{}|{}|{}|{}|{}",
467        random_nonce,
468        created_time,
469        expiration_time,
470        website_id,
471        hex::encode(challenge_param),
472        hex::encode(public_key)
473    )
474}
475
476/// Generates an Ed25519 signature for a given message using the provided signing key
477///
478/// This is a low-level function for generating signatures. For challenge signing,
479/// consider using `sign_challenge` which handles message creation automatically.
480///
481/// # Arguments
482/// * `signing_key`: The Ed25519 signing key to use
483/// * `message`:     The message to sign (will be converted to bytes)
484///
485/// # Returns
486/// * `Result<[u8; 64], CryptoError>`: The signature bytes or an error
487///
488/// # Example
489/// ```no_run
490/// use ironshield_types::{generate_signature, load_private_key_from_env};
491///
492/// let signing_key = load_private_key_from_env()?;
493/// let signature = generate_signature(&signing_key, "message to sign")?;
494/// # Ok::<(), ironshield_types::CryptoError>(())
495/// ```
496pub fn generate_signature(signing_key: &SigningKey, message: &str) -> Result<[u8; 64], CryptoError> {
497    let signature: Signature = signing_key.sign(message.as_bytes());
498    Ok(signature.to_bytes())
499}
500
501/// Signs a challenge using the private key from environment variables.
502///
503/// This function creates a signature over all challenge fields except the signature itself.
504/// The private key is loaded from the IRONSHIELD_PRIVATE_KEY environment variable.
505///
506/// # Arguments
507/// * `challenge`: The challenge to sign (signature field will be ignored).
508///
509/// # Returns
510/// * `Result<[u8; 64], CryptoError>`: The Ed25519 signature bytes or an error.
511///
512/// # Example
513/// ```no_run
514/// use ironshield_types::{IronShieldChallenge, sign_challenge, SigningKey};
515///
516/// let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
517/// let mut challenge = IronShieldChallenge::new(
518///     "test_website".to_string(),
519///     100_000,
520///     dummy_key,
521///     [0x34; 32],
522/// );
523///
524/// // Sign the challenge (requires IRONSHIELD_PRIVATE_KEY environment variable)
525/// let signature = sign_challenge(&challenge).unwrap();
526/// challenge.challenge_signature = signature;
527/// ```
528pub fn sign_challenge(challenge: &IronShieldChallenge) -> Result<[u8; 64], CryptoError> {
529    let signing_key: SigningKey = load_private_key_from_env()?;
530    let message: String = create_signing_message(
531        &challenge.random_nonce,
532        challenge.created_time,
533        challenge.expiration_time,
534        &challenge.website_id,
535        &challenge.challenge_param,
536        &challenge.public_key
537    );
538    generate_signature(&signing_key, &message)
539}
540
541/// Verifies a challenge signature using the public key from environment variables
542///
543/// This function verifies that the challenge signature is valid and that the challenge
544/// data has not been tampered with. The public key is loaded from the IRONSHIELD_PUBLIC_KEY
545/// environment variable.
546///
547/// # Arguments
548/// * `challenge`: The challenge with signature to verify.
549///
550/// # Returns
551/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if verification fails.
552///
553/// # Example
554/// ```no_run
555/// use ironshield_types::{IronShieldChallenge, verify_challenge_signature, SigningKey};
556///
557/// let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
558/// let challenge = IronShieldChallenge::new(
559///     "test_website".to_string(),
560///     100_000,
561///     dummy_key,
562///     [0x34; 32],
563/// );
564///
565/// // Verify the challenge (requires IRONSHIELD_PUBLIC_KEY environment variable)
566/// verify_challenge_signature(&challenge).unwrap();
567/// ```
568pub fn verify_challenge_signature(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
569    let verifying_key: VerifyingKey = load_public_key_from_env()?;
570
571    let message: String = create_signing_message(
572        &challenge.random_nonce,
573        challenge.created_time,
574        challenge.expiration_time,
575        &challenge.website_id,
576        &challenge.challenge_param,
577        &challenge.public_key
578    );
579    let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
580        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
581
582    verifying_key.verify(message.as_bytes(), &signature)
583        .map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
584
585    Ok(())
586}
587
588/// Verifies a challenge signature using a provided public key
589///
590/// This function is similar to `verify_challenge_signature` but uses a provided
591/// public key instead of loading from environment variables. This is useful for
592/// client-side verification where the public key is embedded in the challenge.
593///
594/// # Arguments
595/// * `challenge`:        The challenge with signature to verify
596/// * `public_key_bytes`: The Ed25519 public key bytes to use for verification
597///
598/// # Returns
599/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if verification fails
600pub fn verify_challenge_signature_with_key(
601    challenge: &IronShieldChallenge,
602    public_key_bytes: &[u8; 32]
603) -> Result<(), CryptoError> {
604    let verifying_key: VerifyingKey = VerifyingKey::from_bytes(public_key_bytes)
605        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
606
607    let message: String = create_signing_message(
608        &challenge.random_nonce,
609        challenge.created_time,
610        challenge.expiration_time,
611        &challenge.website_id,
612        &challenge.challenge_param,
613        &challenge.public_key
614    );
615    let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
616        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
617
618    verifying_key.verify(message.as_bytes(), &signature)
619        .map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
620
621    Ok(())
622}
623
624/// Generates a new Ed25519 keypair for testing purposes
625///
626/// This function generates a fresh keypair and returns the keys in raw base64 format
627/// (legacy format) suitable for use as environment variables in tests.
628///
629/// # Returns
630/// * `(String, String)`: (base64_private_key, base64_public_key) in raw Ed25519 format
631///
632/// # Example
633/// ```
634/// use ironshield_types::generate_test_keypair;
635///
636/// let (private_key_b64, public_key_b64) = generate_test_keypair();
637/// std::env::set_var("IRONSHIELD_PRIVATE_KEY", private_key_b64);
638/// std::env::set_var("IRONSHIELD_PUBLIC_KEY", public_key_b64);
639/// ```
640pub fn generate_test_keypair() -> (String, String) {
641    let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
642    let verifying_key: VerifyingKey = signing_key.verifying_key();
643
644    let private_key_b64: String = STANDARD.encode(signing_key.to_bytes());
645    let public_key_b64: String = STANDARD.encode(verifying_key.to_bytes());
646
647    (private_key_b64, public_key_b64)
648}
649
650/// Verifies a challenge and checks if it's valid and not expired
651///
652/// This is a comprehensive validation function that checks:
653/// - Signature validity
654/// - Challenge expiration
655/// - Basic format validation
656///
657/// # Arguments
658/// * `challenge`: The challenge to validate
659///
660/// # Returns
661/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if invalid
662pub fn validate_challenge(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
663    // Check signature first
664    verify_challenge_signature(challenge)?;
665
666    // Check expiration
667    if challenge.is_expired() {
668        return Err(CryptoError::VerificationFailed("Challenge has expired".to_string()));
669    }
670
671    if challenge.website_id.is_empty() {
672        return Err(CryptoError::VerificationFailed("Empty website_id".to_string()));
673    }
674
675    Ok(())
676}
677
678/// Loads a private key from raw key data (for Cloudflare Workers)
679///
680/// This function is designed for use with Cloudflare Workers where secrets
681/// are accessible through the env parameter rather than standard environment variables.
682///
683/// # Arguments
684/// * `key_data`: Base64-encoded key data (PGP or raw Ed25519)
685///
686/// # Returns
687/// * `Result<SigningKey, CryptoError>`: The Ed25519 signing key or an error
688pub fn load_private_key_from_data(key_data: &str) -> Result<SigningKey, CryptoError> {
689    // Try PGP format first
690    match parse_key_simple(key_data, true) {
691        Ok(key_array) => {
692            let signing_key: SigningKey = SigningKey::from_bytes(&key_array);
693            return Ok(signing_key);
694        }
695        Err(CryptoError::PgpParsingFailed(_msg)) => {
696            // Fall back to raw base64 format
697        }
698        Err(CryptoError::Base64DecodingFailed(_msg)) => {
699            // Fall back to raw base64 format
700        }
701        Err(e) => {
702            return Err(e); // Return other errors immediately
703        }
704    }
705
706    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
707    let key_bytes: Vec<u8> = STANDARD.decode(key_data.trim())
708        .map_err(|e| {
709            CryptoError::Base64DecodingFailed(format!("Private key (legacy fallback): {}", e))
710        })?;
711
712    // Verify length for raw Ed25519 key
713    if key_bytes.len() != SECRET_KEY_LENGTH {
714        let error_msg = format!(
715            "Invalid key length: expected {} bytes for Ed25519 private key, got {} bytes",
716            SECRET_KEY_LENGTH,
717            key_bytes.len()
718        );
719        return Err(CryptoError::InvalidKeyFormat(error_msg));
720    }
721
722    let mut key_array = [0u8; SECRET_KEY_LENGTH];
723    key_array.copy_from_slice(&key_bytes);
724
725    Ok(SigningKey::from_bytes(&key_array))
726}
727
728/// Loads a public key from raw key data (for Cloudflare Workers)
729///
730/// This function is designed for use with Cloudflare Workers where secrets
731/// are accessible through the env parameter rather than standard environment variables.
732///
733/// # Arguments
734/// * `key_data`: Base64-encoded key data (PGP or raw Ed25519)
735///
736/// # Returns
737/// * `Result<VerifyingKey, CryptoError>`: The Ed25519 verifying key or an error
738pub fn load_public_key_from_data(key_data: &str) -> Result<VerifyingKey, CryptoError> {
739    // Try PGP format first
740    match parse_key_simple(key_data, false) {
741        Ok(key_array) => {
742            let verifying_key = VerifyingKey::from_bytes(&key_array)
743                .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key from PGP: {}", e)))?;
744            return Ok(verifying_key);
745        }
746        Err(CryptoError::PgpParsingFailed(_msg)) => {
747            // Fall back to raw base64 format
748        }
749        Err(CryptoError::Base64DecodingFailed(_msg)) => {
750            // Fall back to raw base64 format
751        }
752        Err(e) => {
753            return Err(e); // Return other errors immediately
754        }
755    }
756
757    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
758    let key_bytes: Vec<u8> = STANDARD.decode(key_data.trim())
759        .map_err(|e| {
760            CryptoError::Base64DecodingFailed(format!("Public key (legacy fallback): {}", e))
761        })?;
762
763    // Verify length for raw Ed25519 key
764    if key_bytes.len() != PUBLIC_KEY_LENGTH {
765        let error_msg = format!(
766            "Invalid key length: expected {} bytes for Ed25519 public key, got {} bytes",
767            PUBLIC_KEY_LENGTH,
768            key_bytes.len()
769        );
770        return Err(CryptoError::InvalidKeyFormat(error_msg));
771    }
772
773    let mut key_array = [0u8; PUBLIC_KEY_LENGTH];
774    key_array.copy_from_slice(&key_bytes);
775
776    let verifying_key = VerifyingKey::from_bytes(&key_array)
777        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid Ed25519 public key: {}", e)))?;
778
779    Ok(verifying_key)
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use std::env;
786    use std::sync::Mutex;
787    use rand::rngs::OsRng;
788
789    // Use a mutex to ensure tests don't interfere with each other when setting env vars
790    static ENV_MUTEX: Mutex<()> = Mutex::new(());
791
792    #[allow(dead_code)]
793    fn setup_isolated_test_keys() -> (SigningKey, VerifyingKey) {
794        let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
795        let verifying_key: VerifyingKey = signing_key.verifying_key();
796
797        let private_key: String = STANDARD.encode(signing_key.to_bytes());
798        let public_key: String = STANDARD.encode(verifying_key.to_bytes());
799
800        // Set environment variables with mutex protection
801        let _lock = ENV_MUTEX.lock().unwrap();
802        env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
803        env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
804
805        (signing_key, verifying_key)
806    }
807
808    #[test]
809    fn test_basic_ed25519_signing() {
810        // Test basic Ed25519 signing with a simple message
811        let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
812        let verifying_key: VerifyingKey = signing_key.verifying_key();
813
814        let message = b"Hello, world!";
815        let signature: Signature = signing_key.sign(message);
816
817        // This should work without any issues
818        let result = verifying_key.verify(message, &signature);
819        assert!(result.is_ok(), "Basic Ed25519 signing should work");
820    }
821
822    #[test]
823    fn test_crypto_integration_without_env() {
824        // Generate keys directly without using environment variables
825        let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
826        let verifying_key: VerifyingKey = signing_key.verifying_key();
827
828        // Create a challenge with the public key
829        let challenge = IronShieldChallenge::new(
830            "example.com".to_string(),
831            100_000,
832            signing_key.clone(),
833            verifying_key.to_bytes(),
834        );
835
836        // Create the signing message manually
837        let signing_message = create_signing_message(
838            &challenge.random_nonce,
839            challenge.created_time,
840            challenge.expiration_time,
841            &challenge.website_id,
842            &challenge.challenge_param,
843            &challenge.public_key
844        );
845        println!("Signing message: {}", signing_message);
846
847        // The challenge should already be signed, so let's verify it
848        let verification_message = create_signing_message(
849            &challenge.random_nonce,
850            challenge.created_time,
851            challenge.expiration_time,
852            &challenge.website_id,
853            &challenge.challenge_param,
854            &challenge.public_key
855        );
856        assert_eq!(signing_message, verification_message, "Signing message should be consistent");
857
858        let signature_from_bytes = Signature::from_slice(&challenge.challenge_signature)
859            .expect("Should be able to recreate signature from bytes");
860
861        let verification_result = verifying_key.verify(verification_message.as_bytes(), &signature_from_bytes);
862        assert!(verification_result.is_ok(), "Manual verification should succeed");
863
864        // Now test our helper function
865        let verify_result = verify_challenge_signature_with_key(&challenge, &verifying_key.to_bytes());
866        assert!(verify_result.is_ok(), "verify_challenge_signature_with_key should succeed");
867    }
868
869    #[test]
870    fn test_generate_test_keypair() {
871        let (private_key, public_key) = generate_test_keypair();
872
873        // Keys should be valid base64
874        assert!(STANDARD.decode(&private_key).is_ok());
875        assert!(STANDARD.decode(&public_key).is_ok());
876
877        // Keys should be correct length when decoded
878        let private_bytes = STANDARD.decode(&private_key).unwrap();
879        let public_bytes = STANDARD.decode(&public_key).unwrap();
880        assert_eq!(private_bytes.len(), SECRET_KEY_LENGTH);
881        assert_eq!(public_bytes.len(), PUBLIC_KEY_LENGTH);
882    }
883
884    #[test]
885    fn test_load_keys_from_env() {
886        let _lock = ENV_MUTEX.lock().unwrap();
887
888        let (signing_key, verifying_key) = {
889            let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
890            let verifying_key: VerifyingKey = signing_key.verifying_key();
891
892            let private_key: String = STANDARD.encode(signing_key.to_bytes());
893            let public_key: String = STANDARD.encode(verifying_key.to_bytes());
894
895            env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
896            env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
897
898            (signing_key, verifying_key)
899        };
900
901        // Should successfully load keys
902        let loaded_signing_key = load_private_key_from_env().unwrap();
903        let loaded_verifying_key = load_public_key_from_env().unwrap();
904
905        // Keys should match what we set
906        assert_eq!(signing_key.to_bytes(), loaded_signing_key.to_bytes());
907        assert_eq!(verifying_key.to_bytes(), loaded_verifying_key.to_bytes());
908    }
909
910    #[test]
911    fn test_missing_environment_variables() {
912        let _lock = ENV_MUTEX.lock().unwrap();
913
914        // Remove environment variables for this test
915        env::remove_var("IRONSHIELD_PRIVATE_KEY");
916        env::remove_var("IRONSHIELD_PUBLIC_KEY");
917
918        // Should fail with appropriate errors
919        let private_result = load_private_key_from_env();
920        assert!(private_result.is_err());
921        assert!(matches!(private_result.unwrap_err(), CryptoError::MissingEnvironmentVariable(_)));
922
923        let public_result = load_public_key_from_env();
924        assert!(public_result.is_err());
925        assert!(matches!(public_result.unwrap_err(), CryptoError::MissingEnvironmentVariable(_)));
926    }
927
928    #[test]
929    fn test_invalid_key_format() {
930        let _lock = ENV_MUTEX.lock().unwrap();
931
932        // Set invalid keys
933        env::set_var("IRONSHIELD_PRIVATE_KEY", "invalid-base64!");
934        env::set_var("IRONSHIELD_PUBLIC_KEY", "invalid-base64!");
935
936        let private_result = load_private_key_from_env();
937        assert!(private_result.is_err());
938        assert!(matches!(private_result.unwrap_err(), CryptoError::Base64DecodingFailed(_)));
939
940        let public_result = load_public_key_from_env();
941        assert!(public_result.is_err());
942        assert!(matches!(public_result.unwrap_err(), CryptoError::Base64DecodingFailed(_)));
943    }
944
945    #[test]
946    fn test_challenge_signing_and_verification() {
947        let _lock = ENV_MUTEX.lock().unwrap();
948
949        let (signing_key, verifying_key) = {
950            let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
951            let verifying_key: VerifyingKey = signing_key.verifying_key();
952
953            let private_key: String = STANDARD.encode(signing_key.to_bytes());
954            let public_key: String = STANDARD.encode(verifying_key.to_bytes());
955
956            env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
957            env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
958
959            (signing_key, verifying_key)
960        };
961
962        // Create a test challenge - it will be automatically signed
963        let challenge = IronShieldChallenge::new(
964            "test_website".to_string(),
965            100_000,
966            signing_key.clone(),
967            verifying_key.to_bytes(),
968        );
969
970        // Verify the signature with environment keys
971        verify_challenge_signature(&challenge).unwrap();
972
973        // Verify with explicit key
974        verify_challenge_signature_with_key(&challenge, &verifying_key.to_bytes()).unwrap();
975
976        // Verify that the embedded public key matches what we expect
977        assert_eq!(challenge.public_key, verifying_key.to_bytes());
978    }
979
980    #[test]
981    fn test_tampered_challenge_detection() {
982        let _lock = ENV_MUTEX.lock().unwrap();
983
984        let (signing_key, verifying_key) = {
985            let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
986            let verifying_key: VerifyingKey = signing_key.verifying_key();
987
988            let private_key: String = STANDARD.encode(signing_key.to_bytes());
989            let public_key: String = STANDARD.encode(verifying_key.to_bytes());
990
991            env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
992            env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
993
994            (signing_key, verifying_key)
995        };
996
997        // Create and sign a challenge - signature is generated automatically
998        let mut challenge = IronShieldChallenge::new(
999            "test_website".to_string(),
1000            100_000,
1001            signing_key.clone(),
1002            verifying_key.to_bytes(),
1003        );
1004
1005        // Verify original challenge works
1006        verify_challenge_signature(&challenge).unwrap();
1007
1008        // Tamper with the challenge
1009        challenge.random_nonce = "tampered".to_string();
1010
1011        // Verification should fail
1012        let result = verify_challenge_signature(&challenge);
1013        assert!(result.is_err());
1014        assert!(matches!(result.unwrap_err(), CryptoError::VerificationFailed(_)));
1015    }
1016
1017    #[test]
1018    fn test_invalid_signature_format() {
1019        let _lock = ENV_MUTEX.lock().unwrap();
1020        {
1021            let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
1022            let verifying_key: VerifyingKey = signing_key.verifying_key();
1023
1024            let private_key: String = STANDARD.encode(signing_key.to_bytes());
1025            let public_key: String = STANDARD.encode(verifying_key.to_bytes());
1026
1027            env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
1028            env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
1029        }
1030
1031        // Create a challenge that will be properly signed
1032        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
1033        let mut challenge = IronShieldChallenge::new(
1034            "test_website".to_string(),
1035            100_000,
1036            dummy_key,
1037            [0x34; 32],
1038        );
1039
1040        // Now manually corrupt the signature to test invalid format
1041        challenge.challenge_signature = [0xFF; 64]; // Invalid signature
1042
1043        // Verification should fail
1044        let result = verify_challenge_signature(&challenge);
1045        assert!(result.is_err());
1046    }
1047
1048    #[test]
1049    fn test_signing_message_creation() {
1050        let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
1051        let challenge = IronShieldChallenge::new(
1052            "test_website".to_string(),
1053            100_000,
1054            dummy_key,
1055            [0x34; 32],
1056        );
1057
1058        let message = create_signing_message(
1059            &challenge.random_nonce,
1060            challenge.created_time,
1061            challenge.expiration_time,
1062            &challenge.website_id,
1063            &challenge.challenge_param,
1064            &challenge.public_key
1065        );
1066
1067        // Ensure the message format is as expected
1068        let expected_prefix = format!(
1069            "{}|{}|{}|{}|",
1070            challenge.random_nonce,
1071            challenge.created_time,
1072            challenge.expiration_time,
1073            challenge.website_id
1074        );
1075        assert!(message.starts_with(&expected_prefix));
1076        assert!(message.ends_with(&hex::encode(challenge.public_key)));
1077    }
1078
1079    #[test]
1080    fn test_sign_challenge_uses_generate_signature() {
1081        let _lock = ENV_MUTEX.lock().unwrap();
1082
1083        let (signing_key, verifying_key) = {
1084            let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
1085            let verifying_key: VerifyingKey = signing_key.verifying_key();
1086
1087            let private_key: String = STANDARD.encode(signing_key.to_bytes());
1088            let public_key: String = STANDARD.encode(verifying_key.to_bytes());
1089
1090            env::set_var("IRONSHIELD_PRIVATE_KEY", &private_key);
1091            env::set_var("IRONSHIELD_PUBLIC_KEY", &public_key);
1092
1093            (signing_key, verifying_key)
1094        };
1095
1096        // Create a test challenge - it will be automatically signed
1097        let challenge = IronShieldChallenge::new(
1098            "test_website".to_string(),
1099            100_000,
1100            signing_key.clone(),
1101            verifying_key.to_bytes(),
1102        );
1103
1104        // Test that sign_challenge and manual generate_signature produce the same result
1105        let sign_challenge_result = sign_challenge(&challenge).unwrap();
1106
1107        let message = create_signing_message(
1108            &challenge.random_nonce,
1109            challenge.created_time,
1110            challenge.expiration_time,
1111            &challenge.website_id,
1112            &challenge.challenge_param,
1113            &challenge.public_key
1114        );
1115        let manual_signature = generate_signature(&signing_key, &message).unwrap();
1116
1117        assert_eq!(sign_challenge_result, manual_signature,
1118                   "sign_challenge should produce the same result as manual generate_signature");
1119    }
1120}