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