#![allow(clippy::used_underscore_binding, clippy::pub_underscore_fields)]
mod de;
pub mod decoded;
mod error;
mod iter;
pub mod key;
mod spki;
mod verify;
use crate::schema::decoded::{Decoded, Hex};
pub use crate::schema::error::{Error, Result};
use crate::schema::iter::KeysIter;
use crate::schema::key::Key;
use crate::sign::Sign;
pub use crate::transport::{FilesystemTransport, Transport};
use crate::{encode_filename, TargetName};
use aws_lc_rs::digest::{digest, Context, SHA256};
use chrono::{DateTime, Utc};
use globset::{Glob, GlobMatcher};
use hex::ToHex;
use olpc_cjson::CanonicalFormatter;
use serde::de::Error as SerdeDeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize};
use snafu::ResultExt;
use std::collections::{BTreeSet, HashMap};
use std::num::NonZeroU64;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::str::FromStr;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum RoleType {
Root,
Snapshot,
Targets,
Timestamp,
DelegatedTargets,
}
derive_display_from_serialize!(RoleType);
derive_fromstr_from_deserialize!(RoleType);
#[derive(Debug, Clone)]
pub enum RoleId {
StandardRole(RoleType),
DelegatedRole(String),
}
pub trait Role: Serialize {
const TYPE: RoleType;
fn expires(&self) -> DateTime<Utc>;
fn version(&self) -> NonZeroU64;
fn filename(&self, consistent_snapshot: bool) -> String;
fn role_id(&self) -> RoleId {
RoleId::StandardRole(Self::TYPE)
}
fn canonical_form(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut data, CanonicalFormatter::new());
self.serialize(&mut ser)
.context(error::JsonSerializationSnafu { what: "role" })?;
Ok(data)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct Signed<T> {
pub signed: T,
pub signatures: Vec<Signature>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct Signature {
pub keyid: Decoded<Hex>,
pub sig: Decoded<Hex>,
}
#[derive(Debug, Clone)]
pub enum KeyHolder {
Delegations(Delegations),
Root(Root),
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
#[serde(tag = "_type")]
#[serde(rename = "root")]
pub struct Root {
pub spec_version: String,
pub consistent_snapshot: bool,
pub version: NonZeroU64,
pub expires: DateTime<Utc>,
#[serde(deserialize_with = "de::deserialize_keys")]
pub keys: HashMap<Decoded<Hex>, Key>,
pub roles: HashMap<RoleType, RoleKeys>,
#[serde(flatten)]
#[serde(deserialize_with = "de::extra_skip_type")]
pub _extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct RoleKeys {
pub keyids: Vec<Decoded<Hex>>,
pub threshold: NonZeroU64,
#[serde(flatten)]
pub _extra: HashMap<String, Value>,
}
impl Root {
pub fn keys(&self, role: RoleType) -> impl Iterator<Item = &Key> {
KeysIter {
keyids_iter: match self.roles.get(&role) {
Some(role_keys) => role_keys.keyids.iter(),
None => [].iter(),
},
keys: &self.keys,
}
}
pub fn key_id(&self, key_pair: &dyn Sign) -> Option<Decoded<Hex>> {
for (key_id, key) in &self.keys {
if key_pair.tuf_key() == *key {
return Some(key_id.clone());
}
}
None
}
}
impl Role for Root {
const TYPE: RoleType = RoleType::Root;
fn expires(&self) -> DateTime<Utc> {
self.expires
}
fn version(&self) -> NonZeroU64 {
self.version
}
fn filename(&self, _consistent_snapshot: bool) -> String {
format!("{}.root.json", self.version())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
#[serde(tag = "_type")]
#[serde(rename = "snapshot")]
pub struct Snapshot {
pub spec_version: String,
pub version: NonZeroU64,
pub expires: DateTime<Utc>,
pub meta: HashMap<String, Metafile>,
#[serde(flatten)]
#[serde(deserialize_with = "de::extra_skip_type")]
pub _extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct Metafile {
#[serde(skip_serializing_if = "Option::is_none")]
pub length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hashes: Option<Hashes>,
pub version: NonZeroU64,
#[serde(flatten)]
pub _extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct Hashes {
pub sha256: Decoded<Hex>,
#[serde(flatten)]
pub _extra: HashMap<String, Value>,
}
impl Snapshot {
pub fn new(spec_version: String, version: NonZeroU64, expires: DateTime<Utc>) -> Self {
Snapshot {
spec_version,
version,
expires,
meta: HashMap::new(),
_extra: HashMap::new(),
}
}
}
impl Role for Snapshot {
const TYPE: RoleType = RoleType::Snapshot;
fn expires(&self) -> DateTime<Utc> {
self.expires
}
fn version(&self) -> NonZeroU64 {
self.version
}
fn filename(&self, consistent_snapshot: bool) -> String {
if consistent_snapshot {
format!("{}.snapshot.json", self.version())
} else {
"snapshot.json".to_string()
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(tag = "_type")]
#[serde(rename = "targets")]
pub struct Targets {
pub spec_version: String,
pub version: NonZeroU64,
pub expires: DateTime<Utc>,
pub targets: HashMap<TargetName, Target>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delegations: Option<Delegations>,
#[serde(flatten)]
#[serde(deserialize_with = "de::extra_skip_type")]
pub _extra: HashMap<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
pub struct Target {
pub length: u64,
pub hashes: Hashes,
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub custom: HashMap<String, Value>,
#[serde(flatten)]
pub _extra: HashMap<String, Value>,
}
impl Target {
pub async fn from_path<P>(path: P) -> Result<Target>
where
P: AsRef<Path>,
{
let path = path.as_ref();
if !path.is_file() {
return error::TargetNotAFileSnafu { path }.fail();
}
let mut file = File::open(path)
.await
.context(error::FileOpenSnafu { path })?;
let mut digest = Context::new(&SHA256);
let mut buf = [0; 8 * 1024];
let mut length = 0;
loop {
match file
.read(&mut buf)
.await
.context(error::FileReadSnafu { path })?
{
0 => break,
n => {
digest.update(&buf[..n]);
length += n as u64;
}
}
}
Ok(Target {
length,
hashes: Hashes {
sha256: Decoded::from(digest.finish().as_ref().to_vec()),
_extra: HashMap::new(),
},
custom: HashMap::new(),
_extra: HashMap::new(),
})
}
}
impl Targets {
pub fn new(spec_version: String, version: NonZeroU64, expires: DateTime<Utc>) -> Self {
Targets {
spec_version,
version,
expires,
targets: HashMap::new(),
_extra: HashMap::new(),
delegations: Some(Delegations::new()),
}
}
pub fn find_target(&self, target_name: &TargetName, permissive: bool) -> Result<&Target> {
let mut visited: BTreeSet<String> = BTreeSet::new();
let mut terminated = false;
self.find_target_from_role(target_name, &mut visited, &mut terminated, permissive)
}
fn find_target_from_role(
&self,
target_name: &TargetName,
visited: &mut BTreeSet<String>,
terminated: &mut bool,
permissive: bool,
) -> Result<&Target> {
if let Some(target) = self.targets.get(target_name) {
return Ok(target);
}
if let Some(delegations) = &self.delegations {
for role in &delegations.roles {
if !role.paths.matches_target_name(target_name) || visited.contains(&role.name) {
continue;
}
visited.insert(role.name.clone());
if let Some(targets) = &role.targets {
if let Ok(target) = targets.signed.find_target_from_role(
target_name,
visited,
terminated,
permissive,
) {
return Ok(target);
}
if !permissive && *terminated {
break;
}
}
if role.terminating && !permissive {
*terminated = true;
break;
}
}
}
error::TargetNotFoundSnafu {
name: target_name.clone(),
}
.fail()
}
pub fn targets_map(&self) -> HashMap<TargetName, &Target> {
self.targets_iter()
.map(|(target_name, target)| (target_name.clone(), target))
.collect()
}
pub fn targets_iter(&self) -> impl Iterator<Item = (&TargetName, &Target)> + '_ {
let mut iter: Box<dyn Iterator<Item = (&TargetName, &Target)>> =
Box::new(self.targets.iter());
if let Some(delegations) = &self.delegations {
for role in &delegations.roles {
if let Some(targets) = &role.targets {
iter = Box::new(iter.chain(targets.signed.targets_iter()));
}
}
}
iter
}
pub fn clear_targets(&mut self) {
self.targets = HashMap::new();
if let Some(delegations) = &mut self.delegations {
for delegated_role in &mut delegations.roles {
if let Some(targets) = &mut delegated_role.targets {
targets.signed.clear_targets();
}
}
}
}
pub fn add_target(&mut self, name: TargetName, target: Target) {
self.targets.insert(name, target);
}
pub fn remove_target(&mut self, name: &TargetName) -> Option<Target> {
self.targets.remove(name)
}
pub fn delegated_targets(&self, name: &str) -> Result<&Signed<Targets>> {
self.delegated_role(name)?
.targets
.as_ref()
.ok_or(error::Error::NoTargets)
}
pub fn delegated_targets_mut(&mut self, name: &str) -> Result<&mut Signed<Targets>> {
self.delegated_role_mut(name)?
.targets
.as_mut()
.ok_or(error::Error::NoTargets)
}
pub fn delegated_role(&self, name: &str) -> Result<&DelegatedRole> {
for role in &self
.delegations
.as_ref()
.ok_or(error::Error::NoDelegations)?
.roles
{
if role.name == name {
return Ok(role);
} else if let Ok(role) = role
.targets
.as_ref()
.ok_or(error::Error::NoTargets)?
.signed
.delegated_role(name)
{
return Ok(role);
}
}
Err(error::Error::RoleNotFound {
name: name.to_string(),
})
}
pub fn delegated_role_mut(&mut self, name: &str) -> Result<&mut DelegatedRole> {
for role in &mut self
.delegations
.as_mut()
.ok_or(error::Error::NoDelegations)?
.roles
{
if role.name == name {
return Ok(role);
} else if let Ok(role) = role
.targets
.as_mut()
.ok_or(error::Error::NoTargets)?
.signed
.delegated_role_mut(name)
{
return Ok(role);
}
}
Err(error::Error::RoleNotFound {
name: name.to_string(),
})
}
pub fn role_names(&self) -> Vec<&String> {
let mut roles = Vec::new();
if let Some(delelegations) = &self.delegations {
for role in &delelegations.roles {
roles.push(&role.name);
if let Some(targets) = &role.targets {
roles.append(&mut targets.signed.role_names());
}
}
}
roles
}
pub fn parent_of(&self, name: &str) -> Result<&Delegations> {
if let Some(delegations) = &self.delegations {
for role in &delegations.roles {
if role.name == name {
return Ok(delegations);
}
if let Some(targets) = &role.targets {
if let Ok(delegation) = targets.signed.parent_of(name) {
return Ok(delegation);
}
}
}
}
Err(error::Error::RoleNotFound {
name: name.to_string(),
})
}
pub fn signed_delegated_targets(&self) -> Vec<Signed<DelegatedTargets>> {
let mut delegated_targets = Vec::new();
if let Some(delegations) = &self.delegations {
for role in &delegations.roles {
if let Some(targets) = &role.targets {
delegated_targets.push(targets.clone().delegated_targets(&role.name));
delegated_targets.extend(targets.signed.signed_delegated_targets());
}
}
}
delegated_targets
}
pub fn update_targets(&self, new_targets: &mut Signed<Targets>) -> Vec<String> {
let mut needed_roles = Vec::new();
if let Some(delegations) = &mut new_targets.signed.delegations {
for role in &mut delegations.roles {
if let Ok(targets) = self.delegated_targets(&role.name) {
role.targets = Some(targets.clone());
} else {
needed_roles.push(role.name.clone());
}
}
}
needed_roles
}
pub(crate) fn validate(&self) -> Result<()> {
for (target_name, _) in self.targets_iter() {
self.find_target(target_name, true)?;
}
Ok(())
}
}
impl Role for Targets {
const TYPE: RoleType = RoleType::Targets;
fn expires(&self) -> DateTime<Utc> {
self.expires
}
fn version(&self) -> NonZeroU64 {
self.version
}
fn filename(&self, consistent_snapshot: bool) -> String {
if consistent_snapshot {
format!("{}.targets.json", self.version())
} else {
"targets.json".to_string()
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct DelegatedTargets {
#[serde(skip)]
pub name: String,
#[serde(flatten)]
pub targets: Targets,
}
impl Deref for DelegatedTargets {
type Target = Targets;
fn deref(&self) -> &Targets {
&self.targets
}
}
impl DerefMut for DelegatedTargets {
fn deref_mut(&mut self) -> &mut Targets {
&mut self.targets
}
}
impl Role for DelegatedTargets {
const TYPE: RoleType = RoleType::DelegatedTargets;
fn expires(&self) -> DateTime<Utc> {
self.targets.expires
}
fn version(&self) -> NonZeroU64 {
self.targets.version
}
fn filename(&self, consistent_snapshot: bool) -> String {
if consistent_snapshot {
format!("{}.{}.json", self.version(), encode_filename(&self.name))
} else {
format!("{}.json", encode_filename(&self.name))
}
}
fn role_id(&self) -> RoleId {
if self.name == "targets" {
RoleId::StandardRole(RoleType::Targets)
} else {
RoleId::DelegatedRole(self.name.clone())
}
}
}
impl Signed<DelegatedTargets> {
pub fn targets(self) -> (String, Signed<Targets>) {
(
self.signed.name,
Signed {
signed: self.signed.targets,
signatures: self.signatures,
},
)
}
}
impl Signed<Targets> {
pub fn delegated_targets(self, name: &str) -> Signed<DelegatedTargets> {
Signed {
signed: DelegatedTargets {
name: name.to_string(),
targets: self.signed,
},
signatures: self.signatures,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
pub struct Delegations {
#[serde(deserialize_with = "de::deserialize_keys")]
pub keys: HashMap<Decoded<Hex>, Key>,
pub roles: Vec<DelegatedRole>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct DelegatedRole {
pub name: String,
pub keyids: Vec<Decoded<Hex>>,
pub threshold: NonZeroU64,
#[serde(flatten)]
pub paths: PathSet,
pub terminating: bool,
#[serde(skip)]
pub targets: Option<Signed<Targets>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum PathSet {
#[serde(rename = "paths")]
Paths(Vec<PathPattern>),
#[serde(rename = "path_hash_prefixes")]
PathHashPrefixes(Vec<PathHashPrefix>),
}
#[derive(Clone, Debug)]
pub struct PathPattern {
value: String,
glob: GlobMatcher,
}
impl PathPattern {
pub fn new<S: Into<String>>(value: S) -> Result<Self> {
let value = value.into();
let glob = Glob::new(&value)
.context(error::GlobSnafu { pattern: &value })?
.compile_matcher();
Ok(Self { value, glob })
}
pub fn value(&self) -> &str {
&self.value
}
fn matches_target_name(&self, target_name: &TargetName) -> bool {
self.glob.is_match(target_name.resolved())
}
}
impl FromStr for PathPattern {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
PathPattern::new(s)
}
}
impl PartialEq for PathPattern {
fn eq(&self, other: &Self) -> bool {
PartialEq::eq(&self.value, &other.value)
}
}
impl Serialize for PathPattern {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.value().as_ref())
}
}
impl<'de> Deserialize<'de> for PathPattern {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
PathPattern::new(s).map_err(|e| D::Error::custom(format!("{e}")))
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub struct PathHashPrefix(String);
impl PathHashPrefix {
pub fn new<S: Into<String>>(value: S) -> Result<Self> {
Ok(PathHashPrefix(value.into()))
}
pub fn value(&self) -> &str {
&self.0
}
fn matches_target_name(&self, target_name: &TargetName) -> bool {
let target_name_digest =
digest(&SHA256, target_name.resolved().as_bytes()).encode_hex::<String>();
target_name_digest.starts_with(self.value())
}
}
impl FromStr for PathHashPrefix {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
PathHashPrefix::new(s)
}
}
impl PathSet {
fn matches_target_name(&self, target_name: &TargetName) -> bool {
match self {
Self::Paths(paths) => {
for path in paths {
if path.matches_target_name(target_name) {
return true;
}
}
}
Self::PathHashPrefixes(path_prefixes) => {
for prefix in path_prefixes {
if prefix.matches_target_name(target_name) {
return true;
}
}
}
}
false
}
}
impl Delegations {
pub fn new() -> Self {
Delegations {
keys: HashMap::new(),
roles: Vec::new(),
}
}
pub fn target_is_delegated(&self, target: &TargetName) -> bool {
for role in &self.roles {
if role.paths.matches_target_name(target) {
return true;
}
}
false
}
pub fn key_id(&self, key_pair: &dyn Sign) -> Option<Decoded<Hex>> {
for (key_id, key) in &self.keys {
if key_pair.tuf_key() == *key {
return Some(key_id.clone());
}
}
None
}
}
impl DelegatedRole {
pub fn keys(&self) -> RoleKeys {
RoleKeys {
keyids: self.keyids.clone(),
threshold: self.threshold,
_extra: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
#[serde(tag = "_type")]
#[serde(rename = "timestamp")]
pub struct Timestamp {
pub spec_version: String,
pub version: NonZeroU64,
pub expires: DateTime<Utc>,
pub meta: HashMap<String, Metafile>,
#[serde(flatten)]
#[serde(deserialize_with = "de::extra_skip_type")]
pub _extra: HashMap<String, Value>,
}
impl Timestamp {
pub fn new(spec_version: String, version: NonZeroU64, expires: DateTime<Utc>) -> Self {
Timestamp {
spec_version,
version,
expires,
meta: HashMap::new(),
_extra: HashMap::new(),
}
}
}
impl Role for Timestamp {
const TYPE: RoleType = RoleType::Timestamp;
fn expires(&self) -> DateTime<Utc> {
self.expires
}
fn version(&self) -> NonZeroU64 {
self.version
}
fn filename(&self, _consistent_snapshot: bool) -> String {
"timestamp.json".to_string()
}
}
#[test]
fn targets_iter_and_map_test() {
use maplit::hashmap;
let nothing = Target {
length: 0,
hashes: Hashes {
sha256: [0u8].to_vec().into(),
_extra: HashMap::default(),
},
custom: HashMap::default(),
_extra: HashMap::default(),
};
let c_role = DelegatedRole {
name: "c-role".to_string(),
keyids: vec![],
threshold: NonZeroU64::new(1).unwrap(),
paths: PathSet::Paths(vec![PathPattern::new("*").unwrap()]),
terminating: false,
targets: Some(Signed {
signed: Targets {
spec_version: String::new(),
version: NonZeroU64::new(1).unwrap(),
expires: Utc::now(),
targets: hashmap! {
TargetName::new("c.txt").unwrap() => nothing.clone(),
},
delegations: None,
_extra: HashMap::default(),
},
signatures: vec![],
}),
};
let b_delegations = Delegations {
keys: HashMap::default(),
roles: vec![c_role],
};
let b_role = DelegatedRole {
name: "b-role".to_string(),
keyids: vec![],
threshold: NonZeroU64::new(1).unwrap(),
paths: PathSet::Paths(vec![PathPattern::new("*").unwrap()]),
terminating: false,
targets: Some(Signed {
signed: Targets {
spec_version: String::new(),
version: NonZeroU64::new(1).unwrap(),
expires: Utc::now(),
targets: hashmap! {
TargetName::new("b.txt").unwrap() => nothing.clone(),
},
delegations: Some(b_delegations),
_extra: HashMap::default(),
},
signatures: vec![],
}),
};
let a_delegations = Delegations {
keys: HashMap::default(),
roles: vec![b_role],
};
let a = Targets {
spec_version: String::new(),
version: NonZeroU64::new(1).unwrap(),
expires: Utc::now(),
targets: hashmap! {
TargetName::new("a.txt").unwrap() => nothing,
},
delegations: Some(a_delegations),
_extra: HashMap::default(),
};
assert!(a
.targets_iter()
.map(|(key, _)| key)
.any(|item| item.raw() == "a.txt"));
assert!(a
.targets_iter()
.map(|(key, _)| key)
.any(|item| item.raw() == "b.txt"));
assert!(a
.targets_iter()
.map(|(key, _)| key)
.any(|item| item.raw() == "c.txt"));
let map = a.targets_map();
assert!(map.contains_key(&TargetName::new("a.txt").unwrap()));
assert!(map.contains_key(&TargetName::new("b.txt").unwrap()));
assert!(map.contains_key(&TargetName::new("c.txt").unwrap()));
}