Skip to content

Commit

Permalink
Allow using password-protected private keys
Browse files Browse the repository at this point in the history
Signed-off-by: Firas Ghanmi <[email protected]>
  • Loading branch information
fghanmi committed Jul 26, 2024
1 parent 8fb4469 commit c98f607
Show file tree
Hide file tree
Showing 20 changed files with 261 additions and 51 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion tough-ssm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl KeySource for SsmKeySource {
})?
.as_bytes()
.to_vec();
let sign = Box::new(parse_keypair(&data).context(error::KeyPairParseSnafu)?);
let sign = Box::new(parse_keypair(&data,None).context(error::KeyPairParseSnafu)?);
Ok(sign)
}

Expand Down
1 change: 1 addition & 0 deletions tough/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ typed-path = "0.9"
untrusted = "0.9"
url = "2"
walkdir = "2"
openssl = "0.10"

[dev-dependencies]
failure-server = { path = "../integ/failure-server" }
Expand Down
4 changes: 2 additions & 2 deletions tough/src/editor/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ mod tests {
#[tokio::test]
async fn empty_repository() {
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key,password: None };
let root_path = root_path();

let editor = RepositoryEditor::new(root_path).await.unwrap();
Expand Down Expand Up @@ -112,7 +112,7 @@ mod tests {
async fn complete_repository() {
let root = root_path();
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key, password: None };
let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap();
let timestamp_version = NonZeroU64::new(1234).unwrap();
let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap();
Expand Down
5 changes: 4 additions & 1 deletion tough/src/key_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub trait KeySource: Debug + Send + Sync {
pub struct LocalKeySource {
/// The path to a local key file in PEM pkcs8 or RSA format.
pub path: PathBuf,
/// Optional password for the key file.
pub password: Option<String>,
}

/// Implements the `KeySource` trait for a `LocalKeySource` (file)
Expand All @@ -44,7 +46,8 @@ impl KeySource for LocalKeySource {
let data = tokio::fs::read(&self.path)
.await
.context(error::FileReadSnafu { path: &self.path })?;
Ok(Box::new(parse_keypair(&data)?))
let password: Option<&str> = self.password.as_deref();
Ok(Box::new(parse_keypair(&data,password)?))
}

async fn write(
Expand Down
32 changes: 27 additions & 5 deletions tough/src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaKeyPair};
use snafu::ResultExt;
use std::collections::HashMap;
use std::error::Error;

use openssl::rsa::Rsa;
use openssl::pkey::PKey;
use std::str;
/// This trait must be implemented for each type of key with which you will
/// sign things.
#[async_trait]
Expand Down Expand Up @@ -161,19 +163,39 @@ impl Sign for SignKeyPair {
}
}

/// Decrypts an RSA private key in PEM format using the given password.
/// Returns the decrypted key in PKCS8 format.
pub fn decrypt_key(encrypted_key: &[u8], password: &str) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error>> {

let pem_str = str::from_utf8(encrypted_key)?;
let rsa = Rsa::private_key_from_pem_passphrase(pem_str.as_bytes(), password.as_bytes())?;

let pkey = PKey::from_rsa(rsa)?;
let pkcs8 = pkey.private_key_to_pem_pkcs8()?;
Ok(pkcs8)
}

/// Parses a supplied keypair and if it is recognized, returns an object that
/// implements the Sign trait
/// Accepted Keys: ED25519 pkcs8, Ecdsa pkcs8, RSA
pub fn parse_keypair(key: &[u8]) -> Result<impl Sign> {
if let Ok(ed25519_key_pair) = Ed25519KeyPair::from_pkcs8(key) {
pub fn parse_keypair(key: &[u8], password: Option<&str>) -> Result<impl Sign> {

let decrypted_key = if let Some(pw) = password {
decrypt_key(key, pw).unwrap_or_else(|_| key.to_vec())
} else {
key.to_vec()
};
let decrypted_key_slice: &[u8] = &decrypted_key;

if let Ok(ed25519_key_pair) = Ed25519KeyPair::from_pkcs8(decrypted_key_slice) {
Ok(SignKeyPair::ED25519(ed25519_key_pair))
} else if let Ok(ecdsa_key_pair) = EcdsaKeyPair::from_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
key,
decrypted_key_slice,
&rand::SystemRandom::new(),
) {
Ok(SignKeyPair::ECDSA(ecdsa_key_pair))
} else if let Ok(pem) = pem::parse(key) {
} else if let Ok(pem) = pem::parse(decrypted_key_slice) {
match pem.tag() {
"PRIVATE KEY" => {
if let Ok(rsa_key_pair) = RsaKeyPair::from_pkcs8(pem.contents()) {
Expand Down
20 changes: 13 additions & 7 deletions tough/tests/repo_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,16 @@ async fn create_sign_write_reload_repo() {
.unwrap();

let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource { path: key_path() })];
&[Box::new(LocalKeySource { path: key_path(), password: None })];
let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path(),
password: None,
})];
let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path1(),
password: None,
})];

// add role1 to targets
Expand Down Expand Up @@ -257,14 +259,16 @@ async fn create_role_flow() {
let editor = test_repo_editor().await;

let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource { path: key_path() })];
&[Box::new(LocalKeySource { path: key_path(),password: None })];
let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path(),
password: None,
})];
let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path1(),
password: None,
})];

