From c34d72107a5815b25422a814b10aa7d13471578d Mon Sep 17 00:00:00 2001 From: djach7 Date: Wed, 1 Feb 2023 17:00:06 -0500 Subject: [PATCH 1/2] feat: user creation with encrypted password Allows users to create a password for login when onboarding a device. These passwords are optional and should be provided within the 'serviceinfo_api_server.yml' config file. A user's password will be encrypted via SHA-256 if it is not already encrypted when provided to the config. Signed-off-by: djach7 --- Cargo.lock | 20 +++ client-linuxapp/Cargo.toml | 2 + client-linuxapp/src/serviceinfo.rs | 128 +++++++++++++++--- .../templates/serviceinfo-api-server.yml.j2 | 1 + owner-onboarding-server/src/handlers.rs | 15 +- serviceinfo-api-server/src/main.rs | 2 + .../configuration/serviceinfo_api_server.rs | 3 +- util/src/servers/mod.rs | 3 +- 8 files changed, 152 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3a0008e2..27ea5ba9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,6 +203,12 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -798,10 +804,12 @@ dependencies = [ "libcryptsetup-rs", "log", "nix", + "openssl", "passwd", "rand", "secrecy", "serde_bytes", + "sha-crypt", "sys-info", "thiserror", "tokio", @@ -2586,6 +2594,18 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "sha-crypt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e79009728d8311d42d754f2f319a975f9e38f156fd5e422d2451486c78b286" +dependencies = [ + "base64ct", + "rand", + "sha2 0.10.6", + "subtle", +] + [[package]] name = "sha1" version = "0.10.5" diff --git a/client-linuxapp/Cargo.toml b/client-linuxapp/Cargo.toml index cbd132b8c..bba7e4145 100644 --- a/client-linuxapp/Cargo.toml +++ b/client-linuxapp/Cargo.toml @@ -20,6 +20,8 @@ thiserror = "1" libcryptsetup-rs = { version = "0.8.0", features = ["mutex"] } secrecy = "0.8" devicemapper = "0.33" +openssl = "0.10.45" +sha-crypt = "0.5.0" fdo-data-formats = { path = "../data-formats", version = "0.4.10" } fdo-http-wrapper = { path = "../http-wrapper", version = "0.4.10", features = ["client"] } diff --git a/client-linuxapp/src/serviceinfo.rs b/client-linuxapp/src/serviceinfo.rs index 89ce7e6b0..d8c89ae9d 100644 --- a/client-linuxapp/src/serviceinfo.rs +++ b/client-linuxapp/src/serviceinfo.rs @@ -4,6 +4,7 @@ use std::{ fs::{File, Permissions}, io::Write, path::Path, + str, }; use std::{env, fs}; use std::{os::unix::fs::PermissionsExt, path::PathBuf}; @@ -20,6 +21,8 @@ use fdo_data_formats::{ }; use fdo_http_wrapper::client::{RequestResult, ServiceClient}; +use sha_crypt::{sha256_check, sha256_simple, Sha256Params}; + const MAX_SERVICE_INFO_LOOPS: u32 = 1000; fn find_available_modules() -> Result> { @@ -54,22 +57,82 @@ fn set_perm_mode(path: &Path, mode: u32) -> Result<()> { } fn create_user(user: &str) -> Result<()> { + // Checks if user already present let user_info = passwd::Passwd::from_name(user); if user_info.is_some() { - log::info!("User: {} already present", user); + log::info!("User: {user} already present"); return Ok(()); } - log::info!("Creating user: {}", user); - Command::new("useradd") + // Creates new user if user not present + log::info!("Creating user: {user}"); + let status = Command::new("useradd") .arg("-m") .arg(user) .spawn() .context("Error spawning new user command")? .wait() .context("Error creating new user")?; - Ok(()) + + if status.success() { + log::info!("User {user} created successfully"); + Ok(()) + } else { + bail!(format!("User creation failed. Exit Status: {:#?}", status.code())); + } +} + +// Returns true if password is encrypted +// is_password_encrypted functionality is taken from osbuild-composer's crypt.go: +// https://github.com/osbuild/osbuild-composer/blob/main/internal/crypt/crypt.go +fn is_password_encrypted(s: &str) -> bool { + let prefixes = ["$2b$", "$6$", "$5$"]; + + for prefix in prefixes { + if s.starts_with(prefix) { + return true; + } + } + + false } +fn create_user_with_password(user: &str, password: &str) -> Result<()> { + // Checks if user already present + let user_info = passwd::Passwd::from_name(user); + if user_info.is_some() { + log::info!("User {user} is already present"); + return Ok(()); + } + + let mut str_encrypted_pw = password.to_string(); + log::info!("Checking for password encryption"); + if !is_password_encrypted(password) { + log::info!("Encrypting password"); + let default_params: Sha256Params = Default::default(); + str_encrypted_pw = sha256_simple(password, &default_params).expect("Hashing failed"); + assert!(sha256_check(password, &str_encrypted_pw).is_ok()); + } + + // Creates new user if user not present + log::info!("Creating user {user} with password"); + let status = Command::new("useradd") + .arg("-p") + .arg(str_encrypted_pw) + .arg(user) + .spawn() + .context("Error spawning new user command")? + .wait() + .context("Error creating new user")?; + + if status.success() { + log::info!("User {user} created successfully with password"); + Ok(()) + } else { + bail!(format!("User creation failed. Exit Status: {:#?}", status.code())); + } +} + + fn install_ssh_key(user: &str, key: &str) -> Result<()> { let user_info = passwd::Passwd::from_name(user); if user_info.is_none() { @@ -373,7 +436,8 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - let mut active_modules: HashSet = HashSet::new(); let mut sshkey_user: Option = None; - let mut sshkey_key: Option = None; + let mut sshkey_password: Option = None; + let mut sshkey_keys: Option = None; let mut rhsm_organization_id: Option = None; let mut rhsm_activation_key: Option = None; @@ -405,11 +469,24 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - bail!("Non-activated module {} got request", module); } if module == FedoraIotServiceInfoModule::SSHKey.into() { - let value = value.as_str().context("Error parsing sshkey value")?; if key == "username" { + let value = value + .as_str() + .context("Error parsing username value")?; sshkey_user = Some(value.to_string()); - } else if key == "key" { - sshkey_key = Some(value.to_string()); + log::info!("Username is: {value}"); + } else if key == "password" { + let value = value + .as_str() + .context("Error parsing password value")?; + sshkey_password = Some(value.to_string()); + log::info!("Password is present"); + } else if key == "sshkeys" { + let value = value + .as_str() + .context("Error parsing sshkey value")?; + sshkey_keys = Some(value.to_string()); + log::info!("Keys are present"); } } else if module == FedoraIotServiceInfoModule::Reboot.into() { if key == "reboot" { @@ -617,18 +694,33 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - } } - // Do SSH + // Perform SSH or password setup if active_modules.contains(&FedoraIotServiceInfoModule::SSHKey.into()) { - log::debug!("SSHkey module was active, installing SSH key"); - if sshkey_user.is_none() || sshkey_key.is_none() { - bail!("SSHkey module missing username or key"); + if sshkey_user.is_none() { + bail!("SSHkey module missing username"); + } else if sshkey_keys.is_none() && sshkey_password.is_none() { + bail!("SSHkey module missing password and key"); + } + if sshkey_password.is_some() { + log::info!("SSHkey module was active, creating user with password"); + create_user_with_password(sshkey_user.as_ref().unwrap(), sshkey_password.as_ref().unwrap()).context(format!( + "Error creating new user with password: {}", + sshkey_user.as_ref().unwrap() + ))?; + } + if sshkey_keys.is_some() { + log::info!("SSHkey module was active, installing SSH keys"); + create_user(sshkey_user.as_ref().unwrap()).context(format!( + "Error creating new user: {}", + sshkey_user.as_ref().unwrap() + ))?; + let sshkey_keys_v: Vec = sshkey_keys.unwrap().split(" ").map(|s| s.to_string()).collect(); + for key in sshkey_keys_v { + let key_s: String = key; + install_ssh_key(sshkey_user.as_ref().unwrap(), key_s.as_str()).context("Error installing SSH key")?; + log::info!("Installed sshkey: {key_s}"); + } } - create_user(sshkey_user.as_ref().unwrap()).context(format!( - "Error creating new user: {}", - sshkey_user.as_ref().unwrap() - ))?; - install_ssh_key(sshkey_user.as_ref().unwrap(), sshkey_key.as_ref().unwrap()) - .context("Error installing SSH key")?; } // Perform RHSM diff --git a/integration-tests/templates/serviceinfo-api-server.yml.j2 b/integration-tests/templates/serviceinfo-api-server.yml.j2 index 95b58ef9b..e12fe146a 100644 --- a/integration-tests/templates/serviceinfo-api-server.yml.j2 +++ b/integration-tests/templates/serviceinfo-api-server.yml.j2 @@ -8,6 +8,7 @@ admin_auth_token: TestAdminToken service_info: initial_user: username: {{ user }} + password: test sshkeys: - {{ sshkey }} files: diff --git a/owner-onboarding-server/src/handlers.rs b/owner-onboarding-server/src/handlers.rs index a10a69d96..d065c8c1a 100644 --- a/owner-onboarding-server/src/handlers.rs +++ b/owner-onboarding-server/src/handlers.rs @@ -538,8 +538,19 @@ async fn perform_service_info( "username", &initial_user.username, )?; - for key in initial_user.ssh_keys.iter() { - out_si.add(FedoraIotServiceInfoModule::SSHKey, "key", &key)?; + if initial_user.password.is_some() { + out_si.add( + FedoraIotServiceInfoModule::SSHKey, + "password", + &initial_user.password, + )?; + } + if initial_user.ssh_keys.is_some() { + out_si.add( + FedoraIotServiceInfoModule::SSHKey, + "sshkeys", + &(initial_user.ssh_keys.unwrap().join(" ")), + )?; } } diff --git a/serviceinfo-api-server/src/main.rs b/serviceinfo-api-server/src/main.rs index 38bb9e939..ec0dc6f64 100644 --- a/serviceinfo-api-server/src/main.rs +++ b/serviceinfo-api-server/src/main.rs @@ -234,6 +234,7 @@ async fn serviceinfo_handler( if let Some(initial_user) = &per_device_settings.initial_user { reply.reply.initial_user = Some(ServiceInfoApiReplyInitialUser { username: initial_user.username.clone(), + password: initial_user.password.clone(), ssh_keys: initial_user.sshkeys.clone(), }); } @@ -246,6 +247,7 @@ async fn serviceinfo_handler( log::debug!("serviceinfo setting from base file applied"); reply.reply.initial_user = Some(ServiceInfoApiReplyInitialUser { username: initial_user.username.clone(), + password: initial_user.password.clone(), ssh_keys: initial_user.sshkeys.clone(), }); } diff --git a/util/src/servers/configuration/serviceinfo_api_server.rs b/util/src/servers/configuration/serviceinfo_api_server.rs index 1b3e6c6cd..4ec559ec0 100644 --- a/util/src/servers/configuration/serviceinfo_api_server.rs +++ b/util/src/servers/configuration/serviceinfo_api_server.rs @@ -77,5 +77,6 @@ pub struct ServiceInfoCommand { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ServiceInfoInitialUser { pub username: String, - pub sshkeys: Vec, + pub password: Option, + pub sshkeys: Option>, } diff --git a/util/src/servers/mod.rs b/util/src/servers/mod.rs index de566eabb..836e47494 100644 --- a/util/src/servers/mod.rs +++ b/util/src/servers/mod.rs @@ -153,7 +153,8 @@ pub fn yaml_to_cbor(val: &Value) -> Result { #[derive(Debug, Serialize, Deserialize)] pub struct ServiceInfoApiReplyInitialUser { pub username: String, - pub ssh_keys: Vec, + pub password: Option, + pub ssh_keys: Option>, } #[derive(Debug, Serialize, Deserialize)] From d3d912010d7d8d41447fba10f451471e66a80c38 Mon Sep 17 00:00:00 2001 From: djach7 Date: Wed, 3 May 2023 10:43:08 -0400 Subject: [PATCH 2/2] chore(integration-tests): add testing for user creation with password Signed-off-by: djach7 --- Cargo.lock | 31 ++ client-linuxapp/Cargo.toml | 1 + client-linuxapp/src/serviceinfo.rs | 90 ++++- integration-tests/Cargo.toml | 4 +- .../templates/serviceinfo-api-server.yml.j2 | 2 +- integration-tests/tests/common/mod.rs | 2 + integration-tests/tests/e2e.rs | 14 +- integration-tests/tests/service_info.rs | 331 ++++++++++++++++++ owner-onboarding-server/src/handlers.rs | 4 +- 9 files changed, 456 insertions(+), 23 deletions(-) create mode 100644 integration-tests/tests/service_info.rs diff --git a/Cargo.lock b/Cargo.lock index 27ea5ba9c..ed4d309c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,6 +803,7 @@ dependencies = [ "fdo-util", "libcryptsetup-rs", "log", + "logtest", "nix", "openssl", "passwd", @@ -1497,6 +1498,8 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "sha-crypt", + "shadow", "tempfile", "tera", "tokio", @@ -1648,6 +1651,19 @@ name = "log" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +dependencies = [ + "value-bag", +] + +[[package]] +name = "logtest" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e43a8657c1d64516dcc9db8ca03826a4aceaf89d5ce1b37b59f6ff0e43026" +dependencies = [ + "lazy_static", + "log", +] [[package]] name = "maplit" @@ -2641,6 +2657,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "shadow" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaa1dbf13cc052b53af59e70dd467e8e94d6abf8d0eac1f1d15d3e165973b86" +dependencies = [ + "libc", +] + [[package]] name = "shlex" version = "1.1.0" @@ -3194,6 +3219,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "value-bag" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/client-linuxapp/Cargo.toml b/client-linuxapp/Cargo.toml index bba7e4145..96a82a12d 100644 --- a/client-linuxapp/Cargo.toml +++ b/client-linuxapp/Cargo.toml @@ -22,6 +22,7 @@ secrecy = "0.8" devicemapper = "0.33" openssl = "0.10.45" sha-crypt = "0.5.0" +logtest = "2.0.0" fdo-data-formats = { path = "../data-formats", version = "0.4.10" } fdo-http-wrapper = { path = "../http-wrapper", version = "0.4.10", features = ["client"] } diff --git a/client-linuxapp/src/serviceinfo.rs b/client-linuxapp/src/serviceinfo.rs index d8c89ae9d..7260b2141 100644 --- a/client-linuxapp/src/serviceinfo.rs +++ b/client-linuxapp/src/serviceinfo.rs @@ -77,7 +77,10 @@ fn create_user(user: &str) -> Result<()> { log::info!("User {user} created successfully"); Ok(()) } else { - bail!(format!("User creation failed. Exit Status: {:#?}", status.code())); + bail!(format!( + "User creation failed. Exit Status: {:#?}", + status.code() + )); } } @@ -112,7 +115,6 @@ fn create_user_with_password(user: &str, password: &str) -> Result<()> { str_encrypted_pw = sha256_simple(password, &default_params).expect("Hashing failed"); assert!(sha256_check(password, &str_encrypted_pw).is_ok()); } - // Creates new user if user not present log::info!("Creating user {user} with password"); let status = Command::new("useradd") @@ -128,11 +130,13 @@ fn create_user_with_password(user: &str, password: &str) -> Result<()> { log::info!("User {user} created successfully with password"); Ok(()) } else { - bail!(format!("User creation failed. Exit Status: {:#?}", status.code())); + bail!(format!( + "User creation failed. Exit Status: {:#?}", + status.code() + )); } } - fn install_ssh_key(user: &str, key: &str) -> Result<()> { let user_info = passwd::Passwd::from_name(user); if user_info.is_none() { @@ -470,21 +474,15 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - } if module == FedoraIotServiceInfoModule::SSHKey.into() { if key == "username" { - let value = value - .as_str() - .context("Error parsing username value")?; + let value = value.as_str().context("Error parsing username value")?; sshkey_user = Some(value.to_string()); log::info!("Username is: {value}"); } else if key == "password" { - let value = value - .as_str() - .context("Error parsing password value")?; + let value = value.as_str().context("Error parsing password value")?; sshkey_password = Some(value.to_string()); log::info!("Password is present"); } else if key == "sshkeys" { - let value = value - .as_str() - .context("Error parsing sshkey value")?; + let value = value.as_str().context("Error parsing sshkey value")?; sshkey_keys = Some(value.to_string()); log::info!("Keys are present"); } @@ -703,10 +701,14 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - } if sshkey_password.is_some() { log::info!("SSHkey module was active, creating user with password"); - create_user_with_password(sshkey_user.as_ref().unwrap(), sshkey_password.as_ref().unwrap()).context(format!( + create_user_with_password( + sshkey_user.as_ref().unwrap(), + sshkey_password.as_ref().unwrap(), + ) + .context(format!( "Error creating new user with password: {}", - sshkey_user.as_ref().unwrap() - ))?; + sshkey_user.as_ref().unwrap() + ))?; } if sshkey_keys.is_some() { log::info!("SSHkey module was active, installing SSH keys"); @@ -714,10 +716,15 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - "Error creating new user: {}", sshkey_user.as_ref().unwrap() ))?; - let sshkey_keys_v: Vec = sshkey_keys.unwrap().split(" ").map(|s| s.to_string()).collect(); + let sshkey_keys_v: Vec = sshkey_keys + .unwrap() + .split(' ') + .map(|s| s.to_string()) + .collect(); for key in sshkey_keys_v { let key_s: String = key; - install_ssh_key(sshkey_user.as_ref().unwrap(), key_s.as_str()).context("Error installing SSH key")?; + install_ssh_key(sshkey_user.as_ref().unwrap(), key_s.as_str()) + .context("Error installing SSH key")?; log::info!("Installed sshkey: {key_s}"); } } @@ -821,6 +828,8 @@ mod test { use super::BinaryFileInProgress; + use crate::serviceinfo::*; + #[test] fn test_binaryfileinprogress_destination_path() { assert_eq!( @@ -836,4 +845,49 @@ mod test { PathBuf::from("/etc/something") ); } + + #[test] + fn test_pw_encryption() { + let type_5_encryption = "$5$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_5_encryption)); + let type_2b_encryption = "$2b$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_2b_encryption)); + let type_6_encryption = "$6$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_6_encryption)); + let plaintext_encryption = "testpassword"; + assert!(!is_password_encrypted(plaintext_encryption)); + let empty_pw = ""; + assert!(!is_password_encrypted(empty_pw)); + } + + #[test] + fn test_user_creation_no_pw() { + let test_user = "test"; + assert!(create_user(test_user).is_ok()); + let empty_user = ""; + assert!(create_user(empty_user).is_err()); + let at_user = "test@test"; + assert!(create_user(at_user).is_err()); + let dash_user = "-test"; + assert!(create_user(dash_user).is_err()); + let digits_user = "12345"; + assert!(create_user(digits_user).is_err()); + } + + #[test] + fn test_user_creation_with_pw() { + let test_user = "testb"; + let test_password = "password"; + assert!(create_user_with_password(test_user, test_password).is_ok()); + let empty_user = ""; + assert!(create_user_with_password(empty_user, test_password).is_err()); + let at_user = "testb@testb"; + assert!(create_user_with_password(at_user, test_password).is_err()); + let dash_user = "-testb"; + assert!(create_user_with_password(dash_user, test_password).is_err()); + let digits_user = "123456"; + assert!(create_user_with_password(digits_user, test_password).is_err()); + let empty_password = ""; + assert!(create_user_with_password(test_user, empty_password).is_ok()); + } } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index b3c05158d..29525a73e 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -33,8 +33,10 @@ serde_json = "1.0" pretty_assertions = "1.0.0" paste = "1.0" passwd = "0.0.1" +shadow = "0.0.1" pem = "2.0" users = "0.11.0" +sha-crypt = "0.5.0" fdo-data-formats = { path = "../data-formats" } -fdo-util = { path = "../util" } +fdo-util = { path = "../util" } \ No newline at end of file diff --git a/integration-tests/templates/serviceinfo-api-server.yml.j2 b/integration-tests/templates/serviceinfo-api-server.yml.j2 index e12fe146a..581b592b9 100644 --- a/integration-tests/templates/serviceinfo-api-server.yml.j2 +++ b/integration-tests/templates/serviceinfo-api-server.yml.j2 @@ -8,7 +8,7 @@ admin_auth_token: TestAdminToken service_info: initial_user: username: {{ user }} - password: test + password: {{ password }} sshkeys: - {{ sshkey }} files: diff --git a/integration-tests/tests/common/mod.rs b/integration-tests/tests/common/mod.rs index 2b683dee5..d16dc4272 100644 --- a/integration-tests/tests/common/mod.rs +++ b/integration-tests/tests/common/mod.rs @@ -808,6 +808,7 @@ impl<'a> TestServerConfigurator<'a> { users::get_current_username().unwrap().to_str().unwrap(), ); cfg.insert("sshkey", "sshkey_default"); + cfg.insert("password", "testpassword"); } else { L.l("per_device_serviceinfo is set, using device specific values"); cfg.insert( @@ -815,6 +816,7 @@ impl<'a> TestServerConfigurator<'a> { users::get_current_username().unwrap().to_str().unwrap(), ); cfg.insert("sshkey", "sshkey_per_device"); + cfg.insert("password", "testpassword"); } // TODO: Insert more defaults diff --git a/integration-tests/tests/e2e.rs b/integration-tests/tests/e2e.rs index 63792e8b9..4d12a3c4e 100644 --- a/integration-tests/tests/e2e.rs +++ b/integration-tests/tests/e2e.rs @@ -7,6 +7,8 @@ use common::{Binary, LogSide, TestContext}; use anyhow::{bail, Context, Result}; +use sha_crypt::sha256_check; + const L: LogSide = LogSide::Test; #[tokio::test] @@ -138,6 +140,7 @@ where env::set_var("PER_DEVICE_SERVICEINFO", "false"); let mut ctx = TestContext::new().context("Error building test context")?; let new_user: &str = "testuser"; // new user to be created during onboarding + let new_pw: &str = "testpassword"; // new password to accompany new user during onboarding let encrypted_disk_loc = ctx.testpath().join("encrypted.img"); let rendezvous_server = ctx .start_test_server( @@ -156,7 +159,8 @@ where &encrypted_disk_loc.to_string_lossy(), ); if ci { - cfg.insert("user", new_user) + cfg.insert("user", new_user); + cfg.insert("password", new_pw); }; Ok(()) })?) @@ -402,6 +406,14 @@ sshkey_default "User: {} is not created during onboarding", &new_user ); + if let Some(test_user) = shadow::Shadow::from_name(new_user) { + pretty_assertions::assert_eq!( + test_user.password.is_empty(), + false, + "Password not created during onboarding" + ); + assert!(sha256_check("testpassword", &test_user.password).is_ok()); + } } else { L.l("Skipped create initial user validation To validate set env variable FDO_PRIVILEGED and run test as superuser"); diff --git a/integration-tests/tests/service_info.rs b/integration-tests/tests/service_info.rs new file mode 100644 index 000000000..1eea6c3a9 --- /dev/null +++ b/integration-tests/tests/service_info.rs @@ -0,0 +1,331 @@ +mod common; +use std::env; +#[allow(unused_imports)] +use std::{fs, io::Write, process::Command, time::Duration}; + +use common::{Binary, LogSide, TestContext}; + +use anyhow::{bail, Context, Result}; + +use sha_crypt::sha256_check; + +const L: LogSide = LogSide::Test; + +#[tokio::test] +async fn testpw_testpassword() -> Result<()> { + env::set_var("PER_DEVICE_SERVICEINFO", "false"); + + test_e2e_impl_default_serviceinfo( + |_| { + Ok(( + "DIUN_PUB_KEY_INSECURE", + "true".to_string(), + "Trusting any certificate as root", + )) + }, + "FileSystem", + "testuser", + "testpassword", + ) + .await +} + +#[derive(Debug)] +struct TestCase { + #[allow(dead_code)] + diun_verification_method_name: &'static str, + #[allow(dead_code)] + diun_key_type: &'static str, + #[allow(dead_code)] + error: anyhow::Error, +} + +async fn test_e2e_impl_default_serviceinfo( + verification_generator: F, + diun_key_type: &str, + test_user_string: &str, + test_pw_string: &str, +) -> Result<()> +where + F: Fn(&TestContext) -> Result<(&'static str, String, &'static str)>, +{ + let ci = env::var("FDO_PRIVILEGED").is_ok(); + env::set_var("PER_DEVICE_SERVICEINFO", "false"); + let mut ctx = TestContext::new().context("Error building test context")?; + let new_user: &str = test_user_string; // new user to be created during onboarding + let new_pw: &str = test_pw_string; // new password to accompany new user during onboarding + let encrypted_disk_loc = ctx.testpath().join("encrypted.img"); + let rendezvous_server = ctx + .start_test_server( + Binary::RendezvousServer, + |cfg| Ok(cfg.prepare_config_file(None, |_| Ok(()))?), + |_| Ok(()), + ) + .context("Error creating rendezvous server")?; + let serviceinfo_api_server = ctx + .start_test_server( + Binary::ServiceInfoApiServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert( + "encrypted_disk_label", + &encrypted_disk_loc.to_string_lossy(), + ); + if ci { + cfg.insert("user", new_user); + cfg.insert("password", new_pw); + }; + Ok(()) + })?) + }, + |_| Ok(()), + ) + .context("Error creating serviceinfo API dev server")?; + let owner_onboarding_server = ctx + .start_test_server( + Binary::OwnerOnboardingServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert( + "serviceinfo_api_server_port", + &serviceinfo_api_server.server_port().unwrap(), + ); + Ok(()) + })?) + }, + |cmd| { + cmd.env("ALLOW_NONINTEROPERABLE_KDF", &"1"); + Ok(()) + }, + ) + .context("Error creating owner server")?; + let mfg_server = ctx + .start_test_server( + Binary::ManufacturingServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert("diun_key_type", diun_key_type); + cfg.insert("rendezvous_port", &rendezvous_server.server_port().unwrap()); + cfg.insert("device_identification_format", "SerialNumber"); + Ok(()) + })?) + }, + |_| Ok(()), + ) + .context("Error creating manufacturing server")?; + ctx.wait_until_servers_ready() + .await + .context("Error waiting for servers to start")?; + + let (verification_key, verification_value, verification_searchstr) = + verification_generator(&ctx).context("Error generating verification information")?; + + // Execute the DI(UN) protocols + let client_result = ctx + .run_client( + Binary::ManufacturingClient, + Some(&mfg_server), + |cfg| { + cfg.env("DEVICE_CREDENTIAL_FILENAME", "devicecredential.dc") + .env("MANUFACTURING_INFO", "testdevice") + .env(&verification_key, &verification_value); + Ok(()) + }, + Duration::from_secs(5), + ) + .context("Error running manufacturing client")?; + client_result + .expect_success() + .context("Manufacturing client failed")?; + client_result.expect_stderr_line(verification_searchstr)?; + + // Execute some tests on the device credential and ownership voucher + let dc_path = client_result.client_path().join("devicecredential.dc"); + L.l(format!("Device Credential should be in {:?}", dc_path)); + + let ov_dir = ctx.testpath().join("ownership_vouchers"); + let mut ov_files = + std::fs::read_dir(ov_dir).context("Error reading ownership voucher directory")?; + L.l(format!("Ownership Voucher files: {:?}", &ov_files)); + let ov_file = ov_files.next(); + if ov_file.is_none() { + bail!("No ownership voucher files found"); + } + let ov_file = ov_file + .unwrap() + .context("Error reading OV file directory")?; + let num_count = ov_files.count() + 1; // The +1 is because we consumed the first item + if num_count != 1 { + bail!( + "Invalid number of ownership vouchers: {}, expected 1", + num_count + ); + } + L.l(format!("Ownership voucher path: {:?}", ov_file)); + let device_guid = ov_file.file_name().to_str().unwrap().to_string(); + L.l(format!("Device GUID: {}", device_guid)); + + L.l("Adding disk encryption tests"); + L.l("Creating empty disk image"); + if !Command::new("truncate") + .arg("-s") + .arg("1G") + .arg(&encrypted_disk_loc) + .status() + .context("Error running truncate")? + .success() + { + bail!("Error creating empty disk image"); + } + + L.l("Encrypting disk image"); + let mut child = Command::new("cryptsetup") + .arg("luksFormat") + .arg(&encrypted_disk_loc) + .arg("--force-password") + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Error starting cryptsetup luksFormat")?; + { + let mut stdin = child.stdin.take().context("Error taking stdin")?; + writeln!(stdin, "testpassword")?; + stdin.flush()?; + } + + let output = child.wait().context("Error waiting for cryptsetup")?; + if !output.success() { + bail!("Failed to call cryptsetup"); + } + + L.l("Binding disk image"); + let mut child = Command::new("clevis") + .arg("luks") + .arg("bind") + .arg("-d") + .arg(&encrypted_disk_loc) + .arg("test") + .arg("{}") + .env("PATH", ctx.get_path_env()?) + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Error starting clevis luks bind")?; + { + let mut stdin = child.stdin.take().context("Error taking stdin")?; + writeln!(stdin, "testpassword")?; + stdin.flush()?; + } + + let output = child.wait().context("Error waiting for clevis to bind")?; + if !output.success() { + bail!("Failed to call clevis luks bind"); + } + + let client = reqwest::Client::new(); + + // Ensure TO0 is executed + let res = client + .post(format!( + "http://localhost:{}/report-to-rendezvous", // DevSkim: ignore DS137138 + owner_onboarding_server.server_port().unwrap() + )) + .send() + .await?; + L.l(format!("Status code report-to-rendezvous {}", res.status())); + + // Execute TO1/TO2 protocols + let ssh_authorized_keys_path = ctx.testpath().join("authorized_keys"); + let marker_file_path = ctx.testpath().join("marker"); + let binary_file_path_prefix = ctx.testpath().join("binary_files"); + + std::fs::create_dir(&binary_file_path_prefix).context("Error creating binary_files dir")?; + + let output = ctx + .run_client( + Binary::ClientLinuxapp, + None, + |cfg| { + cfg.env("DEVICE_CREDENTIAL", dc_path.to_str().unwrap()) + .env("SSH_KEY_PATH", &ssh_authorized_keys_path.to_str().unwrap()) + .env( + "BINARYFILE_PATH_PREFIX", + binary_file_path_prefix.to_str().unwrap(), + ) + .env( + "DEVICE_ONBOARDING_EXECUTED_MARKER_FILE_PATH", + &marker_file_path.to_str().unwrap(), + ) + .env("ALLOW_NONINTEROPERABLE_KDF", &"1"); + Ok(()) + }, + Duration::from_secs(60), + ) + .context("Error running client")?; + output.expect_success().context("client failed")?; + + pretty_assertions::assert_eq!( + fs::read_to_string(&marker_file_path).context("Error reading marker file")?, + "executed" + ); + pretty_assertions::assert_eq!( + fs::read_to_string(&ssh_authorized_keys_path) + .context("Error reading authorized SSH keys")?, + " +# These keys are installed by FIDO Device Onboarding +sshkey_default +# End of FIDO Device Onboarding keys +" + ); + if ci { + L.l("Running create initial user validation"); + pretty_assertions::assert_eq!( + passwd::Passwd::from_name(new_user).is_some(), + true, + "User: {} is not created during onboarding", + &new_user + ); + if let Some(test_user) = shadow::Shadow::from_name(new_user) { + pretty_assertions::assert_eq!( + test_user.password.is_empty(), + false, + "Password not created during onboarding" + ); + assert!(sha256_check("testpassword", &test_user.password).is_ok()); + } + } else { + L.l("Skipped create initial user validation + To validate set env variable FDO_PRIVILEGED and run test as superuser"); + } + + L.l("Checking encrypted disk image"); + let output = Command::new("cryptsetup") + .arg("luksDump") + .arg(encrypted_disk_loc) + .output() + .context("Error running cryptsetup")?; + if !output.status.success() { + bail!("Failed to call cryptsetup"); + } + let luksdump_stdout = + String::from_utf8(output.stdout).context("Error reading luksDump stdout")?; + L.l(format!("Cryptsetup luksDump output: {:?}", luksdump_stdout)); + let mut found_ds_backup_final = false; + let mut found_reencrypt_unbound = false; + for stdout_line in luksdump_stdout.split('\n') { + if stdout_line.contains("flags") && stdout_line.contains("backup-final") { + found_ds_backup_final = true; + continue; + } + if stdout_line.contains("reencrypt (unbound)") { + found_reencrypt_unbound = true; + continue; + } + } + if !found_ds_backup_final { + bail!("Failed to find backup-final flag in cryptsetup output"); + } + if !found_reencrypt_unbound { + bail!("Failed to find reencrypt (unbound) flag in cryptsetup output"); + } + + Ok(()) +} diff --git a/owner-onboarding-server/src/handlers.rs b/owner-onboarding-server/src/handlers.rs index d065c8c1a..b8a389134 100644 --- a/owner-onboarding-server/src/handlers.rs +++ b/owner-onboarding-server/src/handlers.rs @@ -540,8 +540,8 @@ async fn perform_service_info( )?; if initial_user.password.is_some() { out_si.add( - FedoraIotServiceInfoModule::SSHKey, - "password", + FedoraIotServiceInfoModule::SSHKey, + "password", &initial_user.password, )?; }