Skip to content

Commit

Permalink
Move code to get secrets into a module.
Browse files Browse the repository at this point in the history
This refactor gets the code responsible for importing secrets from the
user. This gets a significant amount of code driving interaction with
the user out of the `hsm` module and into one dedicated to the purpose.
  • Loading branch information
flihp committed Nov 19, 2024
1 parent b4eccf9 commit 2e2673b
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 141 deletions.
119 changes: 11 additions & 108 deletions src/hsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use static_assertions as sa;
use std::collections::HashSet;
use std::{
fs,
io::{self, Read, Write},
io::{self, Write},
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
};
Expand Down Expand Up @@ -40,7 +41,6 @@ const SEED_LEN: usize = 32;
const KEY_LEN: usize = 32;
const SHARE_LEN: usize = KEY_LEN + 1;
const LABEL: &str = "backup";
pub const VERIFIER_FILE: &str = "verifier.json";

pub const LIMIT: usize = 5;
pub const THRESHOLD: usize = 3;
Expand Down Expand Up @@ -73,6 +73,8 @@ pub enum HsmError {
SplitKeyFailed { e: vsss_rs::Error },
#[error("your yubihms is broke")]
Version,
#[error("Not enough shares.")]
NotEnoughShares,
}

pub struct Alphabet {
Expand Down Expand Up @@ -381,119 +383,20 @@ impl Hsm {
/// This function prompts the user to enter M of the N backup shares. It
/// uses these shares to reconstitute the wrap key. This wrap key can then
/// be used to restore previously backed up / export wrapped keys.
pub fn restore_wrap(&self) -> Result<()> {
/// This function prompts the user to enter M of the N backup shares. It
/// uses these shares to reconstitute the wrap key. This wrap key can then
/// be used to restore previously backed up / export wrapped keys.
pub fn restore_wrap(&self, shares: Zeroizing<Vec<Share>>) -> Result<()> {
info!("Restoring HSM from backup");
info!("Restoring backup / wrap key from shares");
// vector used to collect shares
let mut shares: Vec<Share> = Vec::new();

// deserialize verifier:
// verifier was serialized to output/verifier.json in the provisioning ceremony
// it must be included in and deserialized from the ceremony inputs
let verifier = self.out_dir.join(VERIFIER_FILE);
let verifier = fs::read_to_string(verifier)?;
let verifier: Verifier = serde_json::from_str(&verifier)?;

// get enough shares to recover backup key
for _ in 1..=THRESHOLD {
// attempt to get a single share until the custodian enters a
// share that we can verify
loop {
// clear the screen, move cursor to (0,0), & prompt user
print!("\x1B[2J\x1B[1;1H");
print!("Enter share\n: ");
io::stdout().flush()?;
// get share from stdin
let mut share = String::new();
let share = match io::stdin().read_line(&mut share) {
Ok(count) => match count {
0 => {
// Ctrl^D / EOF
continue;
}
// 33 bytes -> 66 characters + 1 newline
67 => share,
_ => {
print!(
"\nexpected 67 characters, got {}.\n\n\
Press any key to try again ...",
share.len()
);
io::stdout().flush()?;

// wait for a keypress / 1 byte from stdin
let _ = io::stdin().read(&mut [0u8]).unwrap();
continue;
}
},
Err(e) => {
print!(
"Error from `Stdin::read_line`: {}\n\n\
Press any key to try again ...",
e
);
io::stdout().flush()?;

// wait for a keypress / 1 byte from stdin
let _ = io::stdin().read(&mut [0u8]).unwrap();
continue;
}
};

// drop all whitespace from line entered, interpret it as a
// hex string that we decode
let share: String =
share.chars().filter(|c| !c.is_whitespace()).collect();
let share_vec = match hex::decode(share) {
Ok(share) => share,
Err(_) => {
println!(
"Failed to decode Share. The value entered isn't \
a valid hex string: try again."
);
continue;
}
};

// construct a Share from the decoded hex string
let share = match Share::try_from(&share_vec[..]) {
Ok(share) => share,
Err(_) => {
println!(
"Failed to convert share entered to Share type. \
The value entered is the wrong length ... try \
again."
);
continue;
}
};

if verifier.verify(&share) {
// if we're going to switch from paper to CDs for key
// share persistence this is the most obvious place to
// put a keyshare on to a CD w/ lots of refactoring
shares.push(share);
print!(
"\nShare verified!\n\nPress any key to continue ..."
);
io::stdout().flush()?;

// wait for a keypress / 1 byte from stdin
let _ = io::stdin().read(&mut [0u8]).unwrap();
break;
} else {
println!("Failed to verify share: try again");
continue;
}
}
if shares.len() < THRESHOLD {
return Err(HsmError::NotEnoughShares.into());
}

print!("\x1B[2J\x1B[1;1H");

let scalar = Feldman::<THRESHOLD, LIMIT>::combine_shares::<
Scalar,
SHARE_LEN,
>(&shares)
>(shares.deref())
.map_err(|e| HsmError::CombineKeyFailed { e })?;

let nz_scalar = NonZeroScalar::from_repr(scalar.to_repr());
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
pub mod ca;
pub mod config;
pub mod hsm;
pub mod secret_reader;
pub mod secret_writer;
pub mod util;
98 changes: 65 additions & 33 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use log::{debug, error, info, LevelFilter};
use std::{
collections::HashMap,
env, fs,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
};
Expand All @@ -21,15 +22,18 @@ use oks::{
self, CsrSpec, DcsrSpec, KeySpec, Transport, CSRSPEC_EXT, DCSRSPEC_EXT,
ENV_NEW_PASSWORD, ENV_PASSWORD, KEYSPEC_EXT,
},
hsm::{Hsm, LIMIT, VERIFIER_FILE},
hsm::{Hsm, Share, Verifier, LIMIT, THRESHOLD},
secret_reader::{StdioPasswordReader, StdioShareReader},
secret_writer::{PrinterSecretWriter, DEFAULT_PRINT_DEV},
util,
};

const PASSWD_PROMPT: &str = "Enter new password: ";
const PASSWD_PROMPT2: &str = "Enter password again to confirm: ";
const PASSWD_PROMPT: &str = "Enter YubiHSM Password: ";
const PASSWD_NEW: &str = "Enter new password: ";
const PASSWD_NEW_2: &str = "Enter password again to confirm: ";

const GEN_PASSWD_LENGTH: usize = 16;
const VERIFIER_FILE: &str = "verifier.json";

// when we write out signed certs to the file system this suffix is appended
const CERT_SUFFIX: &str = "cert.pem";
Expand Down Expand Up @@ -163,7 +167,13 @@ enum HsmCommand {
},

/// Restore a previously split aes256-ccm-wrap key
Restore,
Restore {
#[clap(long, env, default_value = "input")]
backups: PathBuf,

#[clap(long, env, default_value = "input/verifier.json")]
verifier: PathBuf,
},

/// Get serial number from YubiHSM and dump to console.
SerialNumber,
Expand Down Expand Up @@ -200,7 +210,7 @@ fn get_auth_id(auth_id: Option<Id>, command: &HsmCommand) -> Id {
print_dev: _,
passwd_challenge: _,
}
| HsmCommand::Restore
| HsmCommand::Restore { .. }
| HsmCommand::SerialNumber => 1,
// otherwise we assume the auth key that we create is
// present: auth_id 2
Expand All @@ -211,14 +221,18 @@ fn get_auth_id(auth_id: Option<Id>, command: &HsmCommand) -> Id {

/// Get password either from environment, the YubiHSM2 default, or challenge
/// the user with a password prompt.
fn get_passwd(auth_id: Option<Id>, command: &HsmCommand) -> Result<String> {
match env::var(ENV_PASSWORD).ok() {
Some(s) => Ok(s),
fn get_passwd(
auth_id: Option<Id>,
command: &HsmCommand,
) -> Result<Zeroizing<String>> {
let passwd = match env::var(ENV_PASSWORD).ok() {
Some(s) => Zeroizing::new(s),
None => {
let passwd_reader = StdioPasswordReader::default();
if auth_id.is_some() {
// if auth_id was set by the caller but not the password we
// prompt for the password
Ok(rpassword::prompt_password("Enter YubiHSM Password: ")?)
passwd_reader.read(PASSWD_PROMPT)?
} else {
match command {
// if password isn't set, auth_id isn't set, and
Expand All @@ -229,47 +243,52 @@ fn get_passwd(auth_id: Option<Id>, command: &HsmCommand) -> Result<String> {
print_dev: _,
passwd_challenge: _,
}
| HsmCommand::Restore
| HsmCommand::SerialNumber => Ok("password".to_string()),
| HsmCommand::Restore { .. }
| HsmCommand::SerialNumber => {
Zeroizing::new("password".to_string())
}
// otherwise prompt the user for the password
_ => Ok(rpassword::prompt_password(
"Enter YubiHSM Password: ",
)?),
_ => passwd_reader.read(PASSWD_PROMPT)?,
}
}
}
}
};

Ok(passwd)
}

/// get a new password from the environment or by issuing a challenge the user
fn get_new_passwd(hsm: Option<&Hsm>) -> Result<Zeroizing<String>> {
match env::var(ENV_NEW_PASSWORD).ok() {
let passwd = match env::var(ENV_NEW_PASSWORD).ok() {
// prefer new password from env above all else
Some(s) => {
info!("got password from env");
Ok(Zeroizing::new(s))
Zeroizing::new(s)
}
None => match hsm {
// use the HSM otherwise if available
Some(hsm) => {
info!("Generating random password");
Ok(Zeroizing::new(hsm.rand_string(GEN_PASSWD_LENGTH)?))
Zeroizing::new(hsm.rand_string(GEN_PASSWD_LENGTH)?)
}
// last option: challenge the caller
None => loop {
let password =
Zeroizing::new(rpassword::prompt_password(PASSWD_PROMPT)?);
let password2 =
Zeroizing::new(rpassword::prompt_password(PASSWD_PROMPT2)?);
if password != password2 {
error!("the passwords entered do not match");
} else {
debug!("got the same password twice");
return Ok(password);
None => {
let passwd_reader = StdioPasswordReader::default();
loop {
let password = passwd_reader.read(PASSWD_NEW)?;
let password2 = passwd_reader.read(PASSWD_NEW_2)?;
if password != password2 {
error!("the passwords entered do not match");
} else {
debug!("got the same password twice");
break password;
}
}
},
}
},
}
};

Ok(passwd)
}

/// Perform all operations that make up the ceremony for provisioning an
Expand Down Expand Up @@ -746,9 +765,22 @@ fn main() -> Result<()> {
hsm.replace_default_auth(&passwd_new)
}
HsmCommand::Generate { key_spec } => hsm.generate(&key_spec),
HsmCommand::Restore => {
hsm.restore_wrap()?;
oks::hsm::restore(&hsm.client, &hsm.state_dir)?;
HsmCommand::Restore { backups, verifier } => {
let verifier = fs::read_to_string(verifier)?;
let verifier: Verifier = serde_json::from_str(&verifier)?;
let share_itr = StdioShareReader::new(verifier);

let mut shares: Zeroizing<Vec<Share>> =
Zeroizing::new(Vec::new());
for share in share_itr {
shares.deref_mut().push(*share?.deref());
if shares.len() >= THRESHOLD {
break;
}
}

hsm.restore_wrap(shares)?;
oks::hsm::restore(&hsm.client, backups)?;
info!("Deleting default authentication key");
oks::hsm::delete(&hsm.client, 1, Type::AuthenticationKey)
}
Expand Down
Loading

0 comments on commit 2e2673b

Please sign in to comment.