Skip to content

Commit

Permalink
feat: user creation with encrypted password
Browse files Browse the repository at this point in the history
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 <djachimo@redhat.com>
  • Loading branch information
djach7 authored and 7flying committed Jun 14, 2023
1 parent 0ec9396 commit c34d721
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 22 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions client-linuxapp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
128 changes: 110 additions & 18 deletions client-linuxapp/src/serviceinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<Vec<ServiceInfoModule>> {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -373,7 +436,8 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) -
let mut active_modules: HashSet<ServiceInfoModule> = HashSet::new();

let mut sshkey_user: Option<String> = None;
let mut sshkey_key: Option<String> = None;
let mut sshkey_password: Option<String> = None;
let mut sshkey_keys: Option<String> = None;

let mut rhsm_organization_id: Option<String> = None;
let mut rhsm_activation_key: Option<String> = None;
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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<String> = 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
Expand Down
1 change: 1 addition & 0 deletions integration-tests/templates/serviceinfo-api-server.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ admin_auth_token: TestAdminToken
service_info:
initial_user:
username: {{ user }}
password: test
sshkeys:
- {{ sshkey }}
files:
Expand Down
15 changes: 13 additions & 2 deletions owner-onboarding-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ")),
)?;
}
}

Expand Down
2 changes: 2 additions & 0 deletions serviceinfo-api-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
}
Expand All @@ -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(),
});
}
Expand Down
3 changes: 2 additions & 1 deletion util/src/servers/configuration/serviceinfo_api_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ pub struct ServiceInfoCommand {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServiceInfoInitialUser {
pub username: String,
pub sshkeys: Vec<String>,
pub password: Option<String>,
pub sshkeys: Option<Vec<String>>,
}
3 changes: 2 additions & 1 deletion util/src/servers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ pub fn yaml_to_cbor(val: &Value) -> Result<CborValue> {
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceInfoApiReplyInitialUser {
pub username: String,
pub ssh_keys: Vec<String>,
pub password: Option<String>,
pub ssh_keys: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down

0 comments on commit c34d721

Please sign in to comment.