#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
use std::collections::HashMap;
use std::fmt;
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct TlsContext {
pub operation_id: String,
pub trace_id: Option<String>,
pub parent_span_id: Option<String>,
pub operation_name: String,
pub peer_addr: Option<SocketAddr>,
pub domain: Option<String>,
pub mode: Option<String>,
pub kex_method: Option<String>,
pub start_time: DateTime<Utc>,
pub metadata: HashMap<String, String>,
}
impl Default for TlsContext {
fn default() -> Self {
Self::new("unknown")
}
}
impl TlsContext {
#[must_use]
pub fn new(operation_name: &str) -> Self {
Self {
operation_id: Uuid::new_v4().to_string(),
trace_id: None,
parent_span_id: None,
operation_name: operation_name.to_string(),
peer_addr: None,
domain: None,
mode: None,
kex_method: None,
start_time: Utc::now(),
metadata: HashMap::new(),
}
}
#[must_use]
pub fn with_trace(operation_name: &str, trace_id: String) -> Self {
let mut ctx = Self::new(operation_name);
ctx.trace_id = Some(trace_id);
ctx
}
#[must_use]
pub fn child(&self, operation_name: &str) -> Self {
let mut child = Self::new(operation_name);
child.trace_id = self.trace_id.clone();
child.parent_span_id = Some(self.operation_id.clone());
child.peer_addr = self.peer_addr;
child.domain = self.domain.clone();
child.mode = self.mode.clone();
child.kex_method = self.kex_method.clone();
child
}
#[must_use]
pub fn with_peer(mut self, addr: SocketAddr) -> Self {
self.peer_addr = Some(addr);
self
}
#[must_use]
pub fn with_domain(mut self, domain: &str) -> Self {
self.domain = Some(domain.to_string());
self
}
#[must_use]
pub fn with_mode(mut self, mode: &str) -> Self {
self.mode = Some(mode.to_string());
self
}
#[must_use]
pub fn with_kex_method(mut self, method: &str) -> Self {
self.kex_method = Some(method.to_string());
self
}
#[must_use]
pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
#[must_use]
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
#[must_use]
pub fn elapsed(&self) -> std::time::Duration {
Utc::now()
.signed_duration_since(self.start_time)
.to_std()
.unwrap_or(std::time::Duration::ZERO)
}
#[must_use]
pub fn format(&self) -> String {
let mut parts =
vec![format!("op={}", self.operation_name), format!("id={}", self.operation_id)];
if let Some(ref trace_id) = self.trace_id {
parts.push(format!("trace={}", trace_id));
}
if let Some(ref peer) = self.peer_addr {
parts.push(format!("peer={}", peer));
}
if let Some(ref domain) = self.domain {
parts.push(format!("domain={}", domain));
}
if let Some(ref mode) = self.mode {
parts.push(format!("mode={}", mode));
}
parts.join(" ")
}
}
impl fmt::Display for TlsContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format())
}
}
#[derive(Debug, Clone)]
pub struct ErrorChain {
pub errors: Vec<ErrorLink>,
}
impl Default for ErrorChain {
fn default() -> Self {
Self::new()
}
}
impl ErrorChain {
#[must_use]
pub fn new() -> Self {
Self { errors: Vec::new() }
}
pub fn push(&mut self, link: ErrorLink) {
self.errors.push(link);
}
pub fn push_tls_error(&mut self, err: &crate::tls::error::TlsError, context: &TlsContext) {
let link = ErrorLink {
error_type: "TlsError".to_string(),
message: err.to_string(),
code: Some(err.code().to_string()),
context: context.clone(),
timestamp: Utc::now(),
};
self.push(link);
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn format(&self) -> String {
if self.is_empty() {
return "No errors".to_string();
}
self.errors
.iter()
.enumerate()
.map(|(i, link)| {
let ts = link.timestamp.timestamp().unsigned_abs();
format!(
"[{i}][{ts}] {} (code: {:?}) - {}",
link.error_type, link.code, link.message
)
})
.collect::<Vec<_>>()
.join("\n")
}
}
impl fmt::Display for ErrorChain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format())
}
}
#[derive(Debug, Clone)]
pub struct ErrorLink {
pub error_type: String,
pub message: String,
pub code: Option<String>,
pub context: TlsContext,
pub timestamp: DateTime<Utc>,
}
impl ErrorLink {
#[must_use]
pub fn new(error_type: &str, message: &str, context: &TlsContext) -> Self {
Self {
error_type: error_type.to_string(),
message: message.to_string(),
code: None,
context: context.clone(),
timestamp: Utc::now(),
}
}
#[must_use]
pub fn with_code(error_type: &str, message: &str, code: &str, context: &TlsContext) -> Self {
let mut link = Self::new(error_type, message, context);
link.code = Some(code.to_string());
link
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticInfo {
pub context: TlsContext,
pub chain: ErrorChain,
pub system_info: SystemInfo,
pub recommendations: Vec<String>,
}
impl DiagnosticInfo {
#[must_use]
pub fn new(context: &TlsContext, err: &crate::tls::error::TlsError) -> Self {
let mut chain = ErrorChain::new();
chain.push_tls_error(err, context);
let recommendations = generate_recommendations(err);
Self {
context: context.clone(),
chain,
system_info: SystemInfo::collect(),
recommendations,
}
}
#[must_use]
pub fn format(&self) -> String {
let mut output = String::new();
output.push_str("=== TLS Error Diagnostic ===\n\n");
output.push_str(&format!("Context: {}\n", self.context));
output.push_str(&format!("Elapsed: {:?}\n", self.context.elapsed()));
output.push_str("\n--- Error Chain ---\n");
output.push_str(&self.chain.format());
output.push_str("\n--- System Info ---\n");
output.push_str(&self.system_info.format());
if !self.recommendations.is_empty() {
output.push_str("\n--- Recommendations ---\n");
for (i, rec) in self.recommendations.iter().enumerate() {
let num = i.saturating_add(1);
output.push_str(&format!("{num}. {rec}\n"));
}
}
output
}
}
impl fmt::Display for DiagnosticInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format())
}
}
#[derive(Debug, Clone)]
pub struct SystemInfo {
pub platform: String,
pub rust_version: String,
pub tls_version: String,
pub features: Vec<String>,
}
impl SystemInfo {
#[must_use]
pub fn collect() -> Self {
let features = vec!["pq".to_string(), "hybrid".to_string()];
Self {
platform: std::env::consts::OS.to_string(),
rust_version: std::env::var("RUSTC").unwrap_or_else(|_| "unknown".to_string()),
tls_version: env!("CARGO_PKG_VERSION").to_string(),
features,
}
}
#[must_use]
pub fn format(&self) -> String {
format!(
"Platform: {}\nRust: {}\nLatticeArc-TLS: {}\nFeatures: {}",
self.platform,
self.rust_version,
self.tls_version,
self.features.join(", ")
)
}
}
fn generate_recommendations(err: &crate::tls::error::TlsError) -> Vec<String> {
let mut recommendations = Vec::new();
match err {
crate::tls::error::TlsError::PqNotAvailable { .. } => {
recommendations.push("Consider using classical TLS mode".to_string());
recommendations.push("Ensure 'pq' feature is enabled in Cargo.toml".to_string());
}
crate::tls::error::TlsError::Certificate { code, .. } => match code {
crate::tls::error::ErrorCode::CertificateExpired => {
recommendations.push("Certificate has expired - renew or replace".to_string());
}
crate::tls::error::ErrorCode::CertificateHostnameMismatch => {
recommendations.push("Check SNI domain matches certificate".to_string());
}
crate::tls::error::ErrorCode::CertificateChainIncomplete => {
recommendations.push("Ensure full certificate chain is configured".to_string());
}
_ => {
recommendations.push("Verify certificate validity and chain".to_string());
}
},
crate::tls::error::TlsError::Io { code, .. } => match code {
crate::tls::error::ErrorCode::ConnectionRefused => {
recommendations.push("Check if remote service is running".to_string());
recommendations.push("Verify firewall rules allow connection".to_string());
}
crate::tls::error::ErrorCode::ConnectionTimeout => {
recommendations.push("Check network connectivity".to_string());
recommendations.push("Verify remote server is responsive".to_string());
}
crate::tls::error::ErrorCode::DnsResolutionFailed => {
recommendations.push("Check DNS configuration".to_string());
recommendations.push("Verify domain name is correct".to_string());
}
_ => {}
},
crate::tls::error::TlsError::Config { .. } => {
recommendations.push("Review TLS configuration".to_string());
recommendations.push("Check certificate and key files exist".to_string());
}
crate::tls::error::TlsError::Handshake { code, .. } => match code {
crate::tls::error::ErrorCode::ProtocolVersionMismatch => {
recommendations.push("Try with different TLS version".to_string());
recommendations.push("Check if server supports TLS 1.3".to_string());
}
crate::tls::error::ErrorCode::CipherSuiteMismatch => {
recommendations.push("Configure compatible cipher suites".to_string());
}
_ => {}
},
_ => {
recommendations.push("Review error details for specific issues".to_string());
}
}
recommendations.push("Check application logs for additional details".to_string());
recommendations.push("Consider enabling debug logging for more information".to_string());
recommendations
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_tls_context_default_sets_expected_fields_succeeds() {
let ctx = TlsContext::default();
assert!(!ctx.operation_id.is_empty());
assert_eq!(ctx.operation_name, "unknown");
}
#[test]
fn test_tls_context_child_inherits_parent_operation_id_is_correct() {
let parent = TlsContext::new("parent");
let child = parent.child("child");
assert_eq!(child.parent_span_id, Some(parent.operation_id));
assert_eq!(child.trace_id, parent.trace_id);
}
#[test]
fn test_tls_context_with_metadata_stores_value_succeeds() {
let ctx =
TlsContext::new("test").with_metadata("key1", "value1").with_metadata("key2", "value2");
assert_eq!(ctx.get_metadata("key1"), Some(&"value1".to_string()));
assert_eq!(ctx.get_metadata("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_error_chain_empty_has_no_errors_is_correct() {
let chain = ErrorChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
}
#[test]
fn test_error_chain_push_adds_error_succeeds() {
let mut chain = ErrorChain::new();
let ctx = TlsContext::new("test");
let link = ErrorLink::new("TestError", "Test message", &ctx);
chain.push(link);
assert!(!chain.is_empty());
assert_eq!(chain.len(), 1);
}
#[test]
fn test_system_info_collect_populates_fields_succeeds() {
let info = SystemInfo::collect();
assert!(!info.platform.is_empty());
assert!(!info.rust_version.is_empty());
assert!(!info.tls_version.is_empty());
}
#[test]
fn test_tls_context_with_trace_sets_trace_id_succeeds() {
let ctx = TlsContext::with_trace("test_op", "trace-123".to_string());
assert_eq!(ctx.operation_name, "test_op");
assert_eq!(ctx.trace_id, Some("trace-123".to_string()));
}
#[test]
fn test_tls_context_with_peer_sets_peer_addr_succeeds() {
let addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
let ctx = TlsContext::new("connect").with_peer(addr);
assert_eq!(ctx.peer_addr, Some(addr));
}
#[test]
fn test_tls_context_with_domain_sets_server_name_succeeds() {
let ctx = TlsContext::new("connect").with_domain("example.com");
assert_eq!(ctx.domain, Some("example.com".to_string()));
}
#[test]
fn test_tls_context_with_mode_sets_mode_field_succeeds() {
let ctx = TlsContext::new("handshake").with_mode("hybrid");
assert_eq!(ctx.mode, Some("hybrid".to_string()));
}
#[test]
fn test_tls_context_with_kex_method_sets_kex_field_succeeds() {
let ctx = TlsContext::new("kex").with_kex_method("X25519MLKEM768");
assert_eq!(ctx.kex_method, Some("X25519MLKEM768".to_string()));
}
#[test]
fn test_tls_context_format_produces_expected_string_has_correct_size() {
let addr: SocketAddr = "10.0.0.1:8443".parse().unwrap();
let ctx = TlsContext::with_trace("connect", "trace-abc".to_string())
.with_peer(addr)
.with_domain("example.com")
.with_mode("hybrid");
let formatted = ctx.format();
assert!(formatted.contains("op=connect"));
assert!(formatted.contains("trace=trace-abc"));
assert!(formatted.contains("peer=10.0.0.1:8443"));
assert!(formatted.contains("domain=example.com"));
assert!(formatted.contains("mode=hybrid"));
}
#[test]
fn test_tls_context_display_produces_expected_string_succeeds() {
let ctx = TlsContext::new("test");
let display = format!("{}", ctx);
assert!(display.contains("op=test"));
assert!(display.contains("id="));
}
#[test]
fn test_tls_context_elapsed_returns_duration_succeeds() {
let ctx = TlsContext::new("timed");
let elapsed = ctx.elapsed();
assert!(elapsed < std::time::Duration::from_secs(5));
}
#[test]
fn test_tls_context_child_inherits_fields_is_correct() {
let addr: SocketAddr = "10.0.0.1:443".parse().unwrap();
let parent = TlsContext::new("parent")
.with_peer(addr)
.with_domain("example.com")
.with_mode("hybrid")
.with_kex_method("X25519");
let child = parent.child("child");
assert_eq!(child.operation_name, "child");
assert_eq!(child.parent_span_id, Some(parent.operation_id.clone()));
assert_eq!(child.peer_addr, parent.peer_addr);
assert_eq!(child.domain, parent.domain);
assert_eq!(child.mode, parent.mode);
assert_eq!(child.kex_method, parent.kex_method);
}
#[test]
fn test_tls_context_get_metadata_missing_returns_none() {
let ctx = TlsContext::new("test");
assert!(ctx.get_metadata("nonexistent").is_none());
}
#[test]
fn test_error_chain_default_is_empty() {
let chain = ErrorChain::default();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
}
#[test]
fn test_error_chain_format_empty_produces_empty_string_fails() {
let chain = ErrorChain::new();
assert_eq!(chain.format(), "No errors");
}
#[test]
fn test_error_chain_format_with_errors_produces_expected_string_fails() {
let mut chain = ErrorChain::new();
let ctx = TlsContext::new("test");
chain.push(ErrorLink::new("IoError", "Connection refused", &ctx));
chain.push(ErrorLink::with_code("TlsError", "Handshake failed", "E001", &ctx));
let formatted = chain.format();
assert!(formatted.contains("IoError"));
assert!(formatted.contains("Connection refused"));
assert!(formatted.contains("TlsError"));
assert!(formatted.contains("E001"));
}
#[test]
fn test_error_chain_display_produces_expected_string_fails() {
let chain = ErrorChain::new();
let display = format!("{}", chain);
assert_eq!(display, "No errors");
}
#[test]
fn test_error_chain_push_tls_error_succeeds() {
let mut chain = ErrorChain::new();
let ctx = TlsContext::new("test");
let tls_err = crate::tls::error::TlsError::Config {
message: "bad config".to_string(),
field: Some("mode".to_string()),
code: crate::tls::error::ErrorCode::InvalidConfig,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
chain.push_tls_error(&tls_err, &ctx);
assert_eq!(chain.len(), 1);
assert!(!chain.is_empty());
}
#[test]
fn test_error_link_new_stores_message_fails() {
let ctx = TlsContext::new("test");
let link = ErrorLink::new("MyError", "Something failed", &ctx);
assert_eq!(link.error_type, "MyError");
assert_eq!(link.message, "Something failed");
assert!(link.code.is_none());
}
#[test]
fn test_error_link_with_code_stores_code_fails() {
let ctx = TlsContext::new("test");
let link = ErrorLink::with_code("MyError", "Failed", "E404", &ctx);
assert_eq!(link.code, Some("E404".to_string()));
}
#[test]
fn test_system_info_format_produces_expected_string_has_correct_size() {
let info = SystemInfo::collect();
let formatted = info.format();
assert!(formatted.contains("Platform:"));
assert!(formatted.contains("Rust:"));
assert!(formatted.contains("LatticeArc-TLS:"));
assert!(formatted.contains("Features:"));
}
#[test]
fn test_system_info_features_lists_enabled_features_is_correct() {
let info = SystemInfo::collect();
assert!(info.features.contains(&"pq".to_string()));
assert!(info.features.contains(&"hybrid".to_string()));
}
#[test]
fn test_diagnostic_info_new_stores_fields_succeeds() {
let ctx = TlsContext::new("failing_op");
let err = crate::tls::error::TlsError::Config {
message: "test error".to_string(),
field: None,
code: crate::tls::error::ErrorCode::InvalidConfig,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let diag = DiagnosticInfo::new(&ctx, &err);
assert!(!diag.chain.is_empty());
assert!(!diag.recommendations.is_empty());
assert!(!diag.system_info.platform.is_empty());
}
#[test]
fn test_diagnostic_info_format_produces_expected_string_has_correct_size() {
let ctx = TlsContext::new("test_op").with_domain("example.com");
let err = crate::tls::error::TlsError::Config {
message: "invalid".to_string(),
field: None,
code: crate::tls::error::ErrorCode::InvalidConfig,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let diag = DiagnosticInfo::new(&ctx, &err);
let formatted = diag.format();
assert!(formatted.contains("TLS Error Diagnostic"));
assert!(formatted.contains("Error Chain"));
assert!(formatted.contains("System Info"));
assert!(formatted.contains("Recommendations"));
}
#[test]
fn test_diagnostic_info_display_produces_expected_string_succeeds() {
let ctx = TlsContext::new("test_op");
let err = crate::tls::error::TlsError::Config {
message: "test".to_string(),
field: None,
code: crate::tls::error::ErrorCode::InvalidConfig,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let diag = DiagnosticInfo::new(&ctx, &err);
let display = format!("{}", diag);
assert!(display.contains("TLS Error Diagnostic"));
}
#[test]
fn test_recommendations_pq_not_available_returns_classical_recommendation_succeeds() {
let err = crate::tls::error::TlsError::PqNotAvailable {
message: "not available".to_string(),
code: crate::tls::error::ErrorCode::PqNotAvailable,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("classical")));
}
#[test]
fn test_recommendations_certificate_expired_returns_correct_recommendation_succeeds() {
let err = crate::tls::error::TlsError::Certificate {
message: "expired".to_string(),
subject: None,
issuer: None,
code: crate::tls::error::ErrorCode::CertificateExpired,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("expired")));
}
#[test]
fn test_recommendations_hostname_mismatch_returns_sni_recommendation_fails() {
let err = crate::tls::error::TlsError::Certificate {
message: "hostname mismatch".to_string(),
subject: None,
issuer: None,
code: crate::tls::error::ErrorCode::CertificateHostnameMismatch,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("SNI")));
}
#[test]
fn test_recommendations_chain_incomplete_returns_chain_recommendation_succeeds() {
let err = crate::tls::error::TlsError::Certificate {
message: "chain".to_string(),
subject: None,
issuer: None,
code: crate::tls::error::ErrorCode::CertificateChainIncomplete,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("chain")));
}
#[test]
fn test_recommendations_io_connection_refused_returns_correct_recommendation_succeeds() {
let err = crate::tls::error::TlsError::Io {
message: "refused".to_string(),
source: None,
code: crate::tls::error::ErrorCode::ConnectionRefused,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("firewall") || r.contains("running")));
}
#[test]
fn test_recommendations_io_timeout_returns_correct_recommendation_succeeds() {
let err = crate::tls::error::TlsError::Io {
message: "timeout".to_string(),
source: None,
code: crate::tls::error::ErrorCode::ConnectionTimeout,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("network") || r.contains("responsive")));
}
#[test]
fn test_recommendations_io_dns_returns_dns_recommendation_succeeds() {
let err = crate::tls::error::TlsError::Io {
message: "dns".to_string(),
source: None,
code: crate::tls::error::ErrorCode::DnsResolutionFailed,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("DNS")));
}
#[test]
fn test_recommendations_handshake_protocol_mismatch_returns_version_recommendation_fails() {
let err = crate::tls::error::TlsError::Handshake {
message: "version".to_string(),
state: "ClientHello".to_string(),
code: crate::tls::error::ErrorCode::ProtocolVersionMismatch,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("TLS version") || r.contains("TLS 1.3")));
}
#[test]
fn test_recommendations_handshake_cipher_mismatch_returns_cipher_recommendation_fails() {
let err = crate::tls::error::TlsError::Handshake {
message: "cipher".to_string(),
state: "ServerHello".to_string(),
code: crate::tls::error::ErrorCode::CipherSuiteMismatch,
context: Box::default(),
recovery: Box::new(crate::tls::error::RecoveryHint::NoRecovery),
};
let recs = generate_recommendations(&err);
assert!(recs.iter().any(|r| r.contains("cipher")));
}
}