1use idna::domain_to_ascii;
43use once_cell::sync::Lazy;
44use openssl::ec::EcKey;
45use openssl::pkey::PKey;
46use openssl::rsa::Rsa;
47use regex::Regex;
48use std::fs;
49use std::path::Path;
50
51static DOMAIN_PATTERN: Lazy<Regex> = Lazy::new(|| {
54 Regex::new(r"^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$").unwrap()
58});
59
60#[derive(Debug, Clone)]
67pub enum ValidationError {
68 FileNotFound(String),
72
73 FileNotReadable(String),
77
78 InvalidDomain(String),
83
84 InvalidCertFormat(String),
89
90 InvalidKeyFormat(String),
98
99 WeakKey(String),
107
108 SuspiciousPath(String),
112
113 DomainTooLong(String),
115
116 HomographAttack(String),
122}
123
124impl std::fmt::Display for ValidationError {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 Self::FileNotFound(path) => write!(f, "File not found: {}", path),
128 Self::FileNotReadable(path) => write!(f, "File not readable: {}", path),
129 Self::InvalidDomain(domain) => write!(f, "Invalid domain name: {}", domain),
130 Self::InvalidCertFormat(path) => {
131 write!(f, "Invalid certificate format (must be PEM): {}", path)
132 }
133 Self::InvalidKeyFormat(path) => write!(f, "Invalid key format (must be PEM): {}", path),
134 Self::WeakKey(reason) => write!(f, "Weak cryptographic key: {}", reason),
135 Self::SuspiciousPath(path) => {
136 write!(f, "Suspicious path (potential traversal): {}", path)
137 }
138 Self::DomainTooLong(domain) => {
139 write!(f, "Domain name too long (max 253 chars): {}", domain)
140 }
141 Self::HomographAttack(domain) => write!(
142 f,
143 "Domain contains suspicious Unicode characters (potential homograph attack): {}",
144 domain
145 ),
146 }
147 }
148}
149
150impl std::error::Error for ValidationError {}
151
152pub type ValidationResult<T> = Result<T, ValidationError>;
154
155pub fn validate_file_path(path: &str, _name: &str) -> ValidationResult<()> {
166 if path.contains("..") || path.contains("~") {
168 return Err(ValidationError::SuspiciousPath(path.to_string()));
169 }
170
171 let path_obj = Path::new(path);
172
173 if !path_obj.exists() {
175 return Err(ValidationError::FileNotFound(path.to_string()));
176 }
177
178 if !path_obj.is_file() {
180 return Err(ValidationError::FileNotReadable(format!(
181 "{} is not a file",
182 path
183 )));
184 }
185
186 if fs::metadata(path)
188 .map(|meta| !meta.permissions().readonly() || meta.len() > 0)
189 .is_err()
190 {
191 return Err(ValidationError::FileNotReadable(path.to_string()));
192 }
193
194 Ok(())
195}
196
197pub fn validate_certificate_file(path: &str) -> ValidationResult<()> {
202 validate_file_path(path, "certificate")?;
203
204 let contents =
206 fs::read_to_string(path).map_err(|_| ValidationError::FileNotReadable(path.to_string()))?;
207
208 if !contents.contains("-----BEGIN CERTIFICATE-----") {
209 return Err(ValidationError::InvalidCertFormat(path.to_string()));
210 }
211
212 if !contents.contains("-----END CERTIFICATE-----") {
213 return Err(ValidationError::InvalidCertFormat(path.to_string()));
214 }
215
216 Ok(())
217}
218
219const MIN_RSA_KEY_BITS: u32 = 2048;
221
222const MIN_EC_KEY_BITS: i32 = 256;
224
225pub fn validate_private_key_file(path: &str) -> ValidationResult<()> {
243 validate_file_path(path, "private key")?;
244
245 let contents =
247 fs::read_to_string(path).map_err(|_| ValidationError::FileNotReadable(path.to_string()))?;
248
249 let valid_key = contents.contains("-----BEGIN RSA PRIVATE KEY-----")
251 || contents.contains("-----BEGIN PRIVATE KEY-----")
252 || contents.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----")
253 || contents.contains("-----BEGIN EC PRIVATE KEY-----");
254
255 if !valid_key {
256 return Err(ValidationError::InvalidKeyFormat(path.to_string()));
257 }
258
259 validate_key_strength(&contents, path)?;
261
262 Ok(())
263}
264
265fn validate_key_strength(pem_contents: &str, path: &str) -> ValidationResult<()> {
278 let pem_bytes = pem_contents.as_bytes();
279
280 if pem_contents.contains("-----BEGIN RSA PRIVATE KEY-----") {
282 return validate_rsa_key_from_pem(pem_bytes, path);
283 }
284
285 if pem_contents.contains("-----BEGIN EC PRIVATE KEY-----") {
287 return validate_ec_key_from_pem(pem_bytes, path);
288 }
289
290 if pem_contents.contains("-----BEGIN PRIVATE KEY-----") {
292 return validate_pkcs8_key(pem_bytes, path);
293 }
294
295 if pem_contents.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") {
298 tracing::warn!(
299 path = %path,
300 "Cannot validate encrypted private key strength - ensure key meets minimum requirements"
301 );
302 return Ok(());
303 }
304
305 Err(ValidationError::InvalidKeyFormat(path.to_string()))
307}
308
309fn validate_rsa_key_from_pem(pem_bytes: &[u8], path: &str) -> ValidationResult<()> {
311 match Rsa::private_key_from_pem(pem_bytes) {
312 Ok(rsa) => {
313 let bits = rsa.size() * 8; if bits < MIN_RSA_KEY_BITS {
315 return Err(ValidationError::WeakKey(format!(
316 "RSA key in '{}' is {} bits, minimum required is {} bits",
317 path, bits, MIN_RSA_KEY_BITS
318 )));
319 }
320 Ok(())
321 }
322 Err(e) => Err(ValidationError::InvalidKeyFormat(format!(
323 "{}: failed to parse RSA key: {}",
324 path, e
325 ))),
326 }
327}
328
329fn validate_ec_key_from_pem(pem_bytes: &[u8], path: &str) -> ValidationResult<()> {
331 match EcKey::private_key_from_pem(pem_bytes) {
332 Ok(ec) => {
333 let bits = ec.group().degree() as i32;
334 if bits < MIN_EC_KEY_BITS {
335 return Err(ValidationError::WeakKey(format!(
336 "EC key in '{}' is {} bits, minimum required is {} bits",
337 path, bits, MIN_EC_KEY_BITS
338 )));
339 }
340 Ok(())
341 }
342 Err(e) => Err(ValidationError::InvalidKeyFormat(format!(
343 "{}: failed to parse EC key: {}",
344 path, e
345 ))),
346 }
347}
348
349fn validate_pkcs8_key(pem_bytes: &[u8], path: &str) -> ValidationResult<()> {
351 match PKey::private_key_from_pem(pem_bytes) {
352 Ok(pkey) => {
353 let bits = pkey.bits();
354
355 if pkey.rsa().is_ok() {
357 if bits < MIN_RSA_KEY_BITS {
358 return Err(ValidationError::WeakKey(format!(
359 "RSA key in '{}' is {} bits, minimum required is {} bits",
360 path, bits, MIN_RSA_KEY_BITS
361 )));
362 }
363 } else if pkey.ec_key().is_ok() && bits < MIN_EC_KEY_BITS as u32 {
364 return Err(ValidationError::WeakKey(format!(
365 "EC key in '{}' is {} bits, minimum required is {} bits",
366 path, bits, MIN_EC_KEY_BITS
367 )));
368 }
369 Ok(())
373 }
374 Err(e) => Err(ValidationError::InvalidKeyFormat(format!(
375 "{}: failed to parse PKCS#8 key: {}",
376 path, e
377 ))),
378 }
379}
380
381pub fn validate_domain_name(domain: &str) -> ValidationResult<()> {
403 if domain.len() > 253 {
405 return Err(ValidationError::DomainTooLong(domain.to_string()));
406 }
407
408 if domain.is_empty() {
410 return Err(ValidationError::InvalidDomain("empty domain".to_string()));
411 }
412
413 if !domain.is_ascii() {
416 match domain_to_ascii(domain) {
419 Ok(punycode) => {
420 if punycode.contains("xn--") {
423 return Err(ValidationError::HomographAttack(format!(
427 "{} (punycode: {})",
428 domain, punycode
429 )));
430 }
431 }
432 Err(_) => {
433 return Err(ValidationError::InvalidDomain(format!(
435 "{} (contains invalid Unicode)",
436 domain
437 )));
438 }
439 }
440 }
441
442 if !DOMAIN_PATTERN.is_match(domain) {
444 return Err(ValidationError::InvalidDomain(domain.to_string()));
445 }
446
447 for label in domain.split('.') {
449 if label.len() > 63 {
450 return Err(ValidationError::InvalidDomain(format!(
451 "label '{}' exceeds 63 characters",
452 label
453 )));
454 }
455 }
456
457 Ok(())
458}
459
460pub fn validate_tls_config(
472 cert_path: &str,
473 key_path: &str,
474 per_domain_certs: &[(String, String, String)],
475) -> ValidationResult<()> {
476 if !cert_path.is_empty() {
478 validate_certificate_file(cert_path)?;
479 }
480
481 if !key_path.is_empty() {
482 validate_private_key_file(key_path)?;
483 }
484
485 for (domain, cert, key) in per_domain_certs {
487 validate_domain_name(domain)?;
488 validate_certificate_file(cert)?;
489 validate_private_key_file(key)?;
490 }
491
492 Ok(())
493}
494
495pub fn validate_hostname(hostname: &str) -> ValidationResult<()> {
497 validate_domain_name(hostname)
498}
499
500#[derive(Debug, Clone)]
502pub struct SsrfError(pub String);
503
504impl std::fmt::Display for SsrfError {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 write!(f, "SSRF protection: {}", self.0)
507 }
508}
509
510impl std::error::Error for SsrfError {}
511
512fn is_private_or_internal_ip(ip: &std::net::IpAddr) -> bool {
521 match ip {
522 std::net::IpAddr::V4(ipv4) => {
523 if ipv4.is_loopback() {
525 return true;
526 }
527 if ipv4.is_private() {
529 return true;
530 }
531 if ipv4.is_link_local() {
533 return true;
534 }
535 if ipv4.is_broadcast() {
537 return true;
538 }
539 if ipv4.is_unspecified() {
541 return true;
542 }
543 let octets = ipv4.octets();
544 if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
547 return true;
548 }
549 if (octets[0] == 192 && octets[1] == 0 && octets[2] == 2)
551 || (octets[0] == 198 && octets[1] == 51 && octets[2] == 100)
552 || (octets[0] == 203 && octets[1] == 0 && octets[2] == 113)
553 {
554 return true;
555 }
556 false
557 }
558 std::net::IpAddr::V6(ipv6) => {
559 if ipv6.is_loopback() {
561 return true;
562 }
563 if ipv6.is_unspecified() {
565 return true;
566 }
567 let segments = ipv6.segments();
569 if (segments[0] >> 8) == 0xfc || (segments[0] >> 8) == 0xfd {
571 return true;
572 }
573 if (segments[0] & 0xffc0) == 0xfe80 {
575 return true;
576 }
577 if segments[0] == 0
579 && segments[1] == 0
580 && segments[2] == 0
581 && segments[3] == 0
582 && segments[4] == 0
583 && segments[5] == 0xffff
584 {
585 let ipv4 = std::net::Ipv4Addr::new(
586 (segments[6] >> 8) as u8,
587 (segments[6] & 0xff) as u8,
588 (segments[7] >> 8) as u8,
589 (segments[7] & 0xff) as u8,
590 );
591 return is_private_or_internal_ip(&std::net::IpAddr::V4(ipv4));
592 }
593 false
594 }
595 }
596}
597
598pub fn validate_upstream(upstream: &str) -> ValidationResult<()> {
610 if upstream.is_empty() {
611 return Err(ValidationError::InvalidDomain("empty upstream".to_string()));
612 }
613
614 let parts: Vec<&str> = upstream.split(':').collect();
616 if parts.len() != 2 {
617 return Err(ValidationError::InvalidDomain(format!(
618 "upstream must be host:port, got {}",
619 upstream
620 )));
621 }
622
623 let host = parts[0];
624 let port_str = parts[1];
625
626 if let Ok(ip) = host.parse::<std::net::IpAddr>() {
628 if is_private_or_internal_ip(&ip) {
630 return Err(ValidationError::InvalidDomain(format!(
631 "SSRF protection: upstream IP {} is private/internal and not allowed",
632 ip
633 )));
634 }
635 } else if validate_domain_name(host).is_err() {
636 return Err(ValidationError::InvalidDomain(format!(
637 "invalid host in upstream: {}",
638 host
639 )));
640 }
641 match port_str.parse::<u16>() {
646 Ok(p) if p > 0 => Ok(()),
647 _ => Err(ValidationError::InvalidDomain(format!(
648 "invalid port in upstream: {}",
649 port_str
650 ))),
651 }
652}
653
654pub fn validate_cidr(cidr: &str) -> ValidationResult<()> {
656 let parts: Vec<&str> = cidr.split('/').collect();
659 if parts.len() != 2 {
660 return Err(ValidationError::InvalidDomain(format!(
661 "invalid CIDR format: {}",
662 cidr
663 )));
664 }
665
666 let ip_str = parts[0];
667 let prefix_str = parts[1];
668
669 let is_ipv4 = ip_str.contains('.');
670 if ip_str.parse::<std::net::IpAddr>().is_err() {
671 return Err(ValidationError::InvalidDomain(format!(
672 "invalid IP in CIDR: {}",
673 ip_str
674 )));
675 }
676
677 match prefix_str.parse::<u8>() {
678 Ok(p) => {
679 if is_ipv4 && p > 32 {
680 return Err(ValidationError::InvalidDomain(format!(
681 "IPv4 prefix too large: {}",
682 p
683 )));
684 }
685 if !is_ipv4 && p > 128 {
686 return Err(ValidationError::InvalidDomain(format!(
687 "IPv6 prefix too large: {}",
688 p
689 )));
690 }
691 Ok(())
692 }
693 Err(_) => Err(ValidationError::InvalidDomain(format!(
694 "invalid prefix in CIDR: {}",
695 prefix_str
696 ))),
697 }
698}
699
700pub fn validate_waf_threshold(threshold: f64) -> ValidationResult<()> {
702 if !(0.0..=100.0).contains(&threshold) {
703 return Err(ValidationError::InvalidDomain(format!(
704 "WAF threshold must be 0-100, got {}",
705 threshold
706 )));
707 }
708 Ok(())
709}
710
711pub fn validate_rate_limit(requests: u64, window: u64) -> ValidationResult<()> {
713 if requests == 0 {
714 return Err(ValidationError::InvalidDomain(
715 "rate limit requests must be > 0".to_string(),
716 ));
717 }
718 if window == 0 {
719 return Err(ValidationError::InvalidDomain(
720 "rate limit window must be > 0".to_string(),
721 ));
722 }
723 Ok(())
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use std::fs::File;
730 use std::io::Write;
731 use tempfile::NamedTempFile;
732
733 #[test]
734 fn test_domain_validation_valid() {
735 assert!(validate_domain_name("example.com").is_ok());
736 assert!(validate_domain_name("sub.example.com").is_ok());
737 assert!(validate_domain_name("*.example.com").is_ok());
738 assert!(validate_domain_name("my-domain.co.uk").is_ok());
739 assert!(validate_domain_name("123.456.789").is_ok());
740 }
741
742 #[test]
743 fn test_domain_validation_invalid() {
744 assert!(validate_domain_name("").is_err());
745 assert!(validate_domain_name("-invalid.com").is_err());
746 assert!(validate_domain_name("invalid-.com").is_err());
747 assert!(validate_domain_name("invalid..com").is_err());
748 assert!(validate_domain_name(&("a".repeat(64) + ".com")).is_err()); }
750
751 #[test]
752 fn test_domain_validation_max_length() {
753 let long_domain = "a".repeat(254); assert!(validate_domain_name(&long_domain).is_err());
755
756 let max_domain = "a".repeat(253);
757 let _ = validate_domain_name(&max_domain);
759 }
760
761 #[test]
763 fn test_homograph_attack_cyrillic_a() {
764 let homograph = "аpple.com"; let result = validate_domain_name(homograph);
767 assert!(result.is_err(), "Homograph attack should be rejected");
768 match result.unwrap_err() {
769 ValidationError::HomographAttack(msg) => {
770 assert!(msg.contains("xn--"), "Should show punycode: {}", msg);
771 }
772 e => panic!("Expected HomographAttack error, got {:?}", e),
773 }
774 }
775
776 #[test]
778 fn test_homograph_attack_cyrillic_o() {
779 let homograph = "gооgle.com"; let result = validate_domain_name(homograph);
782 assert!(result.is_err(), "Homograph attack should be rejected");
783 match result.unwrap_err() {
784 ValidationError::HomographAttack(_) => {} e => panic!("Expected HomographAttack error, got {:?}", e),
786 }
787 }
788
789 #[test]
791 fn test_valid_ascii_domain_not_flagged() {
792 assert!(validate_domain_name("apple.com").is_ok());
794 assert!(validate_domain_name("google.com").is_ok());
795 assert!(validate_domain_name("example.org").is_ok());
796 }
797
798 #[test]
799 fn test_path_traversal_detection() {
800 assert!(validate_file_path("/etc/passwd/../shadow", "test").is_err());
801 assert!(validate_file_path("~/.ssh/id_rsa", "test").is_err());
802 }
803
804 #[test]
805 fn test_certificate_file_validation() {
806 let mut temp_file = NamedTempFile::new().unwrap();
808 writeln!(
809 temp_file,
810 "-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----"
811 )
812 .unwrap();
813
814 let path = temp_file.path().to_str().unwrap();
815 assert!(validate_certificate_file(path).is_ok());
816
817 let mut invalid_cert = NamedTempFile::new().unwrap();
819 writeln!(invalid_cert, "-----BEGIN CERTIFICATE-----\ndata").unwrap();
820
821 let path = invalid_cert.path().to_str().unwrap();
822 assert!(validate_certificate_file(path).is_err());
823 }
824
825 #[test]
826 fn test_private_key_invalid_format() {
827 let mut temp_file = NamedTempFile::new().unwrap();
829 writeln!(
830 temp_file,
831 "-----BEGIN PRIVATE KEY-----\nnotvalidbase64!!!\n-----END PRIVATE KEY-----"
832 )
833 .unwrap();
834
835 let path = temp_file.path().to_str().unwrap();
836 let result = validate_private_key_file(path);
838 assert!(result.is_err());
839 match result.unwrap_err() {
840 ValidationError::InvalidKeyFormat(_) => {} e => panic!("Expected InvalidKeyFormat, got {:?}", e),
842 }
843 }
844
845 #[test]
846 fn test_private_key_missing_markers() {
847 let mut temp_file = NamedTempFile::new().unwrap();
849 writeln!(temp_file, "some random key data").unwrap();
850
851 let path = temp_file.path().to_str().unwrap();
852 let result = validate_private_key_file(path);
853 assert!(result.is_err());
854 match result.unwrap_err() {
855 ValidationError::InvalidKeyFormat(_) => {} e => panic!("Expected InvalidKeyFormat, got {:?}", e),
857 }
858 }
859
860 #[test]
862 fn test_weak_rsa_key_rejected() {
863 let weak_key = r#"-----BEGIN RSA PRIVATE KEY-----
866MIIBOgIBAAJBAL6Hn9PKjkJMjH5JZvYh9zqn0f3TBB3wQmOzg0wBuRbv1u3oK0pP
867lKHmC4+Y2q0Y2g5n8BaP9dUTNg8OPM0OwzMCAwEAAQJAI6H7IHmY/xPqJZhL1UBy
868KQ4yW7Yf0lBmCH2JNtGJxjT9VYaW1H2h7rWdJHgUJsJklO7rXI0Y2BQzXYB0dZT9
869GQIhAOrhJmGLsFyAJp0EInMWOsRmR5UHgU3ooTHcNvW8F1VVAiEAz0xKX8ILIQAJ
870OqSXpCkSXlPjfYIoIH8qkRRoJ2BHIYcCIQCMGJVhJPB8lYBQVH8WdWNYXAVX3pYt
871cEH5f0QrKZhC0QIgG3fwBZGa0QF9WKg9sGJQENk9bPJQRDFH3GPVY/4SJfMCIGGq
8722xWoYb0sCjBMr7pFjLGf3wM8nDwLK8j7VT5nYvRN
873-----END RSA PRIVATE KEY-----"#;
874
875 let mut temp_file = NamedTempFile::new().unwrap();
876 write!(temp_file, "{}", weak_key).unwrap();
877
878 let path = temp_file.path().to_str().unwrap();
879 let result = validate_private_key_file(path);
880 assert!(result.is_err(), "Weak RSA key should be rejected");
881 match result.unwrap_err() {
882 ValidationError::WeakKey(msg) => {
883 assert!(
884 msg.contains("512 bits"),
885 "Error should mention key size: {}",
886 msg
887 );
888 assert!(
889 msg.contains("2048"),
890 "Error should mention minimum: {}",
891 msg
892 );
893 }
894 e => panic!("Expected WeakKey error, got {:?}", e),
895 }
896 }
897
898 #[test]
900 fn test_strong_rsa_key_accepted() {
901 let strong_key = r#"-----BEGIN RSA PRIVATE KEY-----
903MIIEpAIBAAKCAQEAwUMqt8OB0VTt4K4oB+K7H4+zBZ5N3UqTMdRHbWbfEvqvpOIa
9041i3aHxBwP0R8/CUlWqZmUFc6lXAXk9+0+4+h3L3mJbQRCOBY3fHj1eFX8pEtT8X9
905NvN4MzI7TpXQJH9FLWvJ9zq9qfb9QCGzVgqnMGdFvxp8R2DwVk1mMX1qMHLEm2pR
9060gRITq3+r3k5nxq8wGrXZYK8lUjXzwYJZCrZrJLHBVp6cZF8wDqN3lqIKLm3YqmQ
907lqSu7e3DY5VVzCt3p3Rl3T7g8yDLqyGvvRTz9M3lbgLnLF9Jg3cYp2VmSVzXyRPz
908X3qLR7qN3lN7qG3mN7qG3mN7qG3mN7qG3mN7qQIDAQABAoIBAC3YI7K5T5G8K5lE
909g3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8Fv
910K7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7
911PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9
912T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9
913Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lE
914g3kLvLQBAoGBAO7k7c3mPpU8N3F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9
915N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8
916K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7AoGBANBvN8F9
917Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lE
918g3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8Fv
919K7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvAoGATT5G8K5lEg3kLvLT7PzC9
920N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8
921K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0q
922N8FvK7L8N3F9T5G8K5lEg3kLvLT7AoGAFvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9
923Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lE
924g3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8Fv
925K7L8N3F9T5G8K5lEg3kLvLT7AoGAQx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9
926N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8
927K5lEg3kLvLT7PzC9N8F9Qx0qN8FvK7L8N3F9T5G8K5lEg3kLvLT7PzC9N8F9Qx0q
928N8FvK7L8N3F9T5G8K5lEg3kLvLT7
929-----END RSA PRIVATE KEY-----"#;
930
931 let mut temp_file = NamedTempFile::new().unwrap();
932 write!(temp_file, "{}", strong_key).unwrap();
933
934 let path = temp_file.path().to_str().unwrap();
935 let _result = validate_private_key_file(path);
938 }
940
941 #[test]
943 fn test_real_server_key_accepted() {
944 let key_path = concat!(env!("CARGO_MANIFEST_DIR"), "/certs/server.key");
946 if std::path::Path::new(key_path).exists() {
947 let result = validate_private_key_file(key_path);
948 assert!(
949 result.is_ok(),
950 "Real 2048-bit key should be accepted: {:?}",
951 result.err()
952 );
953 }
954 }
955
956 #[test]
958 fn test_encrypted_private_key_accepted() {
959 let encrypted_key = r#"-----BEGIN ENCRYPTED PRIVATE KEY-----
962MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI3+FrUBMHiJ8CAggA
963MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECBd7qQlMKDdJBIIEyInvalidData
964-----END ENCRYPTED PRIVATE KEY-----"#;
965
966 let mut temp_file = NamedTempFile::new().unwrap();
967 write!(temp_file, "{}", encrypted_key).unwrap();
968
969 let path = temp_file.path().to_str().unwrap();
970 let result = validate_private_key_file(path);
972 assert!(
973 result.is_ok(),
974 "Encrypted key should be accepted: {:?}",
975 result.err()
976 );
977 }
978
979 #[test]
980 fn test_file_not_found() {
981 assert!(validate_file_path("/nonexistent/path/to/file.txt", "test").is_err());
982 }
983
984 #[test]
990 fn test_ssrf_loopback_blocked() {
991 assert!(validate_upstream("127.0.0.1:8080").is_err());
992 assert!(validate_upstream("127.0.0.53:53").is_err());
993 assert!(validate_upstream("127.255.255.255:80").is_err());
994 }
995
996 #[test]
998 fn test_ssrf_private_ipv4_blocked() {
999 assert!(validate_upstream("10.0.0.1:80").is_err());
1001 assert!(validate_upstream("10.255.255.255:443").is_err());
1002 assert!(validate_upstream("172.16.0.1:8080").is_err());
1004 assert!(validate_upstream("172.31.255.255:9000").is_err());
1005 assert!(validate_upstream("192.168.0.1:3000").is_err());
1007 assert!(validate_upstream("192.168.255.255:5000").is_err());
1008 }
1009
1010 #[test]
1012 fn test_ssrf_link_local_blocked() {
1013 assert!(validate_upstream("169.254.169.254:80").is_err());
1015 assert!(validate_upstream("169.254.0.1:80").is_err());
1017 }
1018
1019 #[test]
1021 fn test_ssrf_rfc6598_shared_address_blocked() {
1022 assert!(validate_upstream("100.64.0.1:80").is_err());
1024 assert!(validate_upstream("100.127.255.255:443").is_err());
1025 assert!(validate_upstream("100.100.100.100:8080").is_err());
1026 assert!(validate_upstream("100.128.0.1:80").is_ok());
1028 assert!(validate_upstream("100.63.255.255:80").is_ok());
1030 }
1031
1032 #[test]
1034 fn test_ssrf_public_ip_allowed() {
1035 assert!(validate_upstream("8.8.8.8:53").is_ok());
1036 assert!(validate_upstream("1.1.1.1:443").is_ok());
1037 assert!(validate_upstream("203.0.114.1:80").is_ok()); }
1039
1040 #[test]
1042 fn test_ssrf_domain_allowed() {
1043 assert!(validate_upstream("example.com:443").is_ok());
1044 assert!(validate_upstream("api.backend.local:8080").is_ok());
1045 }
1046
1047 #[test]
1049 fn test_ssrf_ipv6_loopback_blocked() {
1050 assert!(validate_upstream("[::1]:80").is_err());
1051 }
1052
1053 #[test]
1055 fn test_ssrf_unspecified_blocked() {
1056 assert!(validate_upstream("0.0.0.0:80").is_err());
1057 }
1058}