diff --git a/.github/workflows/mockoon.yaml b/.github/workflows/mockoon.yaml index 0c8c876bd..1063c2196 100644 --- a/.github/workflows/mockoon.yaml +++ b/.github/workflows/mockoon.yaml @@ -8,7 +8,7 @@ name: "Mockoon Tests" branches: [master] jobs: - mockoon-tests: + mockoon-verifier-tests: runs-on: ubuntu-latest container: image: quay.io/keylime/keylime-ci:latest @@ -24,5 +24,93 @@ jobs: port: 3000 - name: Set git safe.directory for the working directory run: git config --system --add safe.directory "$PWD" - - name: Mockoon tests custom script execution + - name: Mockoon verifier tests custom script execution run: bash tests/mockoon_tests.sh + + mockoon-registrar-tests: + runs-on: ubuntu-latest + container: + image: quay.io/keylime/keylime-ci:latest + steps: + - uses: actions/checkout@v5 + - name: NPM installation + run: dnf install -y npm + - name: Pre-Mockoon system debugging + run: | + echo "======== PRE-MOCKOON SYSTEM STATE ========" + echo "Available system tools:" + command -v lsof && echo "✓ lsof available" || + echo "✗ lsof not available" + command -v netstat && echo "✓ netstat available" || + echo "✗ netstat not available" + command -v ss && echo "✓ ss available" || + echo "✗ ss available" + command -v curl && echo "✓ curl available" || + echo "✗ curl not available" + command -v docker && echo "✓ docker available" || + echo "✗ docker not available" + + echo "" + echo "Current processes using port 3001 (should be none):" + lsof -i :3001 2>/dev/null || echo "No processes using port 3001" + + echo "" + echo "All listening ports:" + netstat -tulpn 2>/dev/null | head -20 || + ss -tulpn 2>/dev/null | head -20 || + echo "Cannot list ports" + + echo "" + echo "Current user and environment:" + echo "User: $(whoami)" + echo "UID: $(id -u)" + echo "Groups: $(id -G)" + echo "HOME: $HOME" + echo "PWD: $PWD" + echo "CI: ${CI:-not_set}" + echo "GITHUB_ACTIONS: ${GITHUB_ACTIONS:-not_set}" + + echo "" + echo "Container/system info:" + echo "Hostname: $(hostname)" + uname -a + cat /etc/os-release | head -5 + echo "======== END PRE-MOCKOON SYSTEM STATE ========" + - name: Run Mockoon CLI + uses: mockoon/cli-action@v2 + with: + version: latest + data-file: keylime-push-model-agent/test-data/registrar.json + port: 3001 + - name: Post-Mockoon system debugging + run: | + echo "======== POST-MOCKOON SYSTEM STATE ========" + echo "Processes using port 3001 after Mockoon start:" + lsof -i :3001 2>/dev/null || + echo "No processes using port 3001 (unexpected!)" + + echo "" + echo "Mockoon-related processes:" + ps aux | grep -i mockoon | grep -v grep || + echo "No mockoon processes found" + + echo "" + echo "Node.js processes:" + ps aux | grep -E "(node|npm)" | grep -v grep || + echo "No node/npm processes found" + + echo "" + echo "Test HTTP connectivity to port 3001:" + curl -sI --connect-timeout 5 http://localhost:3001 2>/dev/null || + echo "Failed to connect to port 3001" + + echo "" + echo "Network connections:" + netstat -tulpn 2>/dev/null | grep ':3001' || + ss -tulpn 2>/dev/null | grep ':3001' || + echo "No port 3001 connections found" + echo "======== END POST-MOCKOON SYSTEM STATE ========" + - name: Set git safe.directory for the working directory + run: git config --system --add safe.directory "$PWD" + - name: Mockoon registrar tests custom script execution + run: bash tests/mockoon_registrar_tests.sh diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs index 02b231035..de8a65085 100644 --- a/keylime-agent/src/main.rs +++ b/keylime-agent/src/main.rs @@ -515,11 +515,12 @@ async fn main() -> Result<()> { // Load or generate mTLS key pair (separate from payload keys) // The mTLS key is always persistent, stored at the configured path. + // Uses ECC P-256 by default for better security and performance let key_path = Path::new(&config.server_key); let (mtls_pub, mtls_priv) = crypto::load_or_generate_key( key_path, Some(config.server_key_password.as_ref()), - keylime::algorithms::EncryptionAlgorithm::Rsa2048, + keylime::algorithms::EncryptionAlgorithm::Ecc256, false, // Don't validate algorithm for mTLS keys (for backward compatibility) )?; @@ -608,6 +609,12 @@ async fn main() -> Result<()> { registrar_port: config.registrar_port, enable_iak_idevid: config.enable_iak_idevid, ek_handle: config.ek_handle.clone(), + // Pull model agent does not use TLS for registrar communication + registrar_ca_cert: None, + registrar_client_cert: None, + registrar_client_key: None, + registrar_insecure: None, + registrar_timeout: None, }; let aa = AgentRegistration { diff --git a/keylime-push-model-agent/src/main.rs b/keylime-push-model-agent/src/main.rs index f671e3804..af94d24dd 100644 --- a/keylime-push-model-agent/src/main.rs +++ b/keylime-push-model-agent/src/main.rs @@ -106,6 +106,80 @@ fn get_avoid_tpm_from_args(args: &Args) -> bool { args.avoid_tpm.unwrap_or(false) } +fn create_registrar_tls_config( + config: &T, + timeout: u64, +) -> Option { + if !config.registrar_tls_enabled() { + info!("Registrar TLS enabled: false - using plain HTTP"); + return None; + } + + let ca_cert = config.registrar_tls_ca_cert(); + let client_cert = config.registrar_tls_client_cert(); + let client_key = config.registrar_tls_client_key(); + + info!("Registrar TLS enabled: true"); + debug!("Registrar CA certificate: {}", ca_cert); + debug!("Registrar client certificate: {}", client_cert); + debug!("Registrar client key: {}", client_key); + + // Only use TLS if all certificate paths are provided + if !ca_cert.is_empty() + && !client_cert.is_empty() + && !client_key.is_empty() + { + info!("Registrar TLS configuration complete - using HTTPS"); + return Some(registration::RegistrarTlsConfig { + ca_cert: Some(ca_cert.to_string()), + client_cert: Some(client_cert.to_string()), + client_key: Some(client_key.to_string()), + insecure: None, + timeout: Some(timeout), + }); + } + + // Check for partial configuration + let provided_count = [ + !ca_cert.is_empty(), + !client_cert.is_empty(), + !client_key.is_empty(), + ] + .iter() + .filter(|&&x| x) + .count(); + + if provided_count > 0 { + warn!( + "Registrar TLS is enabled but only {} out of 3 certificate paths are configured.", + provided_count + ); + warn!("This may indicate a configuration mistake."); + warn!( + "Missing paths: {}{}{}", + if ca_cert.is_empty() { + "registrar_tls_ca_cert " + } else { + "" + }, + if client_cert.is_empty() { + "registrar_tls_client_cert " + } else { + "" + }, + if client_key.is_empty() { + "registrar_tls_client_key " + } else { + "" + } + ); + } else { + warn!("Registrar TLS is enabled but no certificate paths are configured."); + } + warn!("Falling back to plain HTTP for Registrar communication."); + None +} + async fn run(args: &Args) -> Result<()> { match args.verifier_url { Some(ref url) if url.is_empty() => { @@ -196,11 +270,17 @@ async fn run(args: &Args) -> Result<()> { }; let attestation_client = attestation::AttestationClient::new(&neg_config)?; + + // Create Registrar TLS config from configuration + let registrar_tls_config = + create_registrar_tls_config(config, args.timeout); + let mut state_machine = state_machine::StateMachine::new( attestation_client, neg_config, ctx_info, config.attestation_interval_seconds(), + registrar_tls_config, ); state_machine.run().await; Ok(()) @@ -217,6 +297,257 @@ async fn main() -> Result<()> { mod tests { use super::*; + // Mock config for testing + struct MockConfig { + tls_enabled: bool, + ca_cert: String, + client_cert: String, + client_key: String, + backoff_max_delay: Option, + backoff_max_retries: Option, + backoff_initial_delay: Option, + } + + impl PushModelConfigTrait for MockConfig { + fn registrar_tls_enabled(&self) -> bool { + self.tls_enabled + } + + fn registrar_tls_ca_cert(&self) -> &str { + &self.ca_cert + } + + fn registrar_tls_client_cert(&self) -> &str { + &self.client_cert + } + + fn registrar_tls_client_key(&self) -> &str { + &self.client_key + } + + // Dummy implementations for other required trait methods + fn agent_data_path(&self) -> &str { + "" + } + fn api_versions( + &self, + ) -> Result, keylime::config::OverrideError> { + Ok(vec![]) + } + fn attestation_interval_seconds(&self) -> u64 { + 60 + } + fn certification_keys_server_identifier(&self) -> &str { + "" + } + fn contact_ip(&self) -> &str { + "" + } + fn contact_port(&self) -> u32 { + 0 + } + fn enable_authentication(&self) -> bool { + false + } + fn exponential_backoff_max_delay(&self) -> &Option { + &self.backoff_max_delay + } + fn exponential_backoff_max_retries(&self) -> &Option { + &self.backoff_max_retries + } + fn exponential_backoff_initial_delay(&self) -> &Option { + &self.backoff_initial_delay + } + fn enable_iak_idevid(&self) -> bool { + false + } + fn ek_handle( + &self, + ) -> Result { + Ok(String::new()) + } + fn ima_ml_count_file(&self) -> &str { + "" + } + fn measuredboot_ml_path(&self) -> &str { + "" + } + fn registrar_api_versions( + &self, + ) -> Result, keylime::list_parser::ListParsingError> + { + Ok(vec![]) + } + fn registrar_ip(&self) -> &str { + "" + } + fn registrar_port(&self) -> u32 { + 0 + } + fn server_cert(&self) -> &str { + "" + } + fn server_key(&self) -> &str { + "" + } + fn server_key_password(&self) -> &str { + "" + } + fn tpm_encryption_alg(&self) -> &str { + "" + } + fn tpm_hash_alg(&self) -> &str { + "" + } + fn tpm_signing_alg(&self) -> &str { + "" + } + fn uefi_logs_evidence_version(&self) -> &str { + "" + } + fn uuid(&self) -> &str { + "" + } + fn verifier_url(&self) -> &str { + "" + } + } + + #[test] + fn test_create_registrar_tls_config_disabled() { + // Test when TLS is disabled + let config = MockConfig { + tls_enabled: false, + ca_cert: "".to_string(), + client_cert: "".to_string(), + client_key: "".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_none()); + } + + #[test] + fn test_create_registrar_tls_config_enabled_complete() { + // Test when TLS is enabled with all certificate paths + let config = MockConfig { + tls_enabled: true, + ca_cert: "/path/to/ca.crt".to_string(), + client_cert: "/path/to/client.crt".to_string(), + client_key: "/path/to/client.key".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_some()); + + let tls_config = result.unwrap(); + assert_eq!(tls_config.ca_cert, Some("/path/to/ca.crt".to_string())); + assert_eq!( + tls_config.client_cert, + Some("/path/to/client.crt".to_string()) + ); + assert_eq!( + tls_config.client_key, + Some("/path/to/client.key".to_string()) + ); + assert_eq!(tls_config.timeout, Some(5000)); + assert_eq!(tls_config.insecure, None); + } + + #[test] + fn test_create_registrar_tls_config_enabled_no_certs() { + // Test when TLS is enabled but no certificate paths are provided + let config = MockConfig { + tls_enabled: true, + ca_cert: "".to_string(), + client_cert: "".to_string(), + client_key: "".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_none()); + } + + #[test] + fn test_create_registrar_tls_config_partial_one_cert() { + // Test when TLS is enabled with only 1 certificate path (partial config) + let config = MockConfig { + tls_enabled: true, + ca_cert: "/path/to/ca.crt".to_string(), + client_cert: "".to_string(), + client_key: "".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_none()); + } + + #[test] + fn test_create_registrar_tls_config_partial_two_certs() { + // Test when TLS is enabled with only 2 certificate paths (partial config) + let config = MockConfig { + tls_enabled: true, + ca_cert: "/path/to/ca.crt".to_string(), + client_cert: "/path/to/client.crt".to_string(), + client_key: "".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_none()); + } + + #[test] + fn test_create_registrar_tls_config_partial_different_combo() { + // Test another partial config combination (ca_cert and client_key only) + let config = MockConfig { + tls_enabled: true, + ca_cert: "/path/to/ca.crt".to_string(), + client_cert: "".to_string(), + client_key: "/path/to/client.key".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 5000); + assert!(result.is_none()); + } + + #[test] + fn test_create_registrar_tls_config_custom_timeout() { + // Test that custom timeout is properly set + let config = MockConfig { + tls_enabled: true, + ca_cert: "/path/to/ca.crt".to_string(), + client_cert: "/path/to/client.crt".to_string(), + client_key: "/path/to/client.key".to_string(), + backoff_max_delay: None, + backoff_max_retries: None, + backoff_initial_delay: None, + }; + + let result = create_registrar_tls_config(&config, 10000); + assert!(result.is_some()); + + let tls_config = result.unwrap(); + assert_eq!(tls_config.timeout, Some(10000)); + } + #[actix_rt::test] async fn run_test() { // Set arguments to avoid TPM diff --git a/keylime-push-model-agent/src/registration.rs b/keylime-push-model-agent/src/registration.rs index 1e2903067..0d2b48d65 100644 --- a/keylime-push-model-agent/src/registration.rs +++ b/keylime-push-model-agent/src/registration.rs @@ -8,12 +8,24 @@ use keylime::{ error::Result, }; +pub struct RegistrarTlsConfig { + pub ca_cert: Option, + pub client_cert: Option, + pub client_key: Option, + pub insecure: Option, + pub timeout: Option, +} + pub async fn check_registration( context_info: Option, + tls_config: Option, ) -> Result<()> { if context_info.is_some() { - crate::registration::register_agent(&mut context_info.unwrap()) - .await?; + crate::registration::register_agent( + &mut context_info.unwrap(), + tls_config, + ) + .await?; } Ok(()) } @@ -41,9 +53,23 @@ fn get_retry_config() -> Option { pub async fn register_agent( context_info: &mut context_info::ContextInfo, + tls_config: Option, ) -> Result<()> { let config = keylime::config::get_config(); + let (ca_cert, client_cert, client_key, insecure, timeout) = + if let Some(tls) = tls_config { + ( + tls.ca_cert, + tls.client_cert, + tls.client_key, + tls.insecure, + tls.timeout, + ) + } else { + (None, None, None, None, None) + }; + let ac = AgentRegistrationConfig { contact_ip: config.contact_ip().to_string(), contact_port: config.contact_port(), @@ -55,6 +81,11 @@ pub async fn register_agent( .ek_handle() .expect("failed to get ek_handle") .to_string(), + registrar_ca_cert: ca_cert, + registrar_client_cert: client_cert, + registrar_client_key: client_key, + registrar_insecure: insecure, + registrar_timeout: timeout, }; let cert_config = cert::CertificateConfig { @@ -111,7 +142,7 @@ mod tests { let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); let _config = get_testing_config(tmpdir.path(), None); - let result = check_registration(None).await; + let result = check_registration(None, None).await; assert!(result.is_ok()); } @@ -130,13 +161,542 @@ mod tests { config.exponential_backoff_initial_delay = None; config.exponential_backoff_max_retries = None; config.exponential_backoff_max_delay = None; + // Use an invalid registrar address to guarantee failure + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; // Invalid port to ensure connection fails // Create guard that will automatically clear override when dropped let _guard = keylime::config::TestConfigGuard::new(config); let mut context_info = ContextInfo::new_from_str(alg_config) .expect("Failed to create context info from string"); - let result = register_agent(&mut context_info).await; + let result = register_agent(&mut context_info, None).await; + assert!(result.is_err()); + assert!(context_info.flush_context().is_ok()); + } + + #[actix_rt::test] + async fn test_registrar_tls_config_creation() { + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + assert_eq!(tls_config.ca_cert, Some("/path/to/ca.pem".to_string())); + assert_eq!( + tls_config.client_cert, + Some("/path/to/cert.pem".to_string()) + ); + assert_eq!( + tls_config.client_key, + Some("/path/to/key.pem".to_string()) + ); + assert_eq!(tls_config.insecure, Some(false)); + assert_eq!(tls_config.timeout, Some(5000)); + } + + #[actix_rt::test] + async fn test_registrar_tls_config_all_none() { + let tls_config = RegistrarTlsConfig { + ca_cert: None, + client_cert: None, + client_key: None, + insecure: None, + timeout: None, + }; + + assert_eq!(tls_config.ca_cert, None); + assert_eq!(tls_config.client_cert, None); + assert_eq!(tls_config.client_key, None); + assert_eq!(tls_config.insecure, None); + assert_eq!(tls_config.timeout, None); + } + + #[actix_rt::test] + async fn test_registrar_tls_config_partial() { + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: None, + client_key: None, + insecure: Some(true), + timeout: Some(10000), + }; + + assert_eq!(tls_config.ca_cert, Some("/path/to/ca.pem".to_string())); + assert_eq!(tls_config.client_cert, None); + assert_eq!(tls_config.client_key, None); + assert_eq!(tls_config.insecure, Some(true)); + assert_eq!(tls_config.timeout, Some(10000)); + } + + #[actix_rt::test] + async fn test_registrar_tls_config_empty_strings() { + let tls_config = RegistrarTlsConfig { + ca_cert: Some("".to_string()), + client_cert: Some("".to_string()), + client_key: Some("".to_string()), + insecure: Some(false), + timeout: Some(0), + }; + + assert_eq!(tls_config.ca_cert, Some("".to_string())); + assert_eq!(tls_config.client_cert, Some("".to_string())); + assert_eq!(tls_config.client_key, Some("".to_string())); + assert_eq!(tls_config.insecure, Some(false)); + assert_eq!(tls_config.timeout, Some(0)); + } + + #[actix_rt::test] + async fn test_check_registration_with_none_context() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); + let _config = get_testing_config(tmpdir.path(), None); + + // Test with None context_info and None tls_config + let result = check_registration(None, None).await; + assert!(result.is_ok()); + } + + #[actix_rt::test] + async fn test_check_registration_with_tls_config_none_context() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); + let _config = get_testing_config(tmpdir.path(), None); + + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + // Test with None context_info but Some tls_config (should not register) + let result = check_registration(None, Some(tls_config)).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_register_agent_with_tls_config() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; // Invalid port to ensure connection fails + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + let result = + register_agent(&mut context_info, Some(tls_config)).await; + assert!(result.is_err()); // Should fail due to invalid port + assert!(context_info.flush_context().is_ok()); + } + + #[tokio::test] + async fn test_register_agent_with_partial_tls_config() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + // Test with only CA cert + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: None, + client_key: None, + insecure: None, + timeout: Some(5000), + }; + + let result = + register_agent(&mut context_info, Some(tls_config)).await; + assert!(result.is_err()); + assert!(context_info.flush_context().is_ok()); + } + + #[tokio::test] + async fn test_register_agent_with_insecure_tls() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + // Test with insecure=true + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: Some(true), + timeout: Some(5000), + }; + + let result = + register_agent(&mut context_info, Some(tls_config)).await; + assert!(result.is_err()); + assert!(context_info.flush_context().is_ok()); + } + + #[actix_rt::test] + async fn test_get_retry_config_all_none() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); + let mut config = get_testing_config(tmpdir.path(), None); + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let retry_config = get_retry_config(); + assert!(retry_config.is_none()); + } + + #[actix_rt::test] + async fn test_get_retry_config_with_values() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); + let mut config = get_testing_config(tmpdir.path(), None); + config.exponential_backoff_initial_delay = Some(100); + config.exponential_backoff_max_retries = Some(5); + config.exponential_backoff_max_delay = Some(2000); + + let _guard = keylime::config::TestConfigGuard::new(config); + + let retry_config = get_retry_config(); + assert!(retry_config.is_some()); + let retry = retry_config.unwrap(); //#[allow_ci] + assert_eq!(retry.initial_delay_ms, 100); + assert_eq!(retry.max_retries, 5); + assert_eq!(retry.max_delay_ms, Some(2000)); + } + + #[actix_rt::test] + async fn test_get_retry_config_partial() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tempdir"); + let mut config = get_testing_config(tmpdir.path(), None); + config.exponential_backoff_initial_delay = Some(200); + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let retry_config = get_retry_config(); + assert!(retry_config.is_some()); + let retry = retry_config.unwrap(); //#[allow_ci] + assert_eq!(retry.initial_delay_ms, 200); + assert_eq!(retry.max_retries, 0); + assert_eq!(retry.max_delay_ms, None); + } + + #[actix_rt::test] + async fn test_registrar_tls_config_with_different_timeout_values() { + // Test with zero timeout + let tls_config_zero = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: None, + timeout: Some(0), + }; + assert_eq!(tls_config_zero.timeout, Some(0)); + + // Test with large timeout + let tls_config_large = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: None, + timeout: Some(300000), + }; + assert_eq!(tls_config_large.timeout, Some(300000)); + + // Test with None timeout + let tls_config_none = RegistrarTlsConfig { + ca_cert: Some("/path/to/ca.pem".to_string()), + client_cert: Some("/path/to/cert.pem".to_string()), + client_key: Some("/path/to/key.pem".to_string()), + insecure: None, + timeout: None, + }; + assert_eq!(tls_config_none.timeout, None); + } + + #[actix_rt::test] + async fn test_tls_config_extraction_some() { + let tls_config = Some(RegistrarTlsConfig { + ca_cert: Some("/ca.pem".to_string()), + client_cert: Some("/cert.pem".to_string()), + client_key: Some("/key.pem".to_string()), + insecure: Some(false), + timeout: Some(5000), + }); + + let (ca_cert, client_cert, client_key, insecure, timeout) = + if let Some(tls) = tls_config { + ( + tls.ca_cert, + tls.client_cert, + tls.client_key, + tls.insecure, + tls.timeout, + ) + } else { + (None, None, None, None, None) + }; + + assert_eq!(ca_cert, Some("/ca.pem".to_string())); + assert_eq!(client_cert, Some("/cert.pem".to_string())); + assert_eq!(client_key, Some("/key.pem".to_string())); + assert_eq!(insecure, Some(false)); + assert_eq!(timeout, Some(5000)); + } + + #[actix_rt::test] + async fn test_tls_config_extraction_none() { + let tls_config: Option = None; + + let (ca_cert, client_cert, client_key, insecure, timeout) = + if let Some(tls) = tls_config { + ( + tls.ca_cert, + tls.client_cert, + tls.client_key, + tls.insecure, + tls.timeout, + ) + } else { + (None, None, None, None, None) + }; + + assert_eq!(ca_cert, None); + assert_eq!(client_cert, None); + assert_eq!(client_key, None); + assert_eq!(insecure, None); + assert_eq!(timeout, None); + } + + #[tokio::test] + async fn test_register_agent_with_real_tls_certs() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + keylime::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + // Verify files were created + assert!(ca_path.exists()); + assert!(cert_path.exists()); + assert!(key_path.exists()); + + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; // Invalid port to ensure connection fails + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + let tls_config = RegistrarTlsConfig { + ca_cert: Some(ca_path.to_string_lossy().to_string()), + client_cert: Some(cert_path.to_string_lossy().to_string()), + client_key: Some(key_path.to_string_lossy().to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + // Should fail due to invalid port, but TLS config should be processed + let result = + register_agent(&mut context_info, Some(tls_config)).await; + assert!(result.is_err()); + assert!(context_info.flush_context().is_ok()); + } + + #[tokio::test] + async fn test_register_agent_with_nonexistent_tls_certs() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 8891; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + // Use paths to non-existent certificate files + let tls_config = RegistrarTlsConfig { + ca_cert: Some("/nonexistent/ca.pem".to_string()), + client_cert: Some("/nonexistent/cert.pem".to_string()), + client_key: Some("/nonexistent/key.pem".to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + // Should fail due to missing certificate files + let result = + register_agent(&mut context_info, Some(tls_config)).await; + assert!(result.is_err()); + assert!(context_info.flush_context().is_ok()); + } + + #[actix_rt::test] + async fn test_tls_config_all_fields_set() { + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + keylime::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let tls_config = RegistrarTlsConfig { + ca_cert: Some(ca_path.to_string_lossy().to_string()), + client_cert: Some(cert_path.to_string_lossy().to_string()), + client_key: Some(key_path.to_string_lossy().to_string()), + insecure: Some(false), + timeout: Some(10000), + }; + + // Verify all fields are set correctly + assert_eq!( + tls_config.ca_cert, + Some(ca_path.to_string_lossy().to_string()) + ); + assert_eq!( + tls_config.client_cert, + Some(cert_path.to_string_lossy().to_string()) + ); + assert_eq!( + tls_config.client_key, + Some(key_path.to_string_lossy().to_string()) + ); + assert_eq!(tls_config.insecure, Some(false)); + assert_eq!(tls_config.timeout, Some(10000)); + } + + #[tokio::test] + async fn test_register_agent_tls_with_empty_cert_paths() { + let _mutex = testing::lock_tests().await; + + let tmpdir = tempfile::tempdir().expect("failed to create tmpdir"); + let mut config = get_testing_config(tmpdir.path(), None); + let alg_config = AlgorithmConfigurationString { + tpm_encryption_alg: "rsa".to_string(), + tpm_hash_alg: "sha256".to_string(), + tpm_signing_alg: "rsassa".to_string(), + agent_data_path: "".to_string(), + }; + + config.exponential_backoff_initial_delay = None; + config.exponential_backoff_max_retries = None; + config.exponential_backoff_max_delay = None; + config.registrar_ip = "127.0.0.1".to_string(); + config.registrar_port = 1; + + let _guard = keylime::config::TestConfigGuard::new(config); + + let mut context_info = ContextInfo::new_from_str(alg_config) + .expect("Failed to create context info from string"); + + // Empty paths should result in HTTP fallback + let tls_config = RegistrarTlsConfig { + ca_cert: Some("".to_string()), + client_cert: Some("".to_string()), + client_key: Some("".to_string()), + insecure: Some(false), + timeout: Some(5000), + }; + + let result = + register_agent(&mut context_info, Some(tls_config)).await; assert!(result.is_err()); assert!(context_info.flush_context().is_ok()); } diff --git a/keylime-push-model-agent/src/state_machine.rs b/keylime-push-model-agent/src/state_machine.rs index ffc22cb92..18ba45392 100644 --- a/keylime-push-model-agent/src/state_machine.rs +++ b/keylime-push-model-agent/src/state_machine.rs @@ -6,6 +6,7 @@ use crate::attestation::{ }; #[cfg(not(all(test, feature = "testing")))] use crate::registration; +use crate::registration::RegistrarTlsConfig; #[cfg(test)] use crate::DEFAULT_ATTESTATION_INTERVAL_SECONDS; use anyhow::anyhow; @@ -31,6 +32,7 @@ pub struct StateMachine<'a> { negotiation_config: NegotiationConfig<'a>, context_info: Option, measurement_interval: Duration, + registrar_tls_config: Option, } impl<'a> StateMachine<'a> { @@ -39,6 +41,7 @@ impl<'a> StateMachine<'a> { negotiation_config: NegotiationConfig<'a>, context_info: Option, attestation_interval_seconds: u64, + registrar_tls_config: Option, ) -> Self { let initial_state = State::Unregistered; let measurement_interval = @@ -50,6 +53,7 @@ impl<'a> StateMachine<'a> { negotiation_config, context_info, measurement_interval, + registrar_tls_config, } } @@ -104,8 +108,11 @@ impl<'a> StateMachine<'a> { } async fn register(&mut self) { - let res = - registration::check_registration(self.context_info.clone()).await; + let res = registration::check_registration( + self.context_info.clone(), + self.registrar_tls_config.take(), + ) + .await; match res { Ok(()) => { @@ -275,6 +282,8 @@ mod registration { use keylime::context_info::ContextInfo; use std::sync::{Arc, Mutex, OnceLock}; + pub use crate::registration::RegistrarTlsConfig; + static MOCK_RESULT: OnceLock>>> = OnceLock::new(); @@ -284,6 +293,7 @@ mod registration { pub async fn check_registration( _context_info: Option, + _tls_config: Option, ) -> anyhow::Result<()> { let result = get_mock_result().lock().unwrap().clone(); result.map_err(|e| anyhow!(e)) @@ -441,6 +451,7 @@ mod tpm_tests { neg_config.clone(), None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ), guard, ); @@ -453,6 +464,7 @@ mod tpm_tests { neg_config.clone(), Some(context_info), DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ), guard, ) @@ -627,9 +639,11 @@ mod tpm_tests { let mut context_info = sm.context_info.clone().unwrap(); registration::set_mock_result(Ok(())); - let res = - registration::check_registration(Some(context_info.clone())) - .await; + let res = registration::check_registration( + Some(context_info.clone()), + None, + ) + .await; match res { Ok(()) => { @@ -670,8 +684,11 @@ mod tpm_tests { agent_data_path: "".to_string(), }) .expect("This test requires TPM access with proper permissions"); - let _ = registration::check_registration(Some(context_info.clone())) - .await; + let _ = registration::check_registration( + Some(context_info.clone()), + None, + ) + .await; let mock_server = MockServer::start().await; @@ -720,6 +737,7 @@ mod tpm_tests { neg_config, Some(context_info.clone()), DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // We can't easily test the full run() method since it loops indefinitely. @@ -786,6 +804,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Should start in Unregistered state when no context info is provided. @@ -813,6 +832,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); let debug_output = format!("{:?}", state_machine.get_current_state()); @@ -836,6 +856,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Start in Unregistered state. @@ -872,6 +893,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Should start in Unregistered state. @@ -898,6 +920,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Verify that context_info is None when not provided. @@ -921,6 +944,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Test that the configuration references are stored correctly. @@ -948,6 +972,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Manually set to Failed state to test error handling. @@ -979,6 +1004,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Manually set to Failed state to test error handling. @@ -1005,6 +1031,7 @@ mod tests { test_config1, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); assert!(matches!( state_machine1.get_current_state(), @@ -1020,6 +1047,7 @@ mod tests { test_config2, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); assert!(matches!( state_machine2.get_current_state(), @@ -1044,6 +1072,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Test that avoid_tpm is properly configured through the test config. @@ -1071,6 +1100,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Set a test error and verify debug formatting works. @@ -1103,6 +1133,7 @@ mod tests { test_config, None, DEFAULT_ATTESTATION_INTERVAL_SECONDS, + None, ); // Test with valid seconds_to_next_attestation field in meta object. diff --git a/keylime-push-model-agent/test-data/registrar.json b/keylime-push-model-agent/test-data/registrar.json new file mode 100644 index 000000000..1f5d9c4aa --- /dev/null +++ b/keylime-push-model-agent/test-data/registrar.json @@ -0,0 +1,319 @@ +{ + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "lastMigration": 33, + "name": "Registrar", + "endpointPrefix": "", + "latency": 0, + "port": 3001, + "hostname": "", + "folders": [], + "routes": [ + { + "uuid": "ver-endpoint-uuid-001", + "type": "http", + "documentation": "Registrar API version endpoint", + "method": "get", + "endpoint": "version", + "responses": [ + { + "uuid": "ver-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {\n \"current_version\": \"1.2\",\n \"supported_versions\": [\"1.0\", \"1.1\", \"1.2\"]\n }\n}", + "latency": 0, + "statusCode": 200, + "label": "API version response", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "reg-endpoint-uuid-001", + "type": "http", + "documentation": "Agent registration endpoint - POST /v1.2/agents/{uuid}", + "method": "post", + "endpoint": "v1.2/agents/:uuid", + "responses": [ + { + "uuid": "reg-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {\n \"blob\": \"dGVzdC1ibG9iLWRhdGE=\"\n }\n}", + "latency": 0, + "statusCode": 200, + "label": "Successful registration", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "act-endpoint-uuid-001", + "type": "http", + "documentation": "Agent activation endpoint - PUT /v1.2/agents/{uuid}", + "method": "put", + "endpoint": "v1.2/agents/:uuid", + "responses": [ + { + "uuid": "act-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {}\n}", + "latency": 0, + "statusCode": 200, + "label": "Successful activation", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "reg-tls-endpoint-uuid-001", + "type": "http", + "documentation": "TLS-secured agent registration endpoint - POST /v1.2/agents/{uuid}", + "method": "post", + "endpoint": "tls/v1.2/agents/:uuid", + "responses": [ + { + "uuid": "reg-tls-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {\n \"blob\": \"dGxzLWJsb2ItZGF0YQ==\"\n }\n}", + "latency": 0, + "statusCode": 200, + "label": "Successful TLS registration", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "act-tls-endpoint-uuid-001", + "type": "http", + "documentation": "TLS-secured agent activation endpoint - PUT /v1.2/agents/{uuid}", + "method": "put", + "endpoint": "tls/v1.2/agents/:uuid", + "responses": [ + { + "uuid": "act-tls-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {}\n}", + "latency": 0, + "statusCode": 200, + "label": "Successful TLS activation", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, + { + "uuid": "ver-tls-endpoint-uuid-001", + "type": "http", + "documentation": "TLS-secured Registrar API version endpoint", + "method": "get", + "endpoint": "tls/version", + "responses": [ + { + "uuid": "ver-tls-response-uuid-001", + "body": "{\n \"code\": 200,\n \"status\": \"Success\",\n \"results\": {\n \"current_version\": \"1.2\",\n \"supported_versions\": [\"1.0\", \"1.1\", \"1.2\"]\n }\n}", + "latency": 0, + "statusCode": 200, + "label": "TLS API version response", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + } + ], + "rootChildren": [ + { + "type": "route", + "uuid": "ver-endpoint-uuid-001" + }, + { + "type": "route", + "uuid": "reg-endpoint-uuid-001" + }, + { + "type": "route", + "uuid": "act-endpoint-uuid-001" + }, + { + "type": "route", + "uuid": "reg-tls-endpoint-uuid-001" + }, + { + "type": "route", + "uuid": "act-tls-endpoint-uuid-001" + }, + { + "type": "route", + "uuid": "ver-tls-endpoint-uuid-001" + } + ], + "proxyMode": false, + "proxyHost": "", + "proxyRemovePrefix": false, + "tlsOptions": { + "enabled": false, + "type": "CERT", + "pfxPath": "", + "certPath": "", + "keyPath": "", + "caPath": "", + "passphrase": "" + }, + "cors": true, + "headers": [ + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Access-Control-Allow-Methods", + "value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS" + }, + { + "key": "Access-Control-Allow-Headers", + "value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With" + } + ], + "proxyReqHeaders": [ + { + "key": "", + "value": "" + } + ], + "proxyResHeaders": [ + { + "key": "", + "value": "" + } + ], + "data": [], + "callbacks": [] +} diff --git a/keylime/src/agent_registration.rs b/keylime/src/agent_registration.rs index 3aa2c669a..24cfe9e23 100644 --- a/keylime/src/agent_registration.rs +++ b/keylime/src/agent_registration.rs @@ -23,6 +23,11 @@ pub struct AgentRegistrationConfig { pub enable_iak_idevid: bool, pub registrar_ip: String, pub registrar_port: u32, + pub registrar_ca_cert: Option, + pub registrar_client_cert: Option, + pub registrar_client_key: Option, + pub registrar_insecure: Option, + pub registrar_timeout: Option, } #[derive(Debug, Default, Clone)] @@ -116,12 +121,29 @@ pub async fn register_agent( // Build the registrar client // Create a RegistrarClientBuilder and set the parameters - let mut registrar_client = RegistrarClientBuilder::new() + let mut builder = RegistrarClientBuilder::new() .registrar_address(ac.registrar_ip.clone()) .registrar_port(ac.registrar_port) - .retry_config(aa.retry_config.clone()) - .build() - .await?; + .retry_config(aa.retry_config.clone()); + + // Add TLS configuration if provided + if let Some(ca_cert) = &ac.registrar_ca_cert { + builder = builder.ca_certificate(ca_cert.clone()); + } + if let Some(client_cert) = &ac.registrar_client_cert { + builder = builder.certificate(client_cert.clone()); + } + if let Some(client_key) = &ac.registrar_client_key { + builder = builder.key(client_key.clone()); + } + if let Some(insecure) = ac.registrar_insecure { + builder = builder.insecure(insecure); + } + if let Some(timeout) = ac.registrar_timeout { + builder = builder.timeout(timeout); + } + + let mut registrar_client = builder.build().await?; // Request keyblob material let keyblob = registrar_client.register_agent(&ai).await?; diff --git a/keylime/src/cert.rs b/keylime/src/cert.rs index 7d39e8cdc..51828e684 100644 --- a/keylime/src/cert.rs +++ b/keylime/src/cert.rs @@ -16,14 +16,19 @@ pub struct CertificateConfig { pub fn cert_from_server_key( config: &CertificateConfig, ) -> Result<(X509, PKey)> { + use openssl::ec::EcGroup; + use openssl::nid::Nid; + let cert: X509; let (mtls_pub, mtls_priv) = match config.server_key.as_ref() { "" => { debug!( "The server_key option was not set in the configuration file" ); - debug!("Generating new key pair"); - crypto::rsa_generate_pair(2048)? + debug!("Generating new ECC P-256 key pair"); + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1) + .map_err(crypto::CryptoError::ECGroupFromNidError)?; + crypto::ecc_generate_pair(&group)? } path => { let key_path = Path::new(&path); @@ -37,8 +42,11 @@ pub fn cert_from_server_key( Some(&config.server_key_password), )? } else { - debug!("Generating new key pair"); - let (public, private) = crypto::rsa_generate_pair(2048)?; + debug!("Generating new ECC P-256 key pair"); + let group = + EcGroup::from_curve_name(Nid::X9_62_PRIME256V1) + .map_err(crypto::CryptoError::ECGroupFromNidError)?; + let (public, private) = crypto::ecc_generate_pair(&group)?; // Write the generated key to the file crypto::write_key_pair( &private, diff --git a/keylime/src/config/base.rs b/keylime/src/config/base.rs index 251ca38a5..9f322168f 100644 --- a/keylime/src/config/base.rs +++ b/keylime/src/config/base.rs @@ -35,6 +35,10 @@ pub static DEFAULT_IP: &str = "127.0.0.1"; pub static DEFAULT_PORT: u32 = 9002; pub static DEFAULT_REGISTRAR_IP: &str = "127.0.0.1"; pub static DEFAULT_REGISTRAR_PORT: u32 = 8890; +pub static DEFAULT_REGISTRAR_TLS_ENABLED: bool = false; +pub static DEFAULT_REGISTRAR_TLS_CA_CERT: &str = ""; +pub static DEFAULT_REGISTRAR_TLS_CLIENT_CERT: &str = ""; +pub static DEFAULT_REGISTRAR_TLS_CLIENT_KEY: &str = ""; pub static DEFAULT_KEYLIME_DIR: &str = "/var/lib/keylime"; pub static DEFAULT_IAK_CERT: &str = "iak-cert.crt"; pub static DEFAULT_IDEVID_CERT: &str = "idevid-cert.crt"; @@ -139,6 +143,10 @@ pub struct AgentConfig { pub port: u32, pub registrar_ip: String, pub registrar_port: u32, + pub registrar_tls_enabled: bool, + pub registrar_tls_ca_cert: String, + pub registrar_tls_client_cert: String, + pub registrar_tls_client_key: String, pub run_as: String, pub tpm_encryption_alg: String, pub tpm_hash_alg: String, @@ -316,6 +324,12 @@ impl Default for AgentConfig { port: DEFAULT_PORT, registrar_ip: DEFAULT_REGISTRAR_IP.to_string(), registrar_port: DEFAULT_REGISTRAR_PORT, + registrar_tls_enabled: DEFAULT_REGISTRAR_TLS_ENABLED, + registrar_tls_ca_cert: DEFAULT_REGISTRAR_TLS_CA_CERT.to_string(), + registrar_tls_client_cert: DEFAULT_REGISTRAR_TLS_CLIENT_CERT + .to_string(), + registrar_tls_client_key: DEFAULT_REGISTRAR_TLS_CLIENT_KEY + .to_string(), revocation_actions: DEFAULT_REVOCATION_ACTIONS.to_string(), revocation_actions_dir: DEFAULT_REVOCATION_ACTIONS_DIR .to_string(), diff --git a/keylime/src/config/push_model.rs b/keylime/src/config/push_model.rs index 83a4b0457..25466de37 100644 --- a/keylime/src/config/push_model.rs +++ b/keylime/src/config/push_model.rs @@ -50,6 +50,10 @@ pub struct PushModelConfig { registrar_api_versions: Vec<&str>, registrar_ip: String, registrar_port: u32, + registrar_tls_enabled: bool, + registrar_tls_ca_cert: String, + registrar_tls_client_cert: String, + registrar_tls_client_key: String, server_cert: String, server_key: String, server_key_password: String, diff --git a/keylime/src/crypto.rs b/keylime/src/crypto.rs index 07324b6b4..2feeda7c2 100644 --- a/keylime/src/crypto.rs +++ b/keylime/src/crypto.rs @@ -1130,7 +1130,7 @@ pub fn decrypt_aead(key: &[u8], data: &[u8]) -> Result, CryptoError> { pub mod testing { use super::*; use openssl::encrypt::Encrypter; - use std::path::Path; + use std::path::{Path, PathBuf}; #[derive(Error, Debug)] pub enum CryptoTestError { @@ -1249,6 +1249,112 @@ pub mod testing { .map_err(CryptoError::IOWriteError)?; Ok(()) } + + /// Generates a full set of TLS certificates (CA, server, client) for testing purposes. + /// + /// # Arguments + /// * `temp_dir` - The temporary directory to write the certificate files into. + /// + /// # Returns + /// A tuple containing the paths to the generated files: + /// (ca_cert, server_cert, server_key, client_cert, client_key) + pub fn generate_tls_certs_for_test( + temp_dir: &Path, + ) -> (PathBuf, PathBuf, PathBuf, PathBuf, PathBuf) { + use std::fs::File; + use std::io::Write; + + // Define paths + let ca_path = temp_dir.join("ca.pem"); + let server_cert_path = temp_dir.join("server_cert.pem"); + let server_key_path = temp_dir.join("server_key.pem"); + let client_cert_path = temp_dir.join("client_cert.pem"); + let client_key_path = temp_dir.join("client_key.pem"); + + // Generate CA + let ca_key = rsa_generate(2048).expect("Failed to generate CA key"); + let ca_cert = x509::CertificateBuilder::new() + .private_key(&ca_key) + .common_name("Test CA") + .build() + .expect("Failed to build CA cert"); + + // Generate server certificate + let server_key = + rsa_generate(2048).expect("Failed to generate server key"); + let server_cert = x509::CertificateBuilder::new() + .private_key(&server_key) + .common_name("localhost") + .add_ips(vec!["127.0.0.1"]) + .build() + .expect("Failed to build server cert"); + + // Generate client certificate + let client_key = + rsa_generate(2048).expect("Failed to generate client key"); + let client_cert = x509::CertificateBuilder::new() + .private_key(&client_key) + .common_name("test-client") + .build() + .expect("Failed to build client cert"); + + // Write files + let mut ca_file = + File::create(&ca_path).expect("Failed to create CA file"); + ca_file + .write_all( + &ca_cert.to_pem().expect("Failed to convert CA to PEM"), + ) + .expect("Failed to write CA cert"); + + let mut server_cert_file = File::create(&server_cert_path) + .expect("Failed to create server cert file"); + server_cert_file + .write_all( + &server_cert + .to_pem() + .expect("Failed to convert server cert to PEM"), + ) + .expect("Failed to write server cert"); + + let mut server_key_file = File::create(&server_key_path) + .expect("Failed to create server key file"); + server_key_file + .write_all( + &server_key + .private_key_to_pem_pkcs8() + .expect("Failed to convert key to PEM"), + ) + .expect("Failed to write server key"); + + let mut client_cert_file = File::create(&client_cert_path) + .expect("Failed to create client cert file"); + client_cert_file + .write_all( + &client_cert + .to_pem() + .expect("Failed to convert client cert to PEM"), + ) + .expect("Failed to write client cert"); + + let mut client_key_file = File::create(&client_key_path) + .expect("Failed to create client key file"); + client_key_file + .write_all( + &client_key + .private_key_to_pem_pkcs8() + .expect("Failed to convert key to PEM"), + ) + .expect("Failed to write client key"); + + ( + ca_path, + server_cert_path, + server_key_path, + client_cert_path, + client_key_path, + ) + } } // Unit Testing diff --git a/keylime/src/https_client.rs b/keylime/src/https_client.rs index 1b3e4497f..c714a5001 100644 --- a/keylime/src/https_client.rs +++ b/keylime/src/https_client.rs @@ -64,3 +64,298 @@ pub fn get_https_client(args: &ClientArgs) -> Result { } builder.build().context("Failed to create HTTPS client") } + +#[cfg(feature = "testing")] +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn test_get_https_client_with_valid_certs() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: key_path.to_string_lossy().to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!( + result.is_ok(), + "Failed to create HTTPS client with valid certs" + ); + } + + #[test] + fn test_get_https_client_insecure_mode() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: key_path.to_string_lossy().to_string(), + insecure: Some(true), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!( + result.is_ok(), + "Failed to create HTTPS client in insecure mode" + ); + } + + #[test] + fn test_get_https_client_missing_ca_cert() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + + let args = ClientArgs { + ca_certificate: tmpdir + .path() + .join("nonexistent_ca.pem") + .to_string_lossy() + .to_string(), + certificate: tmpdir + .path() + .join("cert.pem") + .to_string_lossy() + .to_string(), + key: tmpdir.path().join("key.pem").to_string_lossy().to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!(result.is_err(), "Should fail with missing CA certificate"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Failed to open") + || err_msg.contains("nonexistent_ca.pem"), + "Error should mention missing CA file" + ); + } + + #[test] + fn test_get_https_client_missing_client_cert() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, _cert_path, _key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: tmpdir + .path() + .join("nonexistent_cert.pem") + .to_string_lossy() + .to_string(), + key: tmpdir.path().join("key.pem").to_string_lossy().to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!( + result.is_err(), + "Should fail with missing client certificate" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Failed to read client certificate") + || err_msg.contains("nonexistent_cert.pem"), + "Error should mention missing client cert" + ); + } + + #[test] + fn test_get_https_client_missing_client_key() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, cert_path, _key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: tmpdir + .path() + .join("nonexistent_key.pem") + .to_string_lossy() + .to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!(result.is_err(), "Should fail with missing client key"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Failed to read key") + || err_msg.contains("nonexistent_key.pem"), + "Error should mention missing key file" + ); + } + + #[test] + fn test_get_https_client_invalid_ca_cert() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (_ca_path, _server_cert, _server_key, cert_path, key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + // Create invalid CA cert file + let invalid_ca_path = tmpdir.path().join("invalid_ca.pem"); + let mut invalid_ca_file = + File::create(&invalid_ca_path).expect("Failed to create file"); + invalid_ca_file + .write_all(b"INVALID CERTIFICATE DATA") + .expect("Failed to write"); + + let args = ClientArgs { + ca_certificate: invalid_ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: key_path.to_string_lossy().to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!(result.is_err(), "Should fail with invalid CA certificate"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Failed to parse certificate"), + "Error should mention certificate parsing failure" + ); + } + + #[test] + fn test_get_https_client_invalid_client_identity() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, _cert_path, _key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + // Create invalid cert/key files + let invalid_cert_path = tmpdir.path().join("invalid_cert.pem"); + let invalid_key_path = tmpdir.path().join("invalid_key.pem"); + + let mut invalid_cert_file = + File::create(&invalid_cert_path).expect("Failed to create file"); + invalid_cert_file + .write_all(b"INVALID CERT") + .expect("Failed to write"); + + let mut invalid_key_file = + File::create(&invalid_key_path).expect("Failed to create file"); + invalid_key_file + .write_all(b"INVALID KEY") + .expect("Failed to write"); + + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: invalid_cert_path.to_string_lossy().to_string(), + key: invalid_key_path.to_string_lossy().to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!(result.is_err(), "Should fail with invalid client identity"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Failed to add client identity"), + "Error should mention identity creation failure" + ); + } + + #[test] + fn test_get_https_client_with_different_timeouts() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + // Test with various timeout values + for timeout in [0, 1000, 5000, 30000, 300000] { + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: key_path.to_string_lossy().to_string(), + insecure: Some(false), + timeout, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!( + result.is_ok(), + "Should create client with timeout {}ms", + timeout + ); + } + } + + #[test] + fn test_get_https_client_insecure_default() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let (ca_path, _server_cert, _server_key, cert_path, key_path) = + crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + // Test with insecure = None (should default to false) + let args = ClientArgs { + ca_certificate: ca_path.to_string_lossy().to_string(), + certificate: cert_path.to_string_lossy().to_string(), + key: key_path.to_string_lossy().to_string(), + insecure: None, + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!( + result.is_ok(), + "Should create client with insecure=None (defaults to secure)" + ); + } + + #[test] + fn test_get_https_client_empty_ca_cert_path() { + let args = ClientArgs { + ca_certificate: "".to_string(), + certificate: "cert.pem".to_string(), + key: "key.pem".to_string(), + insecure: Some(false), + timeout: 5000, + accept_invalid_hostnames: true, + }; + + let result = get_https_client(&args); + assert!(result.is_err(), "Should fail with empty CA cert path"); + } +} diff --git a/keylime/src/registrar_client.rs b/keylime/src/registrar_client.rs index d05dec9ce..3e53714b5 100644 --- a/keylime/src/registrar_client.rs +++ b/keylime/src/registrar_client.rs @@ -1,6 +1,8 @@ use crate::resilient_client::ResilientClient; use crate::{ - agent_identity::AgentIdentity, agent_registration::RetryConfig, + agent_identity::AgentIdentity, + agent_registration::RetryConfig, + https_client::{self, ClientArgs}, serialization::*, }; use log::*; @@ -40,6 +42,10 @@ pub enum RegistrarClientBuilderError { /// Middleware error #[error("Middleware error: {0}")] Middleware(#[from] reqwest_middleware::Error), + + /// HTTPS client creation error + #[error("HTTPS client creation error: {0}")] + HttpsClient(#[from] anyhow::Error), } #[derive(Debug, Default)] @@ -49,6 +55,11 @@ pub struct RegistrarClientBuilder { registrar_address: Option, registrar_port: Option, retry_config: Option, + ca_certificate: Option, + certificate: Option, + key: Option, + insecure: Option, + timeout: Option, } impl RegistrarClientBuilder { @@ -88,6 +99,56 @@ impl RegistrarClientBuilder { self } + /// Set the CA certificate file path for TLS communication + /// + /// # Arguments: + /// + /// * ca_certificate (String): Path to the CA certificate file + pub fn ca_certificate(mut self, ca_certificate: String) -> Self { + self.ca_certificate = Some(ca_certificate); + self + } + + /// Set the client certificate file path for TLS communication + /// + /// # Arguments: + /// + /// * certificate (String): Path to the client certificate file + pub fn certificate(mut self, certificate: String) -> Self { + self.certificate = Some(certificate); + self + } + + /// Set the client private key file path for TLS communication + /// + /// # Arguments: + /// + /// * key (String): Path to the client private key file + pub fn key(mut self, key: String) -> Self { + self.key = Some(key); + self + } + + /// Set the insecure flag to disable TLS certificate validation + /// + /// # Arguments: + /// + /// * insecure (bool): If true, disable certificate validation + pub fn insecure(mut self, insecure: bool) -> Self { + self.insecure = Some(insecure); + self + } + + /// Set the request timeout in milliseconds + /// + /// # Arguments: + /// + /// * timeout (u64): Request timeout in milliseconds + pub fn timeout(mut self, timeout: u64) -> Self { + self.timeout = Some(timeout); + self + } + /// Parse the received address fn parse_registrar_address(address: String) -> String { // Parse the registrar IP or hostname @@ -112,6 +173,8 @@ impl RegistrarClientBuilder { /// Get the registrar API version from the Registrar '/version' endpoint async fn get_registrar_api_version( &mut self, + resilient_client: &ResilientClient, + scheme: &str, ) -> Result { let Some(ref registrar_ip) = self.registrar_address else { return Err(RegistrarClientBuilderError::RegistrarIPNotSet); @@ -122,34 +185,15 @@ impl RegistrarClientBuilder { }; // Try to reach the registrar - let addr = format!("http://{registrar_ip}:{registrar_port}/version"); + let addr = + format!("{scheme}://{registrar_ip}:{registrar_port}/version"); info!("Requesting registrar API version to {addr}"); - let resp = if let Some(retry_config) = &self.retry_config { - debug!( - "Using ResilientClient for version check with {} retries.", - retry_config.max_retries - ); - let client = ResilientClient::new( - None, - Duration::from_millis(retry_config.initial_delay_ms), - retry_config.max_retries, - &[StatusCode::OK], - retry_config.max_delay_ms.map(Duration::from_millis), - ); - - client - .get_request(reqwest::Method::GET, &addr) - .send() - .await? - } else { - reqwest::Client::new() - .get(&addr) - .send() - .await - .map_err(RegistrarClientBuilderError::Reqwest)? - }; + let resp = resilient_client + .get_request(reqwest::Method::GET, &addr) + .send() + .await?; if !resp.status().is_success() { info!("Registrar at '{addr}' does not support the '/version' endpoint"); @@ -178,31 +222,85 @@ impl RegistrarClientBuilder { return Err(RegistrarClientBuilderError::RegistrarPortNotSet); }; - // Get the registrar API version. If it was caused by an error in the request, set the - // version as UNKNOWN_API_VERSION, otherwise abort the build process - let registrar_api_version = - match self.get_registrar_api_version().await { - Ok(version) => version, - Err(e) => match e { - RegistrarClientBuilderError::RegistrarNoVersion => { - UNKNOWN_API_VERSION.to_string() - } - _ => { - return Err(e); - } - }, + // Determine if TLS should be used + // TLS is used if all TLS parameters are provided and insecure is not true + let use_tls = self.ca_certificate.is_some() + && self.certificate.is_some() + && self.key.is_some() + && !self.insecure.unwrap_or(false); + + let scheme = if use_tls { "https" } else { "http" }; + + info!( + "Building Registrar client: scheme={}, registrar={}:{}, TLS={}", + scheme, registrar_ip, registrar_port, use_tls + ); + + if use_tls { + debug!( + "TLS configuration: ca_cert={:?}, client_cert={:?}, client_key={:?}, insecure={:?}", + self.ca_certificate, + self.certificate, + self.key, + self.insecure + ); + } + + // Create the client (HTTPS or plain HTTP) + let client = if use_tls { + let args = ClientArgs { + ca_certificate: self + .ca_certificate + .clone() + .unwrap_or_default(), + certificate: self.certificate.clone().unwrap_or_default(), + key: self.key.clone().unwrap_or_default(), + insecure: self.insecure, + timeout: self.timeout.unwrap_or(5000), + accept_invalid_hostnames: false, }; + https_client::get_https_client(&args)? + } else { + reqwest::Client::new() + }; - let resilient_client = - self.retry_config.as_ref().map(|retry_config| { - ResilientClient::new( - None, + // Create ResilientClient once, using retry config if provided + let (initial_delay, max_retries, max_delay) = + if let Some(ref retry_config) = self.retry_config { + ( Duration::from_millis(retry_config.initial_delay_ms), retry_config.max_retries, - &[StatusCode::OK], retry_config.max_delay_ms.map(Duration::from_millis), ) - }); + } else { + // No retry config: use 0 retries + (Duration::from_millis(100), 0, None) + }; + + let resilient_client = ResilientClient::new( + Some(client), + initial_delay, + max_retries, + &[StatusCode::OK], + max_delay, + ); + + // Get the registrar API version. If it was caused by an error in the request, set the + // version as UNKNOWN_API_VERSION, otherwise abort the build process + let registrar_api_version = match self + .get_registrar_api_version(&resilient_client, scheme) + .await + { + Ok(version) => version, + Err(e) => match e { + RegistrarClientBuilderError::RegistrarNoVersion => { + UNKNOWN_API_VERSION.to_string() + } + _ => { + return Err(e); + } + }, + }; Ok(RegistrarClient { supported_api_versions: self @@ -212,6 +310,7 @@ impl RegistrarClientBuilder { registrar_ip, registrar_port, resilient_client, + scheme: scheme.to_string(), }) } } @@ -262,13 +361,14 @@ pub enum RegistrarClientError { Middleware(#[from] reqwest_middleware::Error), } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Debug)] pub struct RegistrarClient { api_version: String, supported_api_versions: Option>, registrar_ip: String, registrar_port: u32, - resilient_client: Option, + resilient_client: ResilientClient, + scheme: String, } #[derive(Debug, Serialize, Deserialize)] @@ -359,9 +459,10 @@ impl RegistrarClient { let registrar_ip = &self.registrar_ip; let registrar_port = &self.registrar_port; let uuid = &ai.uuid; + let scheme = &self.scheme; let addr = format!( - "http://{registrar_ip}:{registrar_port}/v{api_version}/agents/{uuid}", + "{scheme}://{registrar_ip}:{registrar_port}/v{api_version}/agents/{uuid}", ); info!( @@ -369,26 +470,18 @@ impl RegistrarClient { &addr, &ai.uuid ); - let resp = match self.resilient_client { - Some(ref client) => client - .get_json_request_from_struct( - reqwest::Method::POST, - &addr, - &data, - None, - ) - .map_err(RegistrarClientError::Serde)? - .send() - .await - .map_err(RegistrarClientError::Middleware)?, - None => { - reqwest::Client::new() - .post(&addr) - .json(&data) - .send() - .await? - } - }; + let resp = self + .resilient_client + .get_json_request_from_struct( + reqwest::Method::POST, + &addr, + &data, + None, + ) + .map_err(RegistrarClientError::Serde)? + .send() + .await + .map_err(RegistrarClientError::Middleware)?; if !resp.status().is_success() { // Check if this is a 403 Forbidden - indicates security rejection @@ -523,9 +616,10 @@ impl RegistrarClient { let registrar_ip = &self.registrar_ip; let registrar_port = &self.registrar_port; let uuid = &ai.uuid; + let scheme = &self.scheme; let addr = format!( - "http://{registrar_ip}:{registrar_port}/v{api_version}/agents/{uuid}", + "{scheme}://{registrar_ip}:{registrar_port}/v{api_version}/agents/{uuid}", ); info!( @@ -533,8 +627,18 @@ impl RegistrarClient { &addr, &ai.uuid ); - let resp = - reqwest::Client::new().put(&addr).json(&data).send().await?; + let resp = self + .resilient_client + .get_json_request_from_struct( + reqwest::Method::PUT, + &addr, + &data, + None, + ) + .map_err(RegistrarClientError::Serde)? + .send() + .await + .map_err(RegistrarClientError::Middleware)?; if !resp.status().is_success() { return Err(RegistrarClientError::Activation { @@ -1346,4 +1450,579 @@ mod tests { assert!(result.is_err()); } } + + #[actix_rt::test] + async fn test_builder_tls_ca_certificate() { + let builder = RegistrarClientBuilder::new() + .ca_certificate("/path/to/ca.pem".to_string()); + + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + } + + #[actix_rt::test] + async fn test_builder_tls_client_certificate() { + let builder = RegistrarClientBuilder::new() + .certificate("/path/to/cert.pem".to_string()); + + assert_eq!( + builder.certificate, + Some("/path/to/cert.pem".to_string()) + ); + } + + #[actix_rt::test] + async fn test_builder_tls_client_key() { + let builder = + RegistrarClientBuilder::new().key("/path/to/key.pem".to_string()); + + assert_eq!(builder.key, Some("/path/to/key.pem".to_string())); + } + + #[actix_rt::test] + async fn test_builder_tls_insecure_true() { + let builder = RegistrarClientBuilder::new().insecure(true); + + assert_eq!(builder.insecure, Some(true)); + } + + #[actix_rt::test] + async fn test_builder_tls_insecure_false() { + let builder = RegistrarClientBuilder::new().insecure(false); + + assert_eq!(builder.insecure, Some(false)); + } + + #[actix_rt::test] + async fn test_builder_tls_timeout() { + let builder = RegistrarClientBuilder::new().timeout(10000); + + assert_eq!(builder.timeout, Some(10000)); + } + + #[actix_rt::test] + async fn test_builder_chaining_with_tls() { + let builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate("/path/to/ca.pem".to_string()) + .certificate("/path/to/cert.pem".to_string()) + .key("/path/to/key.pem".to_string()) + .insecure(false) + .timeout(5000); + + assert_eq!(builder.registrar_address, Some("127.0.0.1".to_string())); + assert_eq!(builder.registrar_port, Some(8890)); + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + assert_eq!( + builder.certificate, + Some("/path/to/cert.pem".to_string()) + ); + assert_eq!(builder.key, Some("/path/to/key.pem".to_string())); + assert_eq!(builder.insecure, Some(false)); + assert_eq!(builder.timeout, Some(5000)); + } + + #[actix_rt::test] + async fn test_builder_tls_default_values() { + let builder = RegistrarClientBuilder::new(); + + assert_eq!(builder.ca_certificate, None); + assert_eq!(builder.certificate, None); + assert_eq!(builder.key, None); + assert_eq!(builder.insecure, None); + assert_eq!(builder.timeout, None); + } + + #[actix_rt::test] + async fn test_builder_partial_tls_config_missing_ca() { + let builder = RegistrarClientBuilder::new() + .certificate("/path/to/cert.pem".to_string()) + .key("/path/to/key.pem".to_string()); + + // Should have cert and key but not CA + assert_eq!(builder.ca_certificate, None); + assert_eq!( + builder.certificate, + Some("/path/to/cert.pem".to_string()) + ); + assert_eq!(builder.key, Some("/path/to/key.pem".to_string())); + } + + #[actix_rt::test] + async fn test_builder_partial_tls_config_missing_cert() { + let builder = RegistrarClientBuilder::new() + .ca_certificate("/path/to/ca.pem".to_string()) + .key("/path/to/key.pem".to_string()); + + // Should have CA and key but not cert + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + assert_eq!(builder.certificate, None); + assert_eq!(builder.key, Some("/path/to/key.pem".to_string())); + } + + #[actix_rt::test] + async fn test_builder_partial_tls_config_missing_key() { + let builder = RegistrarClientBuilder::new() + .ca_certificate("/path/to/ca.pem".to_string()) + .certificate("/path/to/cert.pem".to_string()); + + // Should have CA and cert but not key + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + assert_eq!( + builder.certificate, + Some("/path/to/cert.pem".to_string()) + ); + assert_eq!(builder.key, None); + } + + #[actix_rt::test] + async fn test_builder_empty_string_tls_paths() { + let builder = RegistrarClientBuilder::new() + .ca_certificate("".to_string()) + .certificate("".to_string()) + .key("".to_string()); + + assert_eq!(builder.ca_certificate, Some("".to_string())); + assert_eq!(builder.certificate, Some("".to_string())); + assert_eq!(builder.key, Some("".to_string())); + } + + #[actix_rt::test] + async fn test_builder_timeout_various_values() { + // Test with zero timeout + let builder_zero = RegistrarClientBuilder::new().timeout(0); + assert_eq!(builder_zero.timeout, Some(0)); + + // Test with very large timeout + let builder_large = RegistrarClientBuilder::new().timeout(3600000); + assert_eq!(builder_large.timeout, Some(3600000)); + + // Test with default-ish timeout + let builder_default = RegistrarClientBuilder::new().timeout(5000); + assert_eq!(builder_default.timeout, Some(5000)); + } + + #[actix_rt::test] + async fn test_builder_insecure_with_tls_certs() { + // Test that insecure can be set alongside TLS certificates + let builder = RegistrarClientBuilder::new() + .ca_certificate("/path/to/ca.pem".to_string()) + .certificate("/path/to/cert.pem".to_string()) + .key("/path/to/key.pem".to_string()) + .insecure(true); + + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + assert_eq!( + builder.certificate, + Some("/path/to/cert.pem".to_string()) + ); + assert_eq!(builder.key, Some("/path/to/key.pem".to_string())); + assert_eq!(builder.insecure, Some(true)); + } + + #[actix_rt::test] + async fn test_builder_retry_config_with_tls() { + let retry = Some(RetryConfig { + max_retries: 3, + initial_delay_ms: 100, + max_delay_ms: Some(1000), + }); + + let builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .retry_config(retry.clone()) + .ca_certificate("/path/to/ca.pem".to_string()) + .certificate("/path/to/cert.pem".to_string()) + .key("/path/to/key.pem".to_string()); + + // Verify retry_config was set + assert!(builder.retry_config.is_some()); + let retry_cfg = builder.retry_config.as_ref().unwrap(); //#[allow_ci] + assert_eq!(retry_cfg.max_retries, 3); + assert_eq!(retry_cfg.initial_delay_ms, 100); + assert_eq!(retry_cfg.max_delay_ms, Some(1000)); + + // Verify TLS config was also set + assert_eq!( + builder.ca_certificate, + Some("/path/to/ca.pem".to_string()) + ); + } + + #[actix_rt::test] + async fn test_builder_with_real_tls_certificates() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let ( + ca_path, + _server_cert_path, + _server_key_path, + client_cert_path, + client_key_path, + ) = crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let ca_path_str = ca_path.to_string_lossy().to_string(); + let client_cert_path_str = + client_cert_path.to_string_lossy().to_string(); + let client_key_path_str = + client_key_path.to_string_lossy().to_string(); + + let builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate(ca_path_str.clone()) + .certificate(client_cert_path_str.clone()) + .key(client_key_path_str.clone()); + + // Verify all TLS paths were set correctly + assert_eq!(builder.ca_certificate, Some(ca_path_str.clone())); + assert_eq!(builder.certificate, Some(client_cert_path_str.clone())); + assert_eq!(builder.key, Some(client_key_path_str.clone())); + + // Verify files exist + assert!(ca_path.exists()); + assert!(client_cert_path.exists()); + assert!(client_key_path.exists()); + } + + #[actix_rt::test] + async fn test_builder_build_with_invalid_tls_cert_files() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + + // Try to build with non-existent certificate files + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate( + tmpdir + .path() + .join("nonexistent_ca.pem") + .to_string_lossy() + .to_string(), + ) + .certificate( + tmpdir + .path() + .join("nonexistent_cert.pem") + .to_string_lossy() + .to_string(), + ) + .key( + tmpdir + .path() + .join("nonexistent_key.pem") + .to_string_lossy() + .to_string(), + ); + + // Build should fail because certificate files don't exist + let result = builder.build().await; + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn test_tls_enabled_when_all_certs_provided() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let ( + ca_path, + _server_cert_path, + _server_key_path, + client_cert_path, + client_key_path, + ) = crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate(ca_path.to_string_lossy().to_string()) + .certificate(client_cert_path.to_string_lossy().to_string()) + .key(client_key_path.to_string_lossy().to_string()) + .insecure(false); + + // The build will fail because there's no server running, + // but we can verify that TLS configuration was processed + let result = builder.build().await; + // Should fail at version endpoint, not at TLS setup + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn test_tls_disabled_when_insecure_true() { + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let ( + ca_path, + _server_cert_path, + _server_key_path, + client_cert_path, + client_key_path, + ) = crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate(ca_path.to_string_lossy().to_string()) + .certificate(client_cert_path.to_string_lossy().to_string()) + .key(client_key_path.to_string_lossy().to_string()) + .insecure(true); // This should disable TLS + + // Build will fail due to no server, but won't try to load certs + let result = builder.build().await; + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn test_http_fallback_when_partial_tls_config() { + // When only some TLS params are provided, should fall back to HTTP + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(8890) + .ca_certificate("/path/to/ca.pem".to_string()) + // Missing certificate and key + .insecure(false); + + let result = builder.build().await; + // Should fail trying to connect via HTTP to get version + assert!(result.is_err()); + } + + // Mockoon-based integration tests for registrar HTTP and HTTPS + #[actix_rt::test] + async fn test_mockoon_registrar_http_registration() { + if std::env::var("MOCKOON_REGISTRAR").is_err() { + return; + } + + let mock_data = [0u8; 1]; + let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("mockoon-test-agent") + .add_ips(vec!["127.0.0.1"]) + .build() + .unwrap(); //#[allow_ci] + + let ai = AgentIdentityBuilder::new() + .ak_pub(&mock_data) + .ek_pub(&mock_data) + .enabled_api_versions(vec!["1.2"]) + .mtls_cert(cert) + .ip("127.0.0.1".to_string()) + .port(9001) + .uuid("test-uuid-mockoon-http") + .build() + .await + .expect("failed to build Agent Identity"); + + // Test HTTP registration with Mockoon on port 3001 + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(3001); + + let mut registrar_client = + builder.build().await.expect("Failed to build client"); + + let result = registrar_client.register_agent(&ai).await; + assert!(result.is_ok(), "HTTP registration failed: {result:?}"); + + // Verify we got a blob back + let blob = result.unwrap(); //#[allow_ci] + assert!( + !blob.is_empty(), + "Expected non-empty blob from registration" + ); + } + + #[actix_rt::test] + async fn test_mockoon_registrar_http_activation() { + if std::env::var("MOCKOON_REGISTRAR").is_err() { + return; + } + + let mock_data = [0u8; 1]; + let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("mockoon-test-agent") + .add_ips(vec!["127.0.0.1"]) + .build() + .unwrap(); //#[allow_ci] + + let ai = AgentIdentityBuilder::new() + .ak_pub(&mock_data) + .ek_pub(&mock_data) + .enabled_api_versions(vec!["1.2"]) + .mtls_cert(cert) + .ip("127.0.0.1".to_string()) + .port(9001) + .uuid("test-uuid-mockoon-http") + .build() + .await + .expect("failed to build Agent Identity"); + + // Test HTTP activation with Mockoon on port 3001 + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(3001); + + let mut registrar_client = + builder.build().await.expect("Failed to build client"); + + let result = + registrar_client.activate_agent(&ai, "test-auth-tag").await; + assert!(result.is_ok(), "HTTP activation failed: {result:?}"); + } + + #[actix_rt::test] + async fn test_mockoon_registrar_https_registration() { + if std::env::var("MOCKOON_REGISTRAR").is_err() { + return; + } + + let tmpdir = tempfile::tempdir().expect("Failed to create tempdir"); + let ( + ca_path, + _server_cert_path, + _server_key_path, + client_cert_path, + client_key_path, + ) = crate::crypto::testing::generate_tls_certs_for_test( + tmpdir.path(), + ); + + let mock_data = [0u8; 1]; + let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("mockoon-test-agent-tls") + .add_ips(vec!["127.0.0.1"]) + .build() + .unwrap(); //#[allow_ci] + + let _ = AgentIdentityBuilder::new() + .ak_pub(&mock_data) + .ek_pub(&mock_data) + .enabled_api_versions(vec!["1.2"]) + .mtls_cert(cert) + .ip("127.0.0.1".to_string()) + .port(9001) + .uuid("test-uuid-mockoon-https") + .build() + .await + .expect("failed to build Agent Identity"); + + // Test HTTPS registration with Mockoon on port 3001 + // Note: Mockoon HTTPS requires TLS to be enabled in the registrar.json config + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(3001) + .ca_certificate(ca_path.to_string_lossy().to_string()) + .certificate(client_cert_path.to_string_lossy().to_string()) + .key(client_key_path.to_string_lossy().to_string()) + .insecure(false); + + // Build will attempt to connect to get version + // This test demonstrates TLS configuration flow + let result = builder.build().await; + + // With Mockoon not configured for TLS, this will fail at connection + // In a real TLS-enabled Mockoon setup, this would succeed + // This test verifies the TLS code path is executed + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn test_mockoon_registrar_version_endpoint() { + if std::env::var("MOCKOON_REGISTRAR").is_err() { + return; + } + + // Test that we can retrieve the API version from Mockoon + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(3001); + + let result = builder.build().await; + assert!( + result.is_ok(), + "Failed to build client and get version: {result:?}" + ); + + let client = result.unwrap(); //#[allow_ci] + // Verify the API version was retrieved + assert_eq!(client.api_version, "1.2"); + assert!(client.supported_api_versions.is_some()); + + let supported = client.supported_api_versions.unwrap(); //#[allow_ci] + assert!(supported.contains(&"1.2".to_string())); + } + + #[actix_rt::test] + async fn test_mockoon_registrar_with_retry_config() { + if std::env::var("MOCKOON_REGISTRAR").is_err() { + return; + } + + let retry_config = Some(RetryConfig { + max_retries: 3, + initial_delay_ms: 100, + max_delay_ms: Some(1000), + }); + + let mock_data = [0u8; 1]; + let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("mockoon-test-agent-retry") + .add_ips(vec!["127.0.0.1"]) + .build() + .unwrap(); //#[allow_ci] + + let ai = AgentIdentityBuilder::new() + .ak_pub(&mock_data) + .ek_pub(&mock_data) + .enabled_api_versions(vec!["1.2"]) + .mtls_cert(cert) + .ip("127.0.0.1".to_string()) + .port(9001) + .uuid("test-uuid-mockoon-retry") + .build() + .await + .expect("failed to build Agent Identity"); + + // Test registration with retry configuration + let mut builder = RegistrarClientBuilder::new() + .registrar_address("127.0.0.1".to_string()) + .registrar_port(3001) + .retry_config(retry_config); + + let mut registrar_client = + builder.build().await.expect("Failed to build client"); + + let result = registrar_client.register_agent(&ai).await; + assert!( + result.is_ok(), + "Registration with retry config failed: {result:?}" + ); + } } diff --git a/tests/mockoon_registrar_tests.sh b/tests/mockoon_registrar_tests.sh new file mode 100755 index 000000000..7e18cc511 --- /dev/null +++ b/tests/mockoon_registrar_tests.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 Keylime Authors +# +# Script to run Mockoon-based registrar integration tests +# This script starts a Mockoon server on port 3001 with the registrar configuration +# and runs the integration tests that require a mock registrar server + +source ./tests/common_tests.sh || source ./common_tests.sh + +echo "-------- Testing Registrar with Mockoon" +start_swtpm + +# Check that tpm2-openssl provider is available +if openssl list -provider tpm2 -providers > /dev/null; then + # If any IAK/IDevID related certificate is missing, re-generate them + if [[ ( ! -f "${IAK_IDEVID_CERTS}/iak.cert.pem" ) || + ( ! -f "${IAK_IDEVID_CERTS}/iak.cert.der" ) || + ( ! -f "${IAK_IDEVID_CERTS}/idevid.cert.pem" ) || + ( ! -f "${IAK_IDEVID_CERTS}/idevid.cert.der" ) || + ( ! -f "${IAK_IDEVID_CERTS}/ca-cert-chain.pem" ) ]] + then + # Remove any leftover from old certificates + rm -rf "${IAK_IDEVID_CERTS}" + mkdir -p "${IAK_IDEVID_CERTS}" + echo "-------- Create IAK/IDevID certificates" + "${GIT_ROOT}/tests/generate-iak-idevid-certs.sh" -o "${IAK_IDEVID_CERTS}" + fi +fi + +mkdir -p /var/lib/keylime + +# Function to log detailed information about port 3001 usage +log_port_3001_info() { + echo "======== DETAILED PORT 3001 ANALYSIS ========" + echo "Timestamp: $(date)" + echo "Environment: CI=${CI:-false}, GITHUB_ACTIONS=${GITHUB_ACTIONS:-false}" + echo "" + + echo "--- lsof output for port 3001 ---" + if command -v lsof >/dev/null 2>&1; then + lsof -i :3001 2>/dev/null || echo "No lsof results for port 3001" + echo "" + echo "--- lsof with process details ---" + lsof -i :3001 -P -n 2>/dev/null || echo "No detailed lsof results" + else + echo "lsof command not available" + fi + echo "" + + echo "--- netstat output ---" + if command -v netstat >/dev/null 2>&1; then + netstat -tulpn 2>/dev/null | grep ':3001' || echo "No netstat results for port 3001" + else + echo "netstat command not available" + fi + echo "" + + echo "--- ss (socket statistics) output ---" + if command -v ss >/dev/null 2>&1; then + ss -tulpn 2>/dev/null | grep ':3001' || echo "No ss results for port 3001" + else + echo "ss command not available" + fi + echo "" + + echo "--- Process tree and details ---" + if lsof -i :3001 >/dev/null 2>&1; then + echo "Processes using port 3001:" + for pid in $(lsof -ti :3001 2>/dev/null); do + echo " PID: $pid" + if [ -d "/proc/$pid" ]; then + echo " Command: $(cat "/proc/$pid/comm" 2>/dev/null || echo 'N/A')" + echo " Cmdline: $(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo 'N/A')" + echo " User: $(stat -c '%U' "/proc/$pid" 2>/dev/null || echo 'N/A')" + echo " Parent PID: $(awk '{print $4}' < "/proc/$pid/stat" 2>/dev/null || echo 'N/A')" + echo " Start time: $(stat -c '%Y' "/proc/$pid" 2>/dev/null | xargs -I {} date -d @{} 2>/dev/null || echo 'N/A')" + echo " Working directory: $(readlink "/proc/$pid/cwd" 2>/dev/null || echo 'N/A')" + echo " Environment (filtered):" + grep -E "(MOCKOON|NODE|NPM|PATH|USER|HOME)" "/proc/$pid/environ" 2>/dev/null | tr '\0' '\n' | sed 's/^/ /' || echo " N/A" + else + echo " Process details not available (proc not mounted or process gone)" + fi + echo "" + done + fi + + echo "--- Process list (mockoon related) ---" + pgrep -f -l mockoon 2>/dev/null || echo "No mockoon processes found" + echo "" + + echo "--- Process list (node related on port 3001) ---" + { pgrep -f -l node 2>/dev/null; pgrep -f -l npm 2>/dev/null; } | sort -u || echo "No node/npm processes found" + echo "" + + echo "--- Docker containers (if running in container) ---" + if command -v docker >/dev/null 2>&1 && docker ps >/dev/null 2>&1; then + docker ps | grep -E "(mockoon|3001)" || echo "No docker containers with mockoon or port 3001" + else + echo "Docker not available or not accessible" + fi + echo "" + + echo "--- Systemd services (if available) ---" + if command -v systemctl >/dev/null 2>&1; then + systemctl list-units --type=service | grep -i mockoon || echo "No systemd mockoon services" + else + echo "systemctl not available" + fi + echo "" + + echo "--- HTTP response test ---" + if curl -s --connect-timeout 2 http://localhost:3001 2>/dev/null; then + echo "HTTP response received from port 3001" + echo "Response headers:" + curl -sI --connect-timeout 2 http://localhost:3001 2>/dev/null || echo "Failed to get headers" + else + echo "No HTTP response from port 3001" + fi + echo "" + + echo "======== END PORT 3001 ANALYSIS ========" +} + +# Check if Mockoon is already running on port 3001 (e.g., in CI) +# Try multiple methods to detect if port 3001 is in use +PORT_IN_USE=false +if lsof -i :3001 > /dev/null 2>&1; then + PORT_IN_USE=true +elif netstat -ln 2>/dev/null | grep -q ':3001 '; then + PORT_IN_USE=true +elif ss -ln 2>/dev/null | grep -q ':3001 '; then + PORT_IN_USE=true +elif curl -s --connect-timeout 2 http://localhost:3001 > /dev/null 2>&1; then + PORT_IN_USE=true +fi + +if $PORT_IN_USE; then + echo "-------- Mockoon already running on port 3001 (likely in CI)" + log_port_3001_info + MOCKOON_PID="" +else + # Check if Mockoon is installed for local runs + if ! command -v mockoon-cli &> /dev/null; then + echo "Error: mockoon-cli is not installed" + echo "Install it with: npm install -g @mockoon/cli" + exit 1 + fi + + # Start Mockoon server with registrar configuration on port 3001 + echo "-------- Starting Mockoon server on port 3001 with registrar configuration" + REGISTRAR_JSON="${GIT_ROOT}/keylime-push-model-agent/test-data/registrar.json" + + if [ ! -f "$REGISTRAR_JSON" ]; then + echo "Error: Registrar configuration file not found at $REGISTRAR_JSON" + exit 1 + fi + + # Start Mockoon in the background + mockoon-cli start --data "$REGISTRAR_JSON" --port 3001 & + MOCKOON_PID=$! + + # Wait for Mockoon to start + echo "Waiting for Mockoon server on port 3001 to start..." + if ! command -v curl >/dev/null 2>&1; then + echo "curl not found, falling back to sleep" + sleep 3 + else + for _ in $(seq 1 15); do + if curl -s --connect-timeout 1 http://localhost:3001 > /dev/null; then + echo "Mockoon server is up!" + MOCKOON_READY=true + break + fi + sleep 1 + done + if [ -z "$MOCKOON_READY" ]; then + echo "Error: Timed out waiting for Mockoon server to start." + kill "$MOCKOON_PID" 2>/dev/null || true + exit 1 + fi + fi + + # Check if Mockoon is running + if ! kill -0 $MOCKOON_PID 2>/dev/null; then + echo "Error: Mockoon failed to start" + exit 1 + fi + + echo "Mockoon server started with PID $MOCKOON_PID" +fi + +# Run tests with MOCKOON_REGISTRAR environment variable set +echo "-------- Running registrar tests with Mockoon" +RUST_BACKTRACE=1 RUST_LOG=info \ +KEYLIME_CONFIG=$PWD/keylime-agent.conf \ +MOCKOON_REGISTRAR=1 cargo test --features testing test_mockoon_registrar -- --nocapture + +# Capture test exit code +TEST_EXIT_CODE=$? + +# Stop Mockoon server only if we started it locally +if [ -n "$MOCKOON_PID" ]; then + echo "-------- Stopping Mockoon server" + kill $MOCKOON_PID 2>/dev/null || true + wait $MOCKOON_PID 2>/dev/null || true + + # Check if port 3001 is still in use and force cleanup if needed + PORT_STILL_IN_USE=false + if lsof -i :3001 > /dev/null 2>&1; then + PORT_STILL_IN_USE=true + elif netstat -ln 2>/dev/null | grep -q ':3001 '; then + PORT_STILL_IN_USE=true + elif ss -ln 2>/dev/null | grep -q ':3001 '; then + PORT_STILL_IN_USE=true + fi + + if $PORT_STILL_IN_USE; then + echo "Warning: Port 3001 still in use after stopping Mockoon, forcing cleanup" + echo "---- Port 3001 status before cleanup ----" + log_port_3001_info + + echo "---- Performing cleanup ----" + lsof -ti :3001 | xargs kill -9 2>/dev/null || true + # Additional cleanup methods + pkill -f "mockoon-cli.*3001" 2>/dev/null || true + pkill -f "node.*mockoon.*3001" 2>/dev/null || true + + # Wait a moment and check again + sleep 2 + echo "---- Port 3001 status after cleanup ----" + if lsof -i :3001 >/dev/null 2>&1 || netstat -ln 2>/dev/null | grep -q ':3001 ' || ss -ln 2>/dev/null | grep -q ':3001 '; then + echo "WARNING: Port 3001 still in use after cleanup attempts" + log_port_3001_info + else + echo "Port 3001 successfully cleaned up" + fi + fi +else + echo "-------- Mockoon was already running (CI), not stopping it" +fi + +if [ $TEST_EXIT_CODE -eq 0 ]; then + echo "-------- Registrar Mockoon tests PASSED" +else + echo "-------- Registrar Mockoon tests FAILED with exit code $TEST_EXIT_CODE" +fi + +exit $TEST_EXIT_CODE