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()`:                     Load Ed25519 private key from provided data
19//!                                             or environment (multiple formats)
20//! * `load_public_key()`:                      Load Ed25519 public key from provided data
21//!                                             or environment (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//! Generate test keys and set them as environment variables, then load them
44//! using `load_private_key(None)` and `load_public_key(None)`.
45//!
46//! ### Using with PGP Keys
47//! For PGP keys stored in Cloudflare Secrets Store, use `load_private_key(Some(key_data))`
48//! and `load_public_key(Some(key_data))` with base64-encoded PGP data without ASCII armor headers.
49
50use base64::{
51    Engine,
52    engine::general_purpose::STANDARD
53};
54use ed25519_dalek::{
55    Signature,
56    Signer,
57    Verifier,
58    SigningKey,
59    VerifyingKey,
60    PUBLIC_KEY_LENGTH,
61    SECRET_KEY_LENGTH
62};
63use rand::rngs::OsRng;
64
65use crate::IronShieldChallenge;
66
67use std::env;
68
69/// Debug logging helper that works across different compilation targets
70macro_rules! debug_log {
71    ($($arg:tt)*) => {
72        #[cfg(all(target_arch = "wasm32", feature = "wasm-logging"))]
73        {
74            let msg = format!($($arg)*);
75            web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&msg));
76        }
77        #[cfg(not(target_arch = "wasm32"))]
78        eprintln!($($arg)*);
79        #[cfg(all(target_arch = "wasm32", not(feature = "wasm-logging")))]
80        {
81            // No-op for WASM without logging feature
82            let _ = format!($($arg)*);
83        }
84    };
85}
86
87#[derive(Debug, Clone)]
88pub enum CryptoError {
89    MissingEnvironmentVariable(String),
90    InvalidKeyFormat(String),
91    SigningFailed(String),
92    VerificationFailed(String),
93    Base64DecodingFailed(String),
94    PgpParsingFailed(String),
95}
96
97impl std::fmt::Display for CryptoError {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        match self {
100            CryptoError::MissingEnvironmentVariable(var) => write!(f, "Missing environment variable: {}", var),
101            CryptoError::InvalidKeyFormat(msg) => write!(f, "Invalid key format: {}", msg),
102            CryptoError::SigningFailed(msg) => write!(f, "Signing failed: {}", msg),
103            CryptoError::VerificationFailed(msg) => write!(f, "Verification failed: {}", msg),
104            CryptoError::Base64DecodingFailed(msg) => write!(f, "Base64 decoding failed: {}", msg),
105            CryptoError::PgpParsingFailed(msg) => write!(f, "PGP parsing failed: {}", msg),
106        }
107    }
108}
109
110impl std::error::Error for CryptoError {}
111
112/// Parse key data using sequoia-openpgp
113///
114/// This function attempts to extract Ed25519 key material from various formats:
115/// 1. ASCII-armored PGP keys (with -----BEGIN PGP----- headers)
116/// 2. Base64-encoded PGP binary data (without armor)
117/// 3. Raw base64-encoded Ed25519 keys (32 bytes, legacy fallback)
118///
119/// # Arguments
120/// * `key_data`:   Key data as string
121/// * `is_private`: Whether this is a private key
122///
123/// # Returns
124/// * `Result<[u8; 32], CryptoError>`: The 32-byte Ed25519 key material
125fn parse_key(key_data: &str, is_private: bool) -> Result<[u8; 32], CryptoError> {
126    use sequoia_openpgp::{Cert, parse::Parse};
127    
128    debug_log!("Parsing key data: {} chars", key_data.len());
129    
130    // Try to parse as PGP certificate (handles both ASCII-armored and binary)
131    match Cert::from_bytes(key_data.as_bytes()) {
132        Ok(cert) => {
133            debug_log!("Successfully parsed as PGP certificate");
134            return extract_ed25519_key_from_cert(&cert, is_private);
135        }
136        Err(_) => {
137            debug_log!("Not a PGP certificate, trying base64 decode");
138        }
139    }
140    
141    // Try to decode as base64 and parse as binary PGP
142    if let Ok(decoded) = STANDARD.decode(key_data.trim()) {
143        debug_log!("Decoded base64 to {} bytes", decoded.len());
144        
145        // Try as binary PGP certificate
146        match Cert::from_bytes(&decoded) {
147            Ok(cert) => {
148                debug_log!("Successfully parsed binary PGP certificate");
149                return extract_ed25519_key_from_cert(&cert, is_private);
150            }
151            Err(_) => {
152                debug_log!("Not binary PGP, checking for raw Ed25519 key");
153                
154                // Fallback: raw 32-byte Ed25519 key (legacy support)
155                if decoded.len() == 32 {
156                    debug_log!("Detected raw 32-byte Ed25519 key");
157                    let mut key_array = [0u8; 32];
158                    key_array.copy_from_slice(&decoded);
159                    
160                    // Validate it's a proper Ed25519 key
161                    // For private keys, just creating a SigningKey validates it
162                    // For public keys, we need to check with VerifyingKey
163                    if is_private {
164                        let _signing_key = SigningKey::from_bytes(&key_array);
165                        debug_log!("Raw Ed25519 private key validated");
166                    } else {
167                        VerifyingKey::from_bytes(&key_array)
168                            .map_err(|e| CryptoError::InvalidKeyFormat(
169                                format!("Invalid raw Ed25519 public key: {}", e)
170                            ))?;
171                        debug_log!("Raw Ed25519 public key validated");
172                    }
173                    
174                    return Ok(key_array);
175                }
176            }
177        }
178    }
179    
180    Err(CryptoError::PgpParsingFailed(
181        "Could not parse as PGP certificate or raw Ed25519 key".to_string()
182    ))
183}
184
185/// Extract Ed25519 key material from a sequoia-openpgp certificate
186fn extract_ed25519_key_from_cert(cert: &sequoia_openpgp::Cert, is_private: bool) -> Result<[u8; 32], CryptoError> {
187    use sequoia_openpgp::serialize::Marshal;
188    
189    // Get the primary key from the certificate
190    let primary_key = cert.primary_key().key();
191    
192    // Serialize the public key MPIs (multiprecision integers) to a Vec
193    let mut mpi_bytes = Vec::new();
194    primary_key.mpis().serialize(&mut mpi_bytes)
195        .map_err(|e| CryptoError::PgpParsingFailed(
196            format!("Failed to serialize key MPIs: {}", e)
197        ))?;
198    
199    debug_log!("Key MPI bytes: {} bytes", mpi_bytes.len());
200    
201    // For Ed25519 keys, the MPI format is:
202    // [length_bits_high_byte, length_bits_low_byte, ...key_bytes...]
203    // For Ed25519: [0x00, 0x20] (32 bytes) or [0x01, 0x00] (256 bits) followed by 32 bytes
204    // Or sometimes just [0x40, 0x20, ...32 bytes...]
205    
206    // Look for the 32-byte Ed25519 key in the MPI data
207    // Common patterns:
208    // - [0x00, 0x20, ...32 bytes...] (length = 32)
209    // - [0x01, 0x00, ...32 bytes...] (length = 256 bits)
210    // - [0x40, 0x20, ...32 bytes...] (40 hex = 64 decimal, 20 hex = 32 decimal)
211    
212    if mpi_bytes.len() >= 34 {
213        // Try pattern: [0x00, 0x20, ...] or [0x01, 0x00, ...]
214        if (mpi_bytes[0] == 0x00 && mpi_bytes[1] == 0x20) ||
215           (mpi_bytes[0] == 0x01 && mpi_bytes[1] == 0x00) ||
216           (mpi_bytes[0] == 0x40 && mpi_bytes[1] == 0x20) {
217            
218            let mut key_array = [0u8; 32];
219            key_array.copy_from_slice(&mpi_bytes[2..34]);
220            
221            // Validate it's a proper Ed25519 key
222            if is_private {
223                let _signing_key = SigningKey::from_bytes(&key_array);
224                debug_log!("Ed25519 private key validated from PGP certificate");
225            } else {
226                VerifyingKey::from_bytes(&key_array)
227                    .map_err(|e| CryptoError::InvalidKeyFormat(
228                        format!("Invalid Ed25519 public key from PGP: {}", e)
229                    ))?;
230                debug_log!("Ed25519 public key validated from PGP certificate");
231            }
232            
233            debug_log!("Successfully extracted Ed25519 key from PGP certificate");
234            return Ok(key_array);
235        }
236    }
237    
238    // If the MPI is exactly 32 bytes, it might be the raw key
239    if mpi_bytes.len() == 32 {
240        let mut key_array = [0u8; 32];
241        key_array.copy_from_slice(&mpi_bytes);
242        
243        // Validate the key
244        if is_private {
245            let _signing_key = SigningKey::from_bytes(&key_array);
246            debug_log!("Raw 32-byte private key validated from PGP");
247        } else {
248            VerifyingKey::from_bytes(&key_array)
249                .map_err(|e| CryptoError::InvalidKeyFormat(
250                    format!("Invalid Ed25519 public key from PGP: {}", e)
251                ))?;
252            debug_log!("Raw 32-byte public key validated from PGP");
253        }
254        
255        debug_log!("Extracted raw 32-byte Ed25519 key from PGP");
256        return Ok(key_array);
257    }
258    
259    Err(CryptoError::PgpParsingFailed(
260        format!("Unexpected key format in PGP certificate: {} bytes, expected Ed25519", mpi_bytes.len())
261    ))
262}
263
264/// Loads the private key from provided data or environment variable
265///
266/// This function attempts to load the private key in the following order:
267/// 1. If `key_data` is provided, try to parse it (for production/Cloudflare Secrets Store)
268/// 2. Fall back to IRONSHIELD_PRIVATE_KEY environment variable (for local testing)
269///
270/// The key can be in either format:
271/// - Base64-encoded PGP private key (without armor headers)
272/// - Raw base64-encoded Ed25519 private key (32 bytes, legacy format)
273///
274/// # Arguments
275/// * `key_data`: Optional key data string (for Cloudflare Workers/production)
276///
277/// # Returns
278/// * `Result<SigningKey, CryptoError>`: The Ed25519 signing key or an error
279///
280/// # Environment Variables
281/// * `IRONSHIELD_PRIVATE_KEY`: Fallback env var for local testing
282pub fn load_private_key(key_data: Option<&str>) -> Result<SigningKey, CryptoError> {
283    // Try provided key_data first (production/Secrets Store)
284    if let Some(data) = key_data {
285        debug_log!("Attempting to load private key from provided data");
286        
287        // Try PGP format first
288        match parse_key(data, true) {
289            Ok(key_array) => {
290                let signing_key = SigningKey::from_bytes(&key_array);
291                debug_log!("Successfully loaded private key from provided data");
292                return Ok(signing_key);
293            }
294            Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
295                // Fall through to try raw format
296            }
297            Err(e) => {
298                // For other errors, log and fall through to env var
299                debug_log!("Error parsing provided key data: {}, trying env var fallback", e);
300            }
301        }
302
303        // Try raw base64-encoded Ed25519 key (legacy format)
304        match STANDARD.decode(data.trim()) {
305            Ok(key_bytes) if key_bytes.len() == SECRET_KEY_LENGTH => {
306                let mut key_array = [0u8; SECRET_KEY_LENGTH];
307                key_array.copy_from_slice(&key_bytes);
308                
309                let signing_key = SigningKey::from_bytes(&key_array);
310                debug_log!("Successfully loaded private key from provided data (raw format)");
311                return Ok(signing_key);
312            }
313            Ok(key_bytes) => {
314                debug_log!("Invalid key length in provided data: {} bytes, trying env var fallback", key_bytes.len());
315            }
316            Err(e) => {
317                debug_log!("Base64 decode failed for provided data: {}, trying env var fallback", e);
318            }
319        }
320    }
321
322    // Fall back to environment variable (local testing)
323    debug_log!("Loading private key from IRONSHIELD_PRIVATE_KEY environment variable");
324    
325    let key_str = env::var("IRONSHIELD_PRIVATE_KEY")
326        .map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PRIVATE_KEY".to_string()))?;
327
328    // Try PGP format first
329    match parse_key(&key_str, true) {
330        Ok(key_array) => {
331            let signing_key = SigningKey::from_bytes(&key_array);
332            debug_log!("Successfully loaded private key from environment variable");
333            return Ok(signing_key);
334        }
335        Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
336            // Fall back to raw base64 format
337        }
338        Err(e) => return Err(e), // Return other errors immediately
339    }
340
341    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
342    let key_bytes = STANDARD.decode(key_str.trim())
343        .map_err(|e| CryptoError::Base64DecodingFailed(format!("Private key (legacy fallback): {}", e)))?;
344
345    // Verify length for raw Ed25519 key
346    if key_bytes.len() != SECRET_KEY_LENGTH {
347        return Err(CryptoError::InvalidKeyFormat(
348            format!("Private key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
349                   SECRET_KEY_LENGTH, key_bytes.len())
350        ));
351    }
352
353    // Create signing key from raw bytes
354    let key_array: [u8; SECRET_KEY_LENGTH] = key_bytes.try_into()
355        .map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert private key bytes".to_string()))?;
356
357    let signing_key = SigningKey::from_bytes(&key_array);
358    Ok(signing_key)
359}
360
361/// Loads the public key from provided data or environment variable
362///
363/// This function attempts to load the public key in the following order:
364/// 1. If `key_data` is provided, try to parse it (for production/Cloudflare Secrets Store)
365/// 2. Fall back to IRONSHIELD_PUBLIC_KEY environment variable (for local testing)
366///
367/// The key can be in either format:
368/// - Base64-encoded PGP public key (without armor headers)
369/// - Raw base64-encoded Ed25519 public key (32 bytes, legacy format)
370///
371/// # Arguments
372/// * `key_data`: Optional key data string (for Cloudflare Workers/production)
373///
374/// # Returns
375/// * `Result<VerifyingKey, CryptoError>`: The Ed25519 verifying key or an error
376///
377/// # Environment Variables
378/// * `IRONSHIELD_PUBLIC_KEY`: Fallback env var for local testing
379pub fn load_public_key(key_data: Option<&str>) -> Result<VerifyingKey, CryptoError> {
380    // Try provided key_data first (production/Secrets Store)
381    if let Some(data) = key_data {
382        debug_log!("Attempting to load public key from provided data");
383        
384        // Try PGP format first
385        match parse_key(data, false) {
386            Ok(key_array) => {
387                let verifying_key = VerifyingKey::from_bytes(&key_array)
388                    .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key from PGP: {}", e)))?;
389                debug_log!("Successfully loaded public key from provided data");
390                return Ok(verifying_key);
391            }
392            Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
393                // Fall through to try raw format
394            }
395            Err(e) => {
396                // For other errors, log and fall through to env var
397                debug_log!("Error parsing provided key data: {}, trying env var fallback", e);
398            }
399        }
400
401        // Try raw base64-encoded Ed25519 key (legacy format)
402        match STANDARD.decode(data.trim()) {
403            Ok(key_bytes) if key_bytes.len() == PUBLIC_KEY_LENGTH => {
404                let mut key_array = [0u8; PUBLIC_KEY_LENGTH];
405                key_array.copy_from_slice(&key_bytes);
406
407                match VerifyingKey::from_bytes(&key_array) {
408                    Ok(verifying_key) => {
409                        debug_log!("Successfully loaded public key from provided data (raw format)");
410                        return Ok(verifying_key);
411                    }
412                    Err(e) => {
413                        debug_log!("Invalid Ed25519 public key in provided data: {}, trying env var fallback", e);
414                    }
415                }
416            }
417            Ok(key_bytes) => {
418                debug_log!("Invalid key length in provided data: {} bytes, trying env var fallback", key_bytes.len());
419            }
420            Err(e) => {
421                debug_log!("Base64 decode failed for provided data: {}, trying env var fallback", e);
422            }
423        }
424    }
425
426    // Fall back to environment variable (local testing)
427    debug_log!("Loading public key from IRONSHIELD_PUBLIC_KEY environment variable");
428    
429    let key_str = env::var("IRONSHIELD_PUBLIC_KEY")
430        .map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PUBLIC_KEY".to_string()))?;
431
432    // Try PGP format first
433    match parse_key(&key_str, false) {
434        Ok(key_array) => {
435            let verifying_key = VerifyingKey::from_bytes(&key_array)
436                .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
437            debug_log!("Successfully loaded public key from environment variable");
438            return Ok(verifying_key);
439        }
440        Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
441            // Fall back to raw base64 format
442        }
443        Err(e) => return Err(e), // Return other errors immediately
444    }
445
446    // Fallback: try raw base64-encoded Ed25519 key (legacy format)
447    let key_bytes = STANDARD.decode(key_str.trim())
448        .map_err(|e| CryptoError::Base64DecodingFailed(format!("Public key (legacy fallback): {}", e)))?;
449
450    // Verify length for raw Ed25519 key
451    if key_bytes.len() != PUBLIC_KEY_LENGTH {
452        return Err(CryptoError::InvalidKeyFormat(
453            format!("Public key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
454                   PUBLIC_KEY_LENGTH, key_bytes.len())
455        ));
456    }
457
458    // Create verifying key from raw bytes
459    let key_array: [u8; PUBLIC_KEY_LENGTH] = key_bytes.try_into()
460        .map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert public key bytes".to_string()))?;
461
462    let verifying_key = VerifyingKey::from_bytes(&key_array)
463        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
464
465    Ok(verifying_key)
466}
467
468/// Creates a message to be signed from challenge data components
469///
470/// This function creates a canonical representation of the challenge data for signing.
471/// It takes individual challenge components rather than a complete challenge object,
472/// allowing it to be used during challenge creation.
473///
474/// # Arguments
475/// * `random_nonce`:    The random nonce string
476/// * `created_time`:    The challenge creation timestamp
477/// * `expiration_time`: The challenge expiration timestamp
478/// * `website_id`:      The website identifier
479/// * `challenge_param`: The challenge parameter bytes
480/// * `public_key`:      The public key bytes
481///
482/// # Returns
483/// * `String`: Canonical string representation for signing
484pub fn create_signing_message(
485    random_nonce: &str,
486    created_time: i64,
487    expiration_time: i64,
488    website_id: &str,
489    challenge_param: &[u8; 32],
490    public_key: &[u8; 32]
491) -> String {
492    format!(
493        "{}|{}|{}|{}|{}|{}",
494        random_nonce,
495        created_time,
496        expiration_time,
497        website_id,
498        hex::encode(challenge_param),
499        hex::encode(public_key)
500    )
501}
502
503/// Generates an Ed25519 signature for a given message using the provided signing key
504///
505/// This is a low-level function for generating signatures. For challenge signing,
506/// consider using `sign_challenge` which handles message creation automatically.
507///
508/// # Arguments
509/// * `signing_key`: The Ed25519 signing key to use
510/// * `message`:     The message to sign (will be converted to bytes)
511///
512/// # Returns
513/// * `Result<[u8; 64], CryptoError>`: The signature bytes or an error
514pub fn generate_signature(signing_key: &SigningKey, message: &str) -> Result<[u8; 64], CryptoError> {
515    let signature: Signature = signing_key.sign(message.as_bytes());
516    Ok(signature.to_bytes())
517}
518
519/// Signs a challenge using the private key from environment variables.
520///
521/// This function creates a signature over all challenge fields except the signature itself.
522/// The private key is loaded from the IRONSHIELD_PRIVATE_KEY environment variable.
523///
524/// # Arguments
525/// * `challenge`: The challenge to sign (signature field will be ignored).
526///
527/// # Returns
528/// * `Result<[u8; 64], CryptoError>`: The Ed25519 signature bytes or an error.
529pub fn sign_challenge(challenge: &IronShieldChallenge) -> Result<[u8; 64], CryptoError> {
530    let signing_key: SigningKey = load_private_key(None)?;
531    let message: String = create_signing_message(
532        &challenge.random_nonce,
533        challenge.created_time,
534        challenge.expiration_time,
535        &challenge.website_id,
536        &challenge.challenge_param,
537        &challenge.public_key
538    );
539    generate_signature(&signing_key, &message)
540}
541
542/// Verifies a challenge signature using the public key from environment variables
543///
544/// This function verifies that the challenge signature is valid and that the challenge
545/// data has not been tampered with. The public key is loaded from the IRONSHIELD_PUBLIC_KEY
546/// environment variable.
547///
548/// # Arguments
549/// * `challenge`: The challenge with signature to verify.
550///
551/// # Returns
552/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if verification fails.
553pub fn verify_challenge_signature(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
554    let verifying_key: VerifyingKey = load_public_key(None)?;
555
556    let message: String = create_signing_message(
557        &challenge.random_nonce,
558        challenge.created_time,
559        challenge.expiration_time,
560        &challenge.website_id,
561        &challenge.challenge_param,
562        &challenge.public_key
563    );
564    let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
565        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
566
567    verifying_key.verify(message.as_bytes(), &signature)
568        .map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
569
570    Ok(())
571}
572
573/// Verifies a challenge signature using a provided public key
574///
575/// This function is similar to `verify_challenge_signature` but uses a provided
576/// public key instead of loading from environment variables. This is useful for
577/// client-side verification where the public key is embedded in the challenge.
578///
579/// # Arguments
580/// * `challenge`:        The challenge with signature to verify
581/// * `public_key_bytes`: The Ed25519 public key bytes to use for verification
582///
583/// # Returns
584/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if verification fails
585pub fn verify_challenge_signature_with_key(
586    challenge: &IronShieldChallenge,
587    public_key_bytes: &[u8; 32]
588) -> Result<(), CryptoError> {
589    let verifying_key: VerifyingKey = VerifyingKey::from_bytes(public_key_bytes)
590        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
591
592    let message: String = create_signing_message(
593        &challenge.random_nonce,
594        challenge.created_time,
595        challenge.expiration_time,
596        &challenge.website_id,
597        &challenge.challenge_param,
598        &challenge.public_key
599    );
600    let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
601        .map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
602
603    verifying_key.verify(message.as_bytes(), &signature)
604        .map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
605
606    Ok(())
607}
608
609/// Generates a new Ed25519 keypair for testing purposes
610///
611/// This function generates a fresh keypair and returns the keys in raw base64 format
612/// (legacy format) suitable for use as environment variables in tests.
613///
614/// # Returns
615/// * `(String, String)`: (base64_private_key, base64_public_key) in raw Ed25519 format
616pub fn generate_test_keypair() -> (String, String) {
617    let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
618    let verifying_key: VerifyingKey = signing_key.verifying_key();
619
620    let private_key_b64: String = STANDARD.encode(signing_key.to_bytes());
621    let public_key_b64: String = STANDARD.encode(verifying_key.to_bytes());
622
623    (private_key_b64, public_key_b64)
624}
625
626/// Verifies a challenge and checks if it's valid and not expired
627///
628/// This is a comprehensive validation function that checks:
629/// - Signature validity
630/// - Challenge expiration
631/// - Basic format validation
632///
633/// # Arguments
634/// * `challenge`: The challenge to validate
635///
636/// # Returns
637/// * `Result<(), CryptoError>`: `Ok(())` if valid, error if invalid
638pub fn validate_challenge(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
639    // Check signature first
640    verify_challenge_signature(challenge)?;
641
642    // Check expiration
643    if challenge.is_expired() {
644        return Err(CryptoError::VerificationFailed("Challenge has expired".to_string()));
645    }
646
647    if challenge.website_id.is_empty() {
648        return Err(CryptoError::VerificationFailed("Empty website_id".to_string()));
649    }
650
651    Ok(())
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    /// Test parsing a raw 32-byte Ed25519 private key
659    #[test]
660    fn test_parse_raw_ed25519_private_key() {
661        // Generate a test keypair
662        let (private_b64, _) = generate_test_keypair();
663        
664        // Parse the private key
665        let result = parse_key(&private_b64, true);
666        assert!(result.is_ok(), "Failed to parse raw Ed25519 private key");
667        
668        let key_bytes = result.unwrap();
669        assert_eq!(key_bytes.len(), 32, "Key should be 32 bytes");
670        
671        // Verify it's a valid Ed25519 private key
672        let signing_key = SigningKey::from_bytes(&key_bytes);
673        let _ = signing_key.verifying_key(); // Should not panic
674        
675        println!("Successfully parsed raw Ed25519 private key");
676    }
677
678    /// Test parsing a raw 32-byte Ed25519 public key
679    #[test]
680    fn test_parse_raw_ed25519_public_key() {
681        // Generate a test keypair
682        let (_, public_b64) = generate_test_keypair();
683        
684        // Parse the public key
685        let result = parse_key(&public_b64, false);
686        assert!(result.is_ok(), "Failed to parse raw Ed25519 public key");
687        
688        let key_bytes = result.unwrap();
689        assert_eq!(key_bytes.len(), 32, "Key should be 32 bytes");
690        
691        // Verify it's a valid Ed25519 public key
692        let verifying_key = VerifyingKey::from_bytes(&key_bytes);
693        assert!(verifying_key.is_ok(), "Should be a valid Ed25519 public key");
694        
695        println!("Successfully parsed raw Ed25519 public key");
696    }
697
698    /// Test that parse_key handles whitespace correctly
699    #[test]
700    fn test_parse_key_with_whitespace() {
701        let (private_b64, _) = generate_test_keypair();
702        
703        // Add various types of whitespace
704        let with_spaces = format!("  {}  ", private_b64);
705        let with_newlines = format!("{}\n\n", private_b64);
706        let with_tabs = format!("\t{}\t", private_b64);
707        let with_mixed = format!("\n  {}\t\n  ", private_b64);
708        
709        for key_str in [with_spaces, with_newlines, with_tabs, with_mixed] {
710            let result = parse_key(&key_str, true);
711            assert!(
712                result.is_ok(),
713                "Should handle whitespace, got error: {:?}",
714                result.err()
715            );
716        }
717        
718        println!("Successfully handled various whitespace formats");
719    }
720
721    /// Test error handling for invalid base64
722    #[test]
723    fn test_parse_invalid_base64() {
724        let invalid_base64 = "this is not valid base64!!!@#$%";
725        
726        let result = parse_key(invalid_base64, true);
727        assert!(result.is_err(), "Should fail on invalid base64");
728        
729        // Accept either Base64DecodingFailed or PgpParsingFailed since the function
730        // may "fix" invalid chars and decode to garbage that fails PGP extraction
731        match result.err().unwrap() {
732            CryptoError::Base64DecodingFailed(_) | CryptoError::PgpParsingFailed(_) => {
733                println!("Correctly rejected invalid input");
734            }
735            other => panic!("Expected Base64DecodingFailed or PgpParsingFailed, got: {:?}", other),
736        }
737    }
738
739    /// Test error handling for wrong-sized keys
740    #[test]
741    fn test_parse_wrong_size_key() {
742        // Create a base64 string that decodes to wrong number of bytes
743        let wrong_size = STANDARD.encode(&[0u8; 16]); // Only 16 bytes instead of 32
744        
745        let result = parse_key(&wrong_size, true);
746        assert!(result.is_err(), "Should fail on wrong-sized key");
747        
748        println!("Correctly rejected wrong-sized key");
749    }
750
751    /// Test that private and public keys are correctly distinguished
752    #[test]
753    fn test_private_vs_public_key_validation() {
754        let (private_b64, public_b64) = generate_test_keypair();
755        
756        // Parse private key as private - should work
757        let result = parse_key(&private_b64, true);
758        assert!(result.is_ok(), "Private key should parse as private");
759        
760        // Parse public key as public - should work
761        let result = parse_key(&public_b64, false);
762        assert!(result.is_ok(), "Public key should parse as public");
763        
764        println!("Correctly validated private vs public keys");
765    }
766
767    /// Test the complete flow: generate, parse, sign, verify
768    #[test]
769    fn test_parse_key_end_to_end() {
770        // Generate a keypair
771        let (private_b64, public_b64) = generate_test_keypair();
772        
773        // Parse both keys
774        let private_bytes = parse_key(&private_b64, true)
775            .expect("Failed to parse private key");
776        let public_bytes = parse_key(&public_b64, false)
777            .expect("Failed to parse public key");
778        
779        // Create Ed25519 keys
780        let signing_key = SigningKey::from_bytes(&private_bytes);
781        let verifying_key = VerifyingKey::from_bytes(&public_bytes)
782            .expect("Invalid public key");
783        
784        // Sign a message
785        let message = b"Test message for IronShield";
786        let signature = signing_key.sign(message);
787        
788        // Verify the signature
789        verifying_key
790            .verify(message, &signature)
791            .expect("Signature verification failed");
792        
793        // Verify that the public key derived from private matches parsed public
794        let derived_public = signing_key.verifying_key();
795        assert_eq!(
796            derived_public.to_bytes(),
797            public_bytes,
798            "Derived public key should match parsed public key"
799        );
800        
801        println!("Successfully completed end-to-end test");
802    }
803
804    /// Test empty input handling
805    #[test]
806    fn test_parse_empty_string() {
807        let result = parse_key("", true);
808        assert!(result.is_err(), "Should fail on empty string");
809        
810        println!("Correctly rejected empty string");
811    }
812}
813