summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKim Altintop <kim@eagain.io>2023-03-29 18:01:19 +0200
committerKim Altintop <kim@eagain.io>2023-03-29 18:01:19 +0200
commit168401644e0569ad25aec2e35a589fa73acf59f7 (patch)
treeb5e4e2c6089fd62adc4bc7ef94e3c5a71f1a9dfd
parenta273c0fa68a65b88878b0f568e49042a91b44350 (diff)
core: explicit root keys for identity
Replace threshold on identities with a roles dictionary, where the only currently supported role is "root". Keys in the root role are eligible for identity document updates. This allows users to restrict identity edits to keys on secure storage, while still permitting signatures from weaker protected keys for other purposes. Signed-off-by: Kim Altintop <kim@eagain.io>
-rw-r--r--src/cmd/id.rs16
-rw-r--r--src/cmd/id/init.rs3
-rw-r--r--src/metadata/identity.rs92
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()
}
}