// write the repo to temp location
Expand Down Expand Up @@ -320,7 +324,7 @@ async fn create_role_flow() {

//sign everything since targets key is the same as snapshot and timestamp
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key, password: None };
let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap();
let timestamp_version = NonZeroU64::new(1234).unwrap();
let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap();
Expand Down Expand Up @@ -430,7 +434,7 @@ async fn create_role_flow() {
let metadata_base_url_out = dir_url(&metadata_destination_out);
// add outdir to repo
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key, password: None };

let mut editor = RepositoryEditor::from_repo(root_path(), new_repo)
.await
Expand Down Expand Up @@ -481,14 +485,16 @@ async fn update_targets_flow() {
let editor = test_repo_editor().await;

let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource { path: key_path() })];
&[Box::new(LocalKeySource { path: key_path(), password: None })];
let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path(),
password: None,
})];
let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] =
&[Box::new(LocalKeySource {
path: targets_key_path1(),
password: None,
})];

// write the repo to temp location
Expand Down Expand Up @@ -544,7 +550,7 @@ async fn update_targets_flow() {

//sign everything since targets key is the same as snapshot and timestamp
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key, password: None };
let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap();
let timestamp_version = NonZeroU64::new(1234).unwrap();
let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap();
Expand Down Expand Up @@ -654,7 +660,7 @@ async fn update_targets_flow() {
let metadata_base_url_out = dir_url(&metadata_destination_out);
// add outdir to repo
let root_key = key_path();
let key_source = LocalKeySource { path: root_key };
let key_source = LocalKeySource { path: root_key, password: None};

let mut editor = RepositoryEditor::from_repo(root_path(), new_repo)
.await
Expand Down
1 change: 1 addition & 0 deletions tough/tests/target_path_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn later() -> DateTime<Utc> {
async fn create_root(root_path: &Path, consistent_snapshot: bool) -> Vec<Box<dyn KeySource>> {
let keys: Vec<Box<dyn KeySource>> = vec![Box::new(LocalKeySource {
path: test_data().join("snakeoil.pem"),
password: None,
})];

let key_pair = keys.first().unwrap().as_sign().await.unwrap().tuf_key();
Expand Down
35 changes: 31 additions & 4 deletions tuftool/src/add_key_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ pub(crate) struct AddKeyArgs {
#[arg(short, long = "key", required = true)]
keys: Vec<String>,

/// [Optional] passwords/passphrases of the Key files to sign with
#[arg(short, long = "password")]
passwords: Option<Vec<String>>,

/// New keys to be used for role
#[arg(long = "new-key", required = true)]
new_keys: Vec<String>,

/// [Optional] passwords/passphrases of the new keys
#[arg(long = "new-password")]
new_passwords: Option<Vec<String>>,

/// TUF repository metadata base URL
#[arg(short, long = "metadata-url")]
metadata_base_url: Url,
Expand Down Expand Up @@ -66,8 +74,18 @@ impl AddKeyArgs {
async fn add_key(&self, role: &str, mut editor: TargetsEditor) -> Result<()> {
// create the keypairs to add
let mut key_pairs = HashMap::new();
for source in &self.new_keys {
let key_source = parse_key_source(source)?;
let default_password = String::new();
let new_passwords = match &self.new_passwords {
Some(pws) => pws,
None => &vec![],
};

if new_passwords.len() > self.new_keys.len() {
panic!("More new passwords provided than new key sources");
}
for (i, source) in self.new_keys.iter().enumerate() {
let password = new_passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source,Some(password.to_string()))?;
let key_pair = key_source
.as_sign()
.await
Expand All @@ -83,8 +101,17 @@ impl AddKeyArgs {
}

let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
let passwords = match &self.passwords {
Some(pws) => pws,
None => &vec![],
};
if passwords.len() > self.keys.len() {
panic!("More passwords provided than key sources");
}

for (i,source) in self.keys.iter().enumerate() {
let password = passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source,Some(password.to_string()))?;
keys.push(key_source);
}

Expand Down
30 changes: 26 additions & 4 deletions tuftool/src/add_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ pub(crate) struct AddRoleArgs {
#[arg(short, long = "key", required = true)]
keys: Vec<String>,

/// [Optional] passwords/passphrases of the Key files
#[arg(long = "password")]
passwords: Option<Vec<String>>,

/// TUF repository metadata base URL
#[arg(short, long = "metadata-url")]
metadata_base_url: Url,
Expand Down Expand Up @@ -120,8 +124,17 @@ impl AddRoleArgs {
};

let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
let default_password = String::new();
let passwords = match &self.passwords {
Some(pws) => pws,
None => &vec![],
};
if passwords.len() > self.keys.len() {
panic!("More passwords provided than key sources");
}
for (i, source) in self.keys.iter().enumerate() {
let password = passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source, Some(password.to_string()))?;
keys.push(key_source);
}

Expand Down Expand Up @@ -155,8 +168,17 @@ impl AddRoleArgs {
/// Adds a role to metadata using repo Editor
async fn with_repo_editor(&self, role: &str, mut editor: RepositoryEditor) -> Result<()> {
let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
let default_password = String::new();
let passwords = match &self.passwords {
Some(pws) => pws,
None => &vec![],
};
if passwords.len() > self.keys.len() {
panic!("More passwords provided than key sources");
}
for (i, source) in self.keys.iter().enumerate() {
let password = passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source, Some(password.to_string()))?;
keys.push(key_source);
}

Expand Down
17 changes: 15 additions & 2 deletions tuftool/src/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub(crate) struct CreateArgs {
#[arg(short, long = "key", required = true)]
keys: Vec<String>,

/// [Optional] passwords/passphrases of the Key files
#[arg(short, long = "password")]
passwords: Option<Vec<String>>,

/// The directory where the repository will be written
#[arg(short, long)]
outdir: PathBuf,
Expand Down Expand Up @@ -80,8 +84,17 @@ pub(crate) struct CreateArgs {
impl CreateArgs {
pub(crate) async fn run(&self) -> Result<()> {
let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
let default_password = String::new();
let passwords = match &self.passwords {
Some(pws) => pws,
None => &vec![],
};
if passwords.len() > self.keys.len() {
panic!("More passwords provided than key sources");
}
for (i, source) in self.keys.iter().enumerate() {
let password = passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source, Some(password.to_string()))?;
keys.push(key_source);
}

Expand Down
18 changes: 16 additions & 2 deletions tuftool/src/create_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub(crate) struct CreateRoleArgs {
#[arg(short, long, required = true)]
keys: Vec<String>,

/// [Optional] passwords/passphrases of the Key files
#[arg(short, long = "password")]
passwords: Option<Vec<String>>,

/// The directory where the repository will be written
#[arg(short, long)]
outdir: PathBuf,
Expand All @@ -39,8 +43,18 @@ pub(crate) struct CreateRoleArgs {
impl CreateRoleArgs {
pub(crate) async fn run(&self, role: &str) -> Result<()> {
let mut keys = Vec::new();
for source in &self.keys {
let key_source = parse_key_source(source)?;
let default_password = String::new();
let passwords = match &self.passwords {
Some(pws) => pws,
None => &vec![],
};
if passwords.len() > self.keys.len() {
panic!("More passwords provided than key sources");
}

for (i, source) in self.keys.iter().enumerate() {
let password = passwords.get(i).unwrap_or(&default_password);
let key_source = parse_key_source(source, Some(password.to_string()))?;
keys.push(key_source);
}

Expand Down
Loading

0 comments on commit c98f607

Please sign in to comment.