use crate::datetime::parse_datetime;
use crate::error::{self, Result};
use crate::source::parse_key_source;
use crate::{load_file, write_file};
use chrono::{DateTime, Timelike, Utc};
use clap::Parser;
use log::warn;
use maplit::hashmap;
use ring::rand::SystemRandom;
use snafu::{ensure, OptionExt, ResultExt};
use std::collections::HashMap;
use std::io::Write;
use std::num::NonZeroU64;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
use tough::editor::signed::SignedRole;
use tough::key_source::KeySource;
use tough::schema::decoded::{Decoded, Hex};
use tough::schema::{key::Key, KeyHolder, RoleKeys, RoleType, Root, Signed};
use tough::sign::{parse_keypair, Sign};
#[derive(Debug, Parser)]
pub(crate) enum Command {
Init {
path: PathBuf,
},
BumpVersion {
path: PathBuf,
},
Expire {
path: PathBuf,
#[clap(parse(try_from_str = parse_datetime))]
time: DateTime<Utc>,
},
SetThreshold {
path: PathBuf,
role: RoleType,
threshold: NonZeroU64,
},
SetVersion {
path: PathBuf,
version: NonZeroU64,
},
AddKey {
path: PathBuf,
#[clap(parse(try_from_str = parse_key_source))]
key_source: Box<dyn KeySource>,
#[clap(short = 'r', long = "role")]
roles: Vec<RoleType>,
},
RemoveKey {
path: PathBuf,
key_id: Decoded<Hex>,
role: Option<RoleType>,
},
GenRsaKey {
path: PathBuf,
#[clap(parse(try_from_str = parse_key_source))]
key_source: Box<dyn KeySource>,
#[clap(short = 'b', long = "bits", default_value = "2048")]
bits: u16,
#[clap(short = 'e', long = "exp", default_value = "65537")]
exponent: u32,
#[clap(short = 'r', long = "role")]
roles: Vec<RoleType>,
},
Sign {
path: PathBuf,
#[clap(short = 'k', long = "key",parse(try_from_str = parse_key_source))]
key_sources: Vec<Box<dyn KeySource>>,
#[clap(short = 'c', long = "cross-sign")]
cross_sign: Option<PathBuf>,
#[clap(short = 'i', long = "ignore-threshold")]
ignore_threshold: bool,
},
}
macro_rules! role_keys {
($threshold:expr) => {
RoleKeys {
keyids: Vec::new(),
threshold: $threshold,
_extra: HashMap::new(),
}
};
() => {
role_keys!(NonZeroU64::new(1507).unwrap())
};
}
impl Command {
pub(crate) fn run(self) -> Result<()> {
match self {
Command::Init { path } => Command::init(&path),
Command::BumpVersion { path } => Command::bump_version(&path),
Command::Expire { path, time } => Command::expire(&path, &time),
Command::SetThreshold {
path,
role,
threshold,
} => Command::set_threshold(&path, role, threshold),
Command::SetVersion { path, version } => Command::set_version(&path, version),
Command::AddKey {
path,
roles,
key_source,
} => Command::add_key(&path, &roles, &key_source),
Command::RemoveKey { path, key_id, role } => Command::remove_key(&path, &key_id, role),
Command::GenRsaKey {
path,
roles,
key_source,
bits,
exponent,
} => Command::gen_rsa_key(&path, &roles, &key_source, bits, exponent),
Command::Sign {
path,
key_sources,
cross_sign,
ignore_threshold,
} => Command::sign(&path, &key_sources, cross_sign, ignore_threshold),
}
}
fn init(path: &Path) -> Result<()> {
write_file(
path,
&Signed {
signed: Root {
spec_version: crate::SPEC_VERSION.to_owned(),
consistent_snapshot: true,
version: NonZeroU64::new(1).unwrap(),
expires: round_time(Utc::now()),
keys: HashMap::new(),
roles: hashmap! {
RoleType::Root => role_keys!(),
RoleType::Snapshot => role_keys!(),
RoleType::Targets => role_keys!(),
RoleType::Timestamp => role_keys!(),
},
_extra: HashMap::new(),
},
signatures: Vec::new(),
},
)
}
fn bump_version(path: &Path) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
root.signed.version = NonZeroU64::new(
root.signed
.version
.get()
.checked_add(1)
.context(error::VersionOverflowSnafu)?,
)
.context(error::VersionZeroSnafu)?;
clear_sigs(&mut root);
write_file(path, &root)
}
fn expire(path: &Path, time: &DateTime<Utc>) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
root.signed.expires = round_time(*time);
clear_sigs(&mut root);
write_file(path, &root)
}
fn set_threshold(path: &Path, role: RoleType, threshold: NonZeroU64) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
root.signed
.roles
.entry(role)
.and_modify(|rk| rk.threshold = threshold)
.or_insert_with(|| role_keys!(threshold));
clear_sigs(&mut root);
write_file(path, &root)
}
fn set_version(path: &Path, version: NonZeroU64) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
root.signed.version = version;
clear_sigs(&mut root);
write_file(path, &root)
}
#[allow(clippy::borrowed_box)]
fn add_key(path: &Path, roles: &[RoleType], key_source: &Box<dyn KeySource>) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
let key_pair = key_source
.as_sign()
.context(error::KeyPairFromKeySourceSnafu)?
.tuf_key();
let key_id = hex::encode(add_key(&mut root.signed, roles, key_pair)?);
clear_sigs(&mut root);
println!("{}", key_id);
write_file(path, &root)
}
fn remove_key(path: &Path, key_id: &Decoded<Hex>, role: Option<RoleType>) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
if let Some(role) = role {
if let Some(role_keys) = root.signed.roles.get_mut(&role) {
role_keys
.keyids
.iter()
.position(|k| k.eq(key_id))
.map(|pos| role_keys.keyids.remove(pos));
}
} else {
for role_keys in root.signed.roles.values_mut() {
role_keys
.keyids
.iter()
.position(|k| k.eq(key_id))
.map(|pos| role_keys.keyids.remove(pos));
}
root.signed.keys.remove(key_id);
}
clear_sigs(&mut root);
write_file(path, &root)
}
#[allow(clippy::borrowed_box)]
fn gen_rsa_key(
path: &Path,
roles: &[RoleType],
key_source: &Box<dyn KeySource>,
bits: u16,
exponent: u32,
) -> Result<()> {
let mut root: Signed<Root> = load_file(path)?;
let mut command = std::process::Command::new("openssl");
command.args(&["genpkey", "-algorithm", "RSA", "-pkeyopt"]);
command.arg(format!("rsa_keygen_bits:{}", bits));
command.arg("-pkeyopt");
command.arg(format!("rsa_keygen_pubexp:{}", exponent));
let command_str = format!("{:?}", command);
let output = command.output().context(error::CommandExecSnafu {
command_str: &command_str,
})?;
ensure!(
output.status.success(),
error::CommandStatusSnafu {
command_str: &command_str,
status: output.status
}
);
let stdout =
String::from_utf8(output.stdout).context(error::CommandUtf8Snafu { command_str })?;
let key_pair = parse_keypair(stdout.as_bytes()).context(error::KeyPairParseSnafu)?;
let key_id = hex::encode(add_key(&mut root.signed, roles, key_pair.tuf_key())?);
key_source
.write(&stdout, &key_id)
.context(error::WriteKeySourceSnafu)?;
clear_sigs(&mut root);
println!("{}", key_id);
write_file(path, &root)
}
fn sign(
path: &Path,
key_source: &[Box<dyn KeySource>],
cross_sign: Option<PathBuf>,
ignore_threshold: bool,
) -> Result<()> {
let root: Signed<Root> = load_file(path)?;
let loaded_root = match cross_sign {
None => root.clone(),
Some(cross_sign_root) => load_file(&cross_sign_root)?,
};
let mut signed_root = SignedRole::new(
root.signed.clone(),
&KeyHolder::Root(loaded_root.signed),
key_source,
&SystemRandom::new(),
)
.context(error::SignRootSnafu { path })?;
if !root.signatures.is_empty() {
signed_root = signed_root
.add_old_signatures(root.signatures)
.context(error::SignRootSnafu { path })?;
}
for (roletype, rolekeys) in &signed_root.signed().signed.roles {
let threshold = rolekeys.threshold.get();
let keyids = rolekeys.keyids.len();
if threshold > keyids as u64 {
if !ignore_threshold {
return Err(error::Error::UnstableRoot {
role: *roletype,
threshold,
actual: keyids,
});
}
warn!(
"Loaded unstable root, role '{}' contains '{}' keys, expected '{}'",
*roletype, threshold, keyids
);
}
}
let threshold = signed_root
.signed()
.signed
.roles
.get(&RoleType::Root)
.ok_or(error::Error::UnstableRoot {
role: RoleType::Root,
threshold: 0,
actual: 0,
})?
.threshold
.get();
let signature_count = signed_root.signed().signatures.len();
if threshold > signature_count as u64 {
if !ignore_threshold {
return Err(error::Error::SignatureRoot {
threshold,
signature_count,
});
}
warn!(
"The root.json file requires at least {} signatures, the target file contains {}",
threshold, signature_count
);
}
let parent = path.parent().context(error::PathParentSnafu { path })?;
let mut writer =
NamedTempFile::new_in(parent).context(error::FileTempCreateSnafu { path: parent })?;
writer
.write_all(signed_root.buffer())
.context(error::FileWriteSnafu { path })?;
writer
.persist(path)
.context(error::FilePersistSnafu { path })?;
Ok(())
}
}
fn round_time(time: DateTime<Utc>) -> DateTime<Utc> {
time.with_nanosecond(0).unwrap()
}
fn clear_sigs<T>(role: &mut Signed<T>) {
role.signatures.clear();
}
fn add_key(root: &mut Root, role: &[RoleType], key: Key) -> Result<Decoded<Hex>> {
let key_id = if let Some((key_id, _)) = root
.keys
.iter()
.find(|(_, candidate_key)| key.eq(candidate_key))
{
key_id.clone()
} else {
let key_id = key.key_id().context(error::KeyIdSnafu)?;
ensure!(
!root.keys.contains_key(&key_id),
error::KeyDuplicateSnafu {
key_id: hex::encode(&key_id)
}
);
root.keys.insert(key_id.clone(), key);
key_id
};
for r in role {
let entry = root.roles.entry(*r).or_insert_with(|| role_keys!());
if !entry.keyids.contains(&key_id) {
entry.keyids.push(key_id.clone());
}
}
Ok(key_id)
}