diff options
-rw-r--r-- | src/cmd/id.rs | 16 | ||||
-rw-r--r-- | src/cmd/id/init.rs | 3 | ||||
-rw-r--r-- | src/metadata/identity.rs | 92 |
3 files changed, 93 insertions, 18 deletions
diff --git a/src/cmd/id.rs b/src/cmd/id.rs index 57e79b0..9f1de7e 100644 --- a/src/cmd/id.rs +++ b/src/cmd/id.rs @@ -3,7 +3,6 @@ use std::{ collections::BTreeSet, - num::NonZeroUsize, path::PathBuf, }; @@ -134,7 +133,8 @@ pub fn identity_ref(id: Either<&IdentityId, &git2::Config>) -> cmd::Result<Refna #[derive(serde::Serialize, serde::Deserialize)] struct Editable { keys: metadata::KeySet<'static>, - threshold: NonZeroUsize, + #[serde(flatten)] + roles: metadata::identity::Roles, mirrors: BTreeSet<Url>, expires: Option<metadata::DateTime>, custom: metadata::Custom, @@ -144,7 +144,7 @@ impl From<metadata::Identity> for Editable { fn from( metadata::Identity { keys, - threshold, + roles, mirrors, expires, custom, @@ -153,7 +153,7 @@ impl From<metadata::Identity> for Editable { ) -> Self { Self { keys, - threshold, + roles, mirrors, expires, custom, @@ -167,19 +167,23 @@ impl TryFrom<Editable> for metadata::Identity { fn try_from( Editable { keys, - threshold, + roles, mirrors, expires, custom, }: Editable, ) -> Result<Self, Self::Error> { ensure!(!keys.is_empty(), "keys cannot be empty"); + ensure!( + !roles.is_threshold(), + "flat threshold is deprecated, please specify the root keys explicity" + ); Ok(Self { fmt_version: Default::default(), prev: None, keys, - threshold, + roles, mirrors, expires, custom, diff --git a/src/cmd/id/init.rs b/src/cmd/id/init.rs index 35d3bb8..f481f48 100644 --- a/src/cmd/id/init.rs +++ b/src/cmd/id/init.rs @@ -145,13 +145,14 @@ pub fn init(args: Init) -> cmd::Result<Output> { .map(metadata::Key::from) .chain(args.public) .collect::<KeySet>(); + let roles = metadata::identity::Roles::root(keys.keys().cloned().collect(), threshold); let meta = { let id = metadata::Identity { fmt_version: Default::default(), prev: None, keys, - threshold, + roles, mirrors: args.mirrors.into_iter().collect(), expires: args.expires, custom, diff --git a/src/metadata/identity.rs b/src/metadata/identity.rs index 15967c4..9e17213 100644 --- a/src/metadata/identity.rs +++ b/src/metadata/identity.rs @@ -56,7 +56,7 @@ use crate::{ metadata::git::find_parent, }; -pub const FMT_VERSION: FmtVersion = FmtVersion(super::FmtVersion::new(0, 2, 0)); +pub const FMT_VERSION: FmtVersion = FmtVersion(super::FmtVersion::new(1, 0, 0)); #[derive(Clone, Eq, Ord, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)] pub struct FmtVersion(super::FmtVersion); @@ -154,13 +154,42 @@ impl AsRef<Identity> for Verified { } } +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Roles { + /// Legacy + Threshold(NonZeroUsize), + Roles { + root: Role, + }, +} + +impl Roles { + pub fn root(keys: BTreeSet<KeyId>, threshold: NonZeroUsize) -> Self { + Self::Roles { + root: Role { keys, threshold }, + } + } + + pub fn is_threshold(&self) -> bool { + matches!(self, Self::Threshold(_)) + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Role { + pub keys: BTreeSet<KeyId>, + pub threshold: NonZeroUsize, +} + #[derive(Clone, serde::Deserialize)] pub struct Identity { #[serde(alias = "spec_version")] pub fmt_version: FmtVersion, pub prev: Option<ContentHash>, pub keys: KeySet<'static>, - pub threshold: NonZeroUsize, + #[serde(flatten)] + pub roles: Roles, pub mirrors: BTreeSet<Url>, pub expires: Option<DateTime>, #[serde(default)] @@ -214,14 +243,9 @@ impl Identity { let canonical = self.canonicalise()?; let signed = Sha512::digest(&canonical); - verify_signatures(&signed, self.threshold, signatures.iter(), &self.keys)?; + self.verify_signatures(signatures.iter(), &signed)?; if let Some(prev) = self.prev.as_ref().map(&mut find_prev).transpose()? { - verify_signatures( - &signed, - prev.signed.threshold, - signatures.iter(), - &prev.signed.keys, - )?; + prev.signed.verify_signatures(signatures.iter(), &signed)?; return prev .signed .verify_tail(Cow::Owned(prev.signatures), find_prev); @@ -230,6 +254,39 @@ impl Identity { Ok(IdentityId(Sha256::digest(canonical).into())) } + fn verify_signatures<'a, I>( + &self, + signatures: I, + payload: &[u8], + ) -> Result<(), error::Verification> + where + I: IntoIterator<Item = (&'a KeyId, &'a Signature)>, + { + match &self.roles { + Roles::Threshold(threshold) => { + verify_signatures(payload, *threshold, signatures, &self.keys)?; + }, + Roles::Roles { + root: Role { keys, threshold }, + } => { + let root_keys = self + .keys + .iter() + .filter_map(|(id, key)| { + if keys.contains(id) { + Some((id.clone(), key.clone())) + } else { + None + } + }) + .collect(); + verify_signatures(payload, *threshold, signatures, &root_keys)?; + }, + } + + Ok(()) + } + pub fn canonicalise(&self) -> Result<Vec<u8>, canonical::error::Canonicalise> { canonical::to_vec(Metadata::identity(self)) } @@ -286,19 +343,32 @@ impl serde::Serialize for Identity { { use serde::ser::SerializeStruct; + const HAVE_FMT_VERSION: FmtVersion = FmtVersion(super::FmtVersion::new(0, 2, 0)); + let mut s = serializer.serialize_struct("Identity", 7)?; - let version_field = if self.fmt_version < FMT_VERSION { + let version_field = if self.fmt_version < HAVE_FMT_VERSION { "spec_version" } else { "fmt_version" }; + s.serialize_field(version_field, &self.fmt_version)?; s.serialize_field("prev", &self.prev)?; s.serialize_field("keys", &self.keys)?; - s.serialize_field("threshold", &self.threshold)?; + match &self.roles { + Roles::Threshold(t) => s.serialize_field("threshold", t)?, + Roles::Roles { root } => { + #[derive(serde::Serialize)] + struct Roles<'a> { + root: &'a Role, + } + s.serialize_field("roles", &Roles { root })? + }, + } s.serialize_field("mirrors", &self.mirrors)?; s.serialize_field("expires", &self.expires)?; s.serialize_field("custom", &self.custom)?; + s.end() } } |