#![allow(clippy::used_underscore_binding)]
use crate::editor::signed::{SignedDelegatedTargets, SignedRole};
use crate::error::{self, Result};
use crate::fetch::fetch_max_size;
use crate::key_source::KeySource;
use crate::schema::decoded::{Decoded, Hex};
use crate::schema::key::Key;
use crate::schema::{
DelegatedRole, DelegatedTargets, Delegations, KeyHolder, PathSet, RoleType, Signed, Target,
Targets,
};
use crate::transport::{IntoVec, Transport};
use crate::{encode_filename, Limits};
use crate::{Repository, TargetName};
use aws_lc_rs::rand::SystemRandom;
use chrono::{DateTime, Utc};
use serde_json::Value;
use snafu::{OptionExt, ResultExt};
use std::borrow::Cow;
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt::Display;
use std::num::NonZeroU64;
use std::path::Path;
use url::Url;
const SPEC_VERSION: &str = "1.0.0";
#[derive(Debug, Clone)]
pub struct TargetsEditor {
name: String,
pub(crate) key_holder: Option<KeyHolder>,
delegations: Option<Delegations>,
new_targets: Option<HashMap<TargetName, Target>>,
existing_targets: Option<HashMap<TargetName, Target>>,
version: Option<NonZeroU64>,
expires: Option<DateTime<Utc>>,
new_roles: Option<Vec<DelegatedRole>>,
_extra: Option<HashMap<String, Value>>,
limits: Option<Limits>,
transport: Option<Box<dyn Transport>>,
}
impl TargetsEditor {
pub fn new(name: &str) -> Self {
TargetsEditor {
key_holder: None,
delegations: Some(Delegations::new()),
new_targets: None,
existing_targets: None,
version: None,
expires: None,
name: name.to_string(),
new_roles: None,
_extra: None,
limits: None,
transport: None,
}
}
pub fn from_targets(name: &str, targets: Targets, key_holder: KeyHolder) -> Self {
TargetsEditor {
key_holder: Some(key_holder),
delegations: targets.delegations,
new_targets: None,
existing_targets: Some(targets.targets),
version: None,
expires: None,
name: name.to_string(),
new_roles: None,
_extra: Some(targets._extra),
limits: None,
transport: None,
}
}
pub fn from_repo(repo: Repository, name: &str) -> Result<Self> {
let (targets, key_holder) = if name == "targets" {
(
repo.targets.signed.clone(),
KeyHolder::Root(repo.root.signed.clone()),
)
} else {
let targets = repo
.delegated_role(name)
.context(error::DelegateNotFoundSnafu {
name: name.to_string(),
})?
.targets
.as_ref()
.context(error::NoTargetsSnafu)?
.signed
.clone();
let key_holder = KeyHolder::Delegations(
repo.targets
.signed
.parent_of(name)
.context(error::DelegateMissingSnafu {
name: name.to_string(),
})?
.clone(),
);
(targets, key_holder)
};
Ok(TargetsEditor {
key_holder: Some(key_holder),
delegations: targets.delegations,
new_targets: None,
existing_targets: Some(targets.targets),
version: None,
expires: None,
name: name.to_string(),
new_roles: None,
_extra: Some(targets._extra),
limits: Some(repo.limits),
transport: Some(repo.transport),
})
}
pub fn limits(&mut self, limits: Limits) {
self.limits = Some(limits);
}
pub fn transport(&mut self, transport: Box<dyn Transport>) {
self.transport = Some(transport);
}
pub fn add_target<T, E>(&mut self, name: T, target: Target) -> Result<&mut Self>
where
T: TryInto<TargetName, Error = E>,
E: Display,
{
let target_name = name.try_into().map_err(|e| {
error::InvalidTargetNameSnafu {
inner: e.to_string(),
}
.build()
})?;
self.new_targets
.get_or_insert_with(HashMap::new)
.insert(target_name, target);
Ok(self)
}
pub async fn add_target_path<P>(&mut self, target_path: P) -> Result<&mut Self>
where
P: AsRef<Path>,
{
let target_path = target_path.as_ref();
let target_name = TargetName::new(
target_path
.file_name()
.context(error::NoFileNameSnafu { path: target_path })?
.to_str()
.context(error::PathUtf8Snafu { path: target_path })?,
)?;
let target = Target::from_path(target_path)
.await
.context(error::TargetFromPathSnafu { path: target_path })?;
self.add_target(target_name, target)?;
Ok(self)
}
pub async fn add_target_paths<P>(&mut self, targets: Vec<P>) -> Result<&mut Self>
where
P: AsRef<Path>,
{
for target in targets {
self.add_target_path(target).await?;
}
Ok(self)
}
pub fn remove_target(&mut self, name: &TargetName) -> &mut Self {
if let Some(targets) = self.existing_targets.as_mut() {
targets.remove(name);
}
if let Some(targets) = self.new_targets.as_mut() {
targets.remove(name);
}
self
}
pub fn clear_targets(&mut self) -> &mut Self {
self.existing_targets
.get_or_insert_with(HashMap::new)
.clear();
self.new_targets.get_or_insert_with(HashMap::new).clear();
self
}
pub fn version(&mut self, version: NonZeroU64) -> &mut Self {
self.version = Some(version);
self
}
pub fn expires(&mut self, expires: DateTime<Utc>) -> &mut Self {
self.expires = Some(expires);
self
}
pub fn add_key(
&mut self,
keys: HashMap<Decoded<Hex>, Key>,
role: Option<&str>,
) -> Result<&mut Self> {
let delegations = self
.delegations
.as_mut()
.context(error::NoDelegationsSnafu)?;
let mut keyids = Vec::new();
for (keyid, key) in keys {
if !delegations
.keys
.values()
.any(|candidate_key| key == *candidate_key)
{
delegations.keys.insert(keyid.clone(), key);
}
keyids.push(keyid.clone());
}
if let Some(role) = role {
for delegated_role in &mut delegations.roles {
if delegated_role.name == role {
delegated_role.keyids.extend(keyids.clone());
}
}
for delegated_role in &mut *self.new_roles.get_or_insert(Vec::new()) {
if delegated_role.name == role {
delegated_role.keyids.extend(keyids.clone());
}
}
}
Ok(self)
}
pub fn remove_key(&mut self, keyid: &Decoded<Hex>, role: Option<&str>) -> Result<&mut Self> {
let delegations = self
.delegations
.as_mut()
.context(error::NoDelegationsSnafu)?;
if let Some(role) = role {
for delegated_role in &mut delegations.roles {
if delegated_role.name == role {
delegated_role.keyids.retain(|key| keyid != key);
}
}
} else {
delegations.keys.remove(keyid);
}
Ok(self)
}
pub fn delegate_role(
&mut self,
targets: Signed<DelegatedTargets>,
paths: PathSet,
terminating: bool,
key_pairs: HashMap<Decoded<Hex>, Key>,
keyids: Vec<Decoded<Hex>>,
threshold: NonZeroU64,
) -> Result<&mut Self> {
self.add_key(key_pairs, None)?;
self.new_roles
.get_or_insert(Vec::new())
.push(DelegatedRole {
name: targets.signed.name,
paths,
keyids,
threshold,
terminating,
targets: Some(Signed {
signed: targets.signed.targets,
signatures: targets.signatures,
}),
});
Ok(self)
}
pub fn remove_role(&mut self, role: &str, recursive: bool) -> Result<&mut Self> {
let delegations = self
.delegations
.as_mut()
.context(error::NoDelegationsSnafu)?;
delegations
.roles
.retain(|delegated_role| delegated_role.name != role);
if recursive {
delegations.roles.retain(|delegated_role| {
delegated_role
.targets
.as_ref()
.is_none_or(|targets| targets.signed.delegated_role(role).is_err())
});
}
Ok(self)
}
pub async fn add_role(
&mut self,
name: &str,
metadata_url: &str,
paths: PathSet,
terminating: bool,
threshold: NonZeroU64,
keys: Option<HashMap<Decoded<Hex>, Key>>,
) -> Result<&mut Self> {
let limits = self.limits.context(error::MissingLimitsSnafu)?;
let transport: &dyn Transport = self
.transport
.as_ref()
.context(error::MissingTransportSnafu)?
.as_ref();
let metadata_base_url = parse_url(metadata_url)?;
let encoded_name = encode_filename(name);
let encoded_filename = format!("{encoded_name}.json");
let role_url = metadata_base_url
.join(&encoded_filename)
.with_context(|_| error::JoinUrlEncodedSnafu {
original: name,
encoded: encoded_name,
filename: encoded_filename,
url: metadata_base_url,
})?;
let stream = fetch_max_size(
transport,
role_url.clone(),
limits.max_targets_size,
"max targets limit",
)
.await?;
let data = stream
.into_vec()
.await
.context(error::TransportSnafu { url: role_url })?;
let role: Signed<crate::schema::Targets> =
serde_json::from_slice(&data).context(error::ParseMetadataSnafu {
role: RoleType::Targets,
})?;
let delegated_targets = Signed {
signed: DelegatedTargets {
name: name.to_string(),
targets: role.signed.clone(),
},
signatures: role.signatures.clone(),
};
let (keyids, key_pairs) = if let Some(keys) = keys {
(keys.keys().cloned().collect(), keys)
} else {
let key_pairs = role
.signed
.delegations
.context(error::NoDelegationsSnafu)?
.keys;
(key_pairs.keys().cloned().collect(), key_pairs)
};
self.delegate_role(
delegated_targets,
paths,
terminating,
key_pairs,
keyids,
threshold,
)?;
Ok(self)
}
pub fn build_targets(&self) -> Result<DelegatedTargets> {
let version = self.version.context(error::MissingSnafu {
field: "targets version",
})?;
let expires = self.expires.context(error::MissingSnafu {
field: "targets expiration",
})?;
let mut targets: HashMap<TargetName, Target> = HashMap::new();
if let Some(ref existing_targets) = self.existing_targets {
targets.extend(existing_targets.clone());
}
if let Some(ref new_targets) = self.new_targets {
targets.extend(new_targets.clone());
}
let mut delegations = self.delegations.clone();
if let Some(delegations) = delegations.as_mut() {
if let Some(new_roles) = self.new_roles.as_ref() {
delegations.roles.extend(new_roles.clone());
}
}
let _extra = self._extra.clone().unwrap_or_default();
Ok(DelegatedTargets {
name: self.name.clone(),
targets: Targets {
spec_version: SPEC_VERSION.to_string(),
version,
expires,
targets,
_extra,
delegations,
},
})
}
async fn create_key_holder(&self, keys: &[Box<dyn KeySource>]) -> Result<KeyHolder> {
let mut delegations = Delegations::new();
let mut keyids = Vec::new();
let mut key_pairs = HashMap::new();
for source in keys {
let key_pair = source
.as_sign()
.await
.context(error::KeyPairFromKeySourceSnafu)?
.tuf_key();
key_pairs.insert(
key_pair
.key_id()
.context(error::JsonSerializationSnafu {})?
.clone(),
key_pair.clone(),
);
keyids.push(
key_pair
.key_id()
.context(error::JsonSerializationSnafu {})?
.clone(),
);
}
delegations.keys = key_pairs;
delegations.roles.push(DelegatedRole {
name: self.name.clone(),
threshold: NonZeroU64::new(1).unwrap(),
paths: PathSet::Paths([].to_vec()),
terminating: false,
keyids,
targets: None,
});
Ok(KeyHolder::Delegations(delegations))
}
pub async fn create_signed(
&self,
keys: &[Box<dyn KeySource>],
) -> Result<Signed<DelegatedTargets>> {
let rng = SystemRandom::new();
let key_holder = if let Some(key_holder) = self.key_holder.as_ref() {
key_holder.clone()
} else {
self.create_key_holder(keys).await?
};
let targets = self.build_targets()?;
let targets = SignedRole::new(targets, &key_holder, keys, &rng).await?;
Ok(targets.signed)
}
pub async fn sign(&self, keys: &[Box<dyn KeySource>]) -> Result<SignedDelegatedTargets> {
let rng = SystemRandom::new();
let mut roles = Vec::new();
let key_holder = if let Some(key_holder) = self.key_holder.as_ref() {
key_holder.clone()
} else {
self.create_key_holder(keys).await?
};
let signed_targets = self.build_targets()?;
let signed_targets = SignedRole::new(signed_targets, &key_holder, keys, &rng).await?;
roles.push(signed_targets);
if let Some(new_roles) = &self.new_roles {
for role in new_roles {
roles.push(SignedRole::from_signed(
role.clone()
.targets
.context(error::NoTargetsSnafu)?
.delegated_targets(&role.name),
)?);
}
}
Ok(SignedDelegatedTargets {
roles,
consistent_snapshot: false,
})
}
}
fn parse_url(url: &str) -> Result<Url> {
let mut url = Cow::from(url);
if !url.ends_with('/') {
url.to_mut().push('/');
}
Url::parse(&url).context(error::ParseUrlSnafu { url })
}