diff options
Diffstat (limited to 'src/metadata')
-rw-r--r-- | src/metadata/drop.rs | 274 | ||||
-rw-r--r-- | src/metadata/error.rs | 40 | ||||
-rw-r--r-- | src/metadata/git.rs | 232 | ||||
-rw-r--r-- | src/metadata/identity.rs | 366 | ||||
-rw-r--r-- | src/metadata/mirrors.rs | 95 |
5 files changed, 1007 insertions, 0 deletions
diff --git a/src/metadata/drop.rs b/src/metadata/drop.rs new file mode 100644 index 0000000..d231712 --- /dev/null +++ b/src/metadata/drop.rs @@ -0,0 +1,274 @@ +// Copyright © 2022 Kim Altintop <kim@eagain.io> +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::{ + borrow::Cow, + collections::{ + BTreeMap, + BTreeSet, + HashMap, + }, + io, + num::NonZeroUsize, +}; + +use log::warn; +use sha2::{ + Digest, + Sha512, +}; +use signature::Verifier; + +use super::{ + error, + Alternates, + ContentHash, + Custom, + DateTime, + IdentityId, + KeyId, + KeySet, + Metadata, + Mirrors, + Signature, + Signed, + SpecVersion, +}; +use crate::{ + git::Refname, + json::canonical, + str::Varchar, +}; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Roles { + pub root: Role, + pub snapshot: Role, + pub mirrors: Role, + pub branches: HashMap<Refname, Annotated>, +} + +impl Roles { + pub(crate) fn ids(&self) -> BTreeSet<IdentityId> { + let Self { + root: Role { ids: root, .. }, + snapshot: Role { ids: snapshot, .. }, + mirrors: Role { ids: mirrors, .. }, + branches, + } = self; + + let mut ids = BTreeSet::new(); + ids.extend(root); + ids.extend(snapshot); + ids.extend(mirrors); + ids.extend(branches.values().flat_map(|a| &a.role.ids)); + ids + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Role { + pub ids: BTreeSet<IdentityId>, + pub threshold: NonZeroUsize, +} + +pub type Description = Varchar<String, 128>; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Annotated { + #[serde(flatten)] + pub role: Role, + pub description: Description, +} + +pub type Verified = super::Verified<Drop>; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Drop { + pub spec_version: SpecVersion, + #[serde(default = "Description::new")] + pub description: Description, + pub prev: Option<ContentHash>, + pub roles: Roles, + #[serde(default)] + pub custom: Custom, +} + +impl Drop { + pub fn verified<'a, F, G>( + self, + signatures: &BTreeMap<KeyId, Signature>, + find_prev: F, + find_signer: G, + ) -> Result<Verified, error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + G: FnMut(&IdentityId) -> io::Result<KeySet<'a>>, + { + self.verify(signatures, find_prev, find_signer)?; + Ok(super::Verified(self)) + } + + pub fn verify<'a, F, G>( + &self, + signatures: &BTreeMap<KeyId, Signature>, + mut find_prev: F, + mut find_signer: G, + ) -> Result<(), error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + G: FnMut(&IdentityId) -> io::Result<KeySet<'a>>, + { + use error::Verification::*; + + if !crate::SPEC_VERSION.is_compatible(&self.spec_version) { + return Err(IncompatibleSpecVersion); + } + + let canonical = self.canonicalise()?; + let payload = Sha512::digest(&canonical); + verify::AuthorisedSigners::from_ids(&self.roles.root.ids, &mut find_signer)? + .verify_signatures(&payload, self.roles.root.threshold, signatures)?; + if let Some(prev) = self.prev.as_ref().map(&mut find_prev).transpose()? { + verify::AuthorisedSigners::from_ids(&prev.signed.roles.root.ids, &mut find_signer)? + .verify_signatures(&payload, prev.signed.roles.root.threshold, signatures)?; + return prev.signed.verify(&prev.signatures, find_prev, find_signer); + } + + Ok(()) + } + + pub fn verify_mirrors<'a, F>( + &self, + mirrors: &Signed<Mirrors>, + find_signer: F, + ) -> Result<(), error::Verification> + where + F: FnMut(&IdentityId) -> io::Result<KeySet<'a>>, + { + use error::Verification::*; + + if let Some(deadline) = &mirrors.signed.expires { + if deadline < &DateTime::now() { + return Err(Expired); + } + } + if !crate::SPEC_VERSION.is_compatible(&mirrors.signed.spec_version) { + return Err(IncompatibleSpecVersion); + } + + let payload = Sha512::digest(mirrors.signed.canonicalise()?); + verify::AuthorisedSigners::from_ids(&self.roles.mirrors.ids, find_signer)? + .verify_signatures(&payload, self.roles.mirrors.threshold, &mirrors.signatures) + } + + pub fn verify_alternates<'a, F>( + &self, + alt: &Signed<Alternates>, + find_signer: F, + ) -> Result<(), error::Verification> + where + F: FnMut(&IdentityId) -> io::Result<KeySet<'a>>, + { + use error::Verification::*; + + if let Some(deadline) = &alt.signed.expires { + if deadline < &DateTime::now() { + return Err(Expired); + } + } + if !crate::SPEC_VERSION.is_compatible(&alt.signed.spec_version) { + return Err(IncompatibleSpecVersion); + } + + let payload = Sha512::digest(alt.signed.canonicalise()?); + verify::AuthorisedSigners::from_ids(&self.roles.mirrors.ids, find_signer)? + .verify_signatures(&payload, self.roles.mirrors.threshold, &alt.signatures) + } + + pub fn canonicalise(&self) -> Result<Vec<u8>, canonical::error::Canonicalise> { + canonical::to_vec(Metadata::drop(self)) + } +} + +impl From<Drop> for Cow<'static, Drop> { + fn from(d: Drop) -> Self { + Self::Owned(d) + } +} + +impl<'a> From<&'a Drop> for Cow<'a, Drop> { + fn from(d: &'a Drop) -> Self { + Self::Borrowed(d) + } +} + +mod verify { + use super::*; + + pub struct AuthorisedSigners<'a, 'b>(BTreeMap<&'a IdentityId, KeySet<'b>>); + + impl<'a, 'b> AuthorisedSigners<'a, 'b> { + pub fn from_ids<F>( + ids: &'a BTreeSet<IdentityId>, + mut find_signer: F, + ) -> Result<AuthorisedSigners<'a, 'b>, error::Verification> + where + F: FnMut(&IdentityId) -> io::Result<KeySet<'b>>, + { + let mut signers = BTreeMap::new(); + for id in ids { + signers.insert(id, find_signer(id)?); + } + signers + .values() + .try_fold(BTreeSet::new(), |mut all_keys, keys| { + for key in keys.keys() { + if !all_keys.insert(key) { + return Err(error::Verification::DuplicateKey(*key)); + } + } + + Ok(all_keys) + })?; + + Ok(Self(signers)) + } + + pub fn verify_signatures<'c, S>( + &mut self, + payload: &[u8], + threshold: NonZeroUsize, + signatures: S, + ) -> Result<(), error::Verification> + where + S: IntoIterator<Item = (&'c KeyId, &'c Signature)>, + { + use error::Verification::SignatureThreshold; + + let mut need_signatures = threshold.get(); + for (key_id, signature) in signatures { + if let Some(sig_id) = self.0.iter().find_map(|(id, keys)| { + #[allow(clippy::unnecessary_lazy_evaluations)] + keys.contains_key(key_id).then(|| *id) + }) { + let key = self.0.remove(sig_id).unwrap().remove(key_id).unwrap(); + if key.verify(payload, signature).is_ok() { + need_signatures -= 1; + } else { + warn!("Bad signature by {key_id}"); + } + + if need_signatures == 0 { + break; + } + } + } + if need_signatures > 0 { + return Err(SignatureThreshold); + } + + Ok(()) + } + } +} diff --git a/src/metadata/error.rs b/src/metadata/error.rs new file mode 100644 index 0000000..66173f9 --- /dev/null +++ b/src/metadata/error.rs @@ -0,0 +1,40 @@ +// Copyright © 2022 Kim Altintop <kim@eagain.io> +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::io; + +use thiserror::Error; + +use super::KeyId; +use crate::json::canonical::error::Canonicalise; + +#[derive(Debug, Error)] +pub enum SigId { + #[error("payload not at root revision")] + NotRoot, + + #[error("invalid payload: canonicalisation failed")] + Canonical(#[from] Canonicalise), +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Verification { + #[error("incompatible spec version")] + IncompatibleSpecVersion, + + #[error("canonicalisation failed")] + Canonicalise(#[from] Canonicalise), + + #[error("required signature threshold not met")] + SignatureThreshold, + + #[error("metadata past its expiry date")] + Expired, + + #[error("duplicate key: key {0} appears in more than one identity")] + DuplicateKey(KeyId), + + #[error(transparent)] + Io(#[from] io::Error), +} diff --git a/src/metadata/git.rs b/src/metadata/git.rs new file mode 100644 index 0000000..1dde3da --- /dev/null +++ b/src/metadata/git.rs @@ -0,0 +1,232 @@ +// Copyright © 2022 Kim Altintop <kim@eagain.io> +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::{ + borrow::Cow, + io, +}; + +use anyhow::anyhow; + +use super::{ + drop, + identity, + Alternates, + ContentHash, + Drop, + Identity, + IdentityId, + KeySet, + Metadata, + Mirrors, + Signed, +}; +use crate::{ + cmd, + git::if_not_found_none, + json, +}; + +pub const META_FILE_ALTERNATES: &str = "alternates.json"; +pub const META_FILE_DROP: &str = "drop.json"; +pub const META_FILE_ID: &str = "id.json"; +pub const META_FILE_MIRRORS: &str = "mirrors.json"; + +pub mod error { + use thiserror::Error; + + #[derive(Debug, Error)] + #[error("unexpected metadata type")] + pub struct TypeMismatch; + + #[derive(Debug, Error)] + #[error("{file} not found in tree")] + pub struct FileNotFound { + pub file: &'static str, + } +} + +pub struct GitMeta<T> { + pub hash: ContentHash, + pub signed: Signed<T>, +} + +pub type GitIdentity = GitMeta<Identity>; +pub type GitDrop = GitMeta<Drop>; +pub type GitMirrors = GitMeta<Mirrors>; +pub type GitAlternates = GitMeta<Alternates>; + +impl GitMeta<Drop> { + pub fn verified<'a, F, G>( + self, + find_prev: F, + find_signer: G, + ) -> Result<drop::Verified, super::error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Drop>>, + G: FnMut(&IdentityId) -> io::Result<KeySet<'a>>, + { + self.signed.verified(find_prev, find_signer) + } +} + +impl GitMeta<Identity> { + pub fn verified<F>(self, find_prev: F) -> Result<identity::Verified, super::error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Identity>>, + { + self.signed.verified(find_prev) + } +} + +pub struct FromSearchPath<'a, T> { + /// The repository (from the search path) the object was found in + pub repo: &'a git2::Repository, + pub meta: GitMeta<T>, +} + +pub trait FromGit: Sized + Clone +where + for<'a> Cow<'a, Self>: TryFrom<Metadata<'a>>, +{ + const METADATA_JSON: &'static str; + + fn from_blob(blob: &git2::Blob) -> crate::Result<GitMeta<Self>> { + let hash = ContentHash::from(blob); + let signed = json::from_blob::<Signed<Metadata>>(blob)? + .fmap(Cow::<Self>::try_from) + .transpose() + .map_err(|_| error::TypeMismatch)? + .fmap(Cow::into_owned); + + Ok(GitMeta { hash, signed }) + } + + fn from_tip<R: AsRef<str>>( + repo: &git2::Repository, + refname: R, + ) -> crate::Result<GitMeta<Self>> { + Self::from_reference(repo, &repo.find_reference(refname.as_ref())?) + } + + fn from_reference( + repo: &git2::Repository, + reference: &git2::Reference, + ) -> crate::Result<GitMeta<Self>> { + Self::from_commit(repo, &reference.peel_to_commit()?) + } + + fn from_commit(repo: &git2::Repository, commit: &git2::Commit) -> crate::Result<GitMeta<Self>> { + Self::from_tree(repo, &commit.tree()?) + } + + fn from_tree(repo: &git2::Repository, tree: &git2::Tree) -> crate::Result<GitMeta<Self>> { + let entry = tree + .get_name(Self::METADATA_JSON) + .ok_or(error::FileNotFound { + file: Self::METADATA_JSON, + })?; + let blob = entry.to_object(repo)?.peel_to_blob()?; + + Self::from_blob(&blob) + } + + fn from_content_hash( + repo: &git2::Repository, + hash: &ContentHash, + ) -> crate::Result<GitMeta<Self>> { + let blob = repo.find_blob(hash.into())?; + Self::from_blob(&blob) + } + + fn from_search_path<R: AsRef<str>>( + search_path: &[git2::Repository], + refname: R, + ) -> crate::Result<FromSearchPath<Self>> { + let (repo, reference) = find_ref_in_path(search_path, refname.as_ref())? + .ok_or_else(|| anyhow!("{} not found in search path", refname.as_ref()))?; + Self::from_reference(repo, &reference).map(|meta| FromSearchPath { repo, meta }) + } +} + +impl FromGit for Identity { + const METADATA_JSON: &'static str = META_FILE_ID; +} + +impl FromGit for Drop { + const METADATA_JSON: &'static str = META_FILE_DROP; +} + +impl FromGit for Mirrors { + const METADATA_JSON: &'static str = META_FILE_MIRRORS; +} + +impl FromGit for Alternates { + const METADATA_JSON: &'static str = META_FILE_ALTERNATES; +} + +pub fn find_parent<T>( + repo: &git2::Repository, +) -> impl Fn(&ContentHash) -> io::Result<Signed<T>> + '_ +where + T: FromGit, + for<'a> Cow<'a, T>: TryFrom<Metadata<'a>>, +{ + |hash| { + T::from_content_hash(repo, hash) + .map_err(as_io) + .map(|meta| meta.signed) + } +} + +pub fn find_parent_in_tree<'a, T>( + repo: &'a git2::Repository, + tree: &'a git2::Tree<'a>, +) -> impl Fn(&ContentHash) -> io::Result<Signed<T>> + 'a +where + T: FromGit, + for<'b> Cow<'b, T>: TryFrom<Metadata<'b>>, +{ + fn go<T>( + repo: &git2::Repository, + tree: &git2::Tree, + hash: &ContentHash, + ) -> crate::Result<Signed<T>> + where + T: FromGit, + for<'b> Cow<'b, T>: TryFrom<Metadata<'b>>, + { + let oid = git2::Oid::from(hash); + let blob = tree + .get_id(oid) + .ok_or_else(|| anyhow!("parent {} not found in tree {}", oid, tree.id()))? + .to_object(repo)? + .into_blob() + .map_err(|_| anyhow!("parent {} is not a file", oid))?; + + T::from_blob(&blob).map(|meta| meta.signed) + } + + move |hash| go(repo, tree, hash).map_err(as_io) +} + +pub fn find_ref_in_path<'a>( + search_path: &'a [git2::Repository], + name: &str, +) -> cmd::Result<Option<(&'a git2::Repository, git2::Reference<'a>)>> { + for repo in search_path { + let have_ref = if_not_found_none(repo.resolve_reference_from_short_name(name))?; + if let Some(r) = have_ref { + return Ok(Some((repo, r))); + } + } + + Ok(None) +} + +fn as_io<E>(e: E) -> io::Error +where + E: Into<Box<dyn std::error::Error + Send + Sync>>, +{ + io::Error::new(io::ErrorKind::Other, e) +} diff --git a/src/metadata/identity.rs b/src/metadata/identity.rs new file mode 100644 index 0000000..8071e84 --- /dev/null +++ b/src/metadata/identity.rs @@ -0,0 +1,366 @@ +// Copyright © 2022 Kim Altintop <kim@eagain.io> +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::{ + borrow::Cow, + collections::{ + BTreeMap, + BTreeSet, + }, + fmt, + io, + marker::PhantomData, + num::NonZeroUsize, + path::PathBuf, + str::FromStr, +}; + +use anyhow::{ + anyhow, + ensure, +}; +use hex::FromHex; +use log::warn; +use sha2::{ + Digest, + Sha256, + Sha512, +}; +use signature::Verifier; +use url::Url; + +use super::{ + error, + git::{ + find_parent_in_tree, + FromGit, + META_FILE_ID, + }, + Ancestors, + ContentHash, + Custom, + DateTime, + Key, + KeyId, + KeySet, + Metadata, + Signature, + Signed, + SpecVersion, +}; +use crate::{ + json::{ + self, + canonical, + }, + metadata::git::find_parent, +}; + +#[derive( + Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, serde::Serialize, serde::Deserialize, +)] +pub struct IdentityId(#[serde(with = "hex::serde")] [u8; 32]); + +impl TryFrom<&Identity> for IdentityId { + type Error = error::SigId; + + fn try_from(id: &Identity) -> Result<Self, Self::Error> { + if id.prev.is_some() { + return Err(error::SigId::NotRoot); + } + let digest = Sha256::digest(id.canonicalise()?); + Ok(Self(digest.into())) + } +} + +impl fmt::Display for IdentityId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&hex::encode(self.0)) + } +} + +impl fmt::Debug for IdentityId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_string()) + } +} + +impl FromStr for IdentityId { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + FromHex::from_hex(s).map(Self) + } +} + +impl TryFrom<String> for IdentityId { + type Error = hex::FromHexError; + + fn try_from(value: String) -> Result<Self, Self::Error> { + FromHex::from_hex(value).map(Self) + } +} + +pub struct Verified { + id: IdentityId, + cur: Identity, +} + +impl Verified { + pub fn id(&self) -> &IdentityId { + &self.id + } + + pub fn identity(&self) -> &Identity { + &self.cur + } + + pub fn into_parts(self) -> (IdentityId, Identity) { + (self.id, self.cur) + } + + /// `true` if signature is valid over message for any of the signer's + /// _current_ set of keys + pub fn did_sign<T: AsRef<[u8]>>(&self, msg: T, sig: &Signature) -> bool { + self.cur + .keys + .values() + .any(|key| key.verify(msg.as_ref(), sig).is_ok()) + } +} + +impl AsRef<Identity> for Verified { + fn as_ref(&self) -> &Identity { + self.identity() + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Identity { + pub spec_version: SpecVersion, + pub prev: Option<ContentHash>, + pub keys: KeySet<'static>, + pub threshold: NonZeroUsize, + pub mirrors: BTreeSet<Url>, + pub expires: Option<DateTime>, + #[serde(default)] + pub custom: Custom, +} + +impl Identity { + pub fn verified<F>( + self, + signatures: &BTreeMap<KeyId, Signature>, + find_prev: F, + ) -> Result<Verified, error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + { + let id = self.verify(signatures, find_prev)?; + Ok(Verified { id, cur: self }) + } + + pub fn verify<F>( + &self, + signatures: &BTreeMap<KeyId, Signature>, + find_prev: F, + ) -> Result<IdentityId, error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + { + use error::Verification::Expired; + + if let Some(deadline) = &self.expires { + if deadline < &DateTime::now() { + return Err(Expired); + } + } + self.verify_tail(Cow::Borrowed(signatures), find_prev) + } + + fn verify_tail<F>( + &self, + signatures: Cow<BTreeMap<KeyId, Signature>>, + mut find_prev: F, + ) -> Result<IdentityId, error::Verification> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + { + use error::Verification::IncompatibleSpecVersion; + + if !crate::SPEC_VERSION.is_compatible(&self.spec_version) { + return Err(IncompatibleSpecVersion); + } + + let canonical = self.canonicalise()?; + let signed = Sha512::digest(&canonical); + verify_signatures(&signed, self.threshold, signatures.iter(), &self.keys)?; + if let Some(prev) = self.prev.as_ref().map(&mut find_prev).transpose()? { + verify_signatures( + &signed, + prev.signed.threshold, + signatures.iter(), + &prev.signed.keys, + )?; + return prev + .signed + .verify_tail(Cow::Owned(prev.signatures), find_prev); + } + + Ok(IdentityId(Sha256::digest(canonical).into())) + } + + pub fn canonicalise(&self) -> Result<Vec<u8>, canonical::error::Canonicalise> { + canonical::to_vec(Metadata::identity(self)) + } + + pub fn ancestors<F>(&self, find_prev: F) -> impl Iterator<Item = io::Result<Signed<Self>>> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + { + Ancestors { + prev: self.prev.clone(), + find_prev, + _marker: PhantomData, + } + } + + pub fn has_ancestor<F>(&self, ancestor: &ContentHash, find_prev: F) -> io::Result<bool> + where + F: FnMut(&ContentHash) -> io::Result<Signed<Self>>, + { + match &self.prev { + None => Ok(false), + Some(parent) if parent == ancestor => Ok(true), + Some(_) => { + for prev in self.ancestors(find_prev) { + match &prev?.signed.prev { + None => return Ok(false), + Some(parent) if parent == ancestor => return Ok(true), + _ => continue, + } + } + + Ok(false) + }, + } + } +} + +impl From<Identity> for Cow<'static, Identity> { + fn from(s: Identity) -> Self { + Self::Owned(s) + } +} + +impl<'a> From<&'a Identity> for Cow<'a, Identity> { + fn from(s: &'a Identity) -> Self { + Self::Borrowed(s) + } +} + +fn verify_signatures<'a, S>( + payload: &[u8], + threshold: NonZeroUsize, + signatures: S, + keys: &BTreeMap<KeyId, Key>, +) -> Result<(), error::Verification> +where + S: IntoIterator<Item = (&'a KeyId, &'a Signature)>, +{ + use error::Verification::SignatureThreshold; + + let mut need_signatures = threshold.get(); + for (key_id, signature) in signatures { + if let Some(key) = keys.get(key_id) { + if key.verify(payload, signature).is_ok() { + need_signatures -= 1; + } else { + warn!("Bad signature by {key_id}"); + } + + if need_signatures == 0 { + break; + } + } + } + if need_signatures > 0 { + return Err(SignatureThreshold); + } + + Ok(()) +} + +const FOLDED_HISTORY: &str = ".history"; + +pub fn fold_to_tree<'a>( + repo: &'a git2::Repository, + tree: &mut git2::TreeBuilder<'a>, + Signed { signed, signatures }: Signed<Identity>, +) -> crate::Result<()> { + use git2::FileMode::{ + Blob, + Tree, + }; + + let meta = Signed { + signed: Metadata::from(&signed), + signatures, + }; + tree.insert(META_FILE_ID, json::to_blob(repo, &meta)?, Blob.into())?; + + let mut history = { + let existing = tree + .get(FOLDED_HISTORY)? + .map(|t| t.to_object(repo)) + .transpose()?; + repo.treebuilder(existing.as_ref().and_then(git2::Object::as_tree))? + }; + let mut parents = Vec::new(); + for parent in signed.ancestors(find_parent(repo)) { + let meta = parent?.fmap(Metadata::from); + let blob = json::to_blob(repo, &meta)?; + parents.push(blob); + } + for (n, oid) in parents.into_iter().rev().enumerate() { + history.insert(&format!("{n}.json"), oid, Blob.into())?; + } + tree.insert(FOLDED_HISTORY, history.write()?, Tree.into())?; + + Ok(()) +} + +pub fn find_in_tree( + repo: &git2::Repository, + root: &git2::Tree, + id: &IdentityId, +) -> crate::Result<Verified> { + let (id_path, hist_path) = { + let base = PathBuf::from(id.to_string()); + (base.join(META_FILE_ID), base.join(FOLDED_HISTORY)) + }; + + let blob = root + .get_path(&id_path)? + .to_object(repo)? + .into_blob() + .map_err(|_| anyhow!("{} is not a file", id_path.display()))?; + let meta = Identity::from_blob(&blob)?.signed; + let hist = root + .get_path(&hist_path)? + .to_object(repo)? + .into_tree() + .map_err(|_| anyhow!("{} is not a directory", hist_path.display()))?; + + let verified = meta + .signed + .verified(&meta.signatures, find_parent_in_tree(repo, &hist))?; + ensure!( + verified.id() == id, + "ids don't match after verification: expected {} found {}", + id, + verified.id() + ); + + Ok(verified) +} diff --git a/src/metadata/mirrors.rs b/src/metadata/mirrors.rs new file mode 100644 index 0000000..9124dd3 --- /dev/null +++ b/src/metadata/mirrors.rs @@ -0,0 +1,95 @@ +// Copyright © 2022 Kim Altintop <kim@eagain.io> +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::{ + borrow::Cow, + collections::BTreeSet, +}; + +use url::Url; + +use super::{ + Custom, + DateTime, + Metadata, + SpecVersion, +}; +use crate::{ + json::canonical, + str::Varchar, +}; + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct Mirror { + pub url: Url, + #[serde(default)] + pub kind: Kind, + #[serde(default)] + pub custom: Custom, +} + +#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Kind { + /// Can fetch bundles + Bundled, + /// Can fetch packs via git-protocol + #[default] + Packed, + /// Not serving bundles at all + Sparse, + /// Unknown kind + Unknown(Varchar<String, 16>), +} + +#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct Mirrors { + pub spec_version: SpecVersion, + pub mirrors: Vec<Mirror>, + pub expires: Option<DateTime>, +} + +impl Mirrors { + pub fn canonicalise(&self) -> Result<Vec<u8>, canonical::error::Canonicalise> { + canonical::to_vec(Metadata::mirrors(self)) + } +} + +impl From<Mirrors> for Cow<'static, Mirrors> { + fn from(m: Mirrors) -> Self { + Self::Owned(m) + } +} + +impl<'a> From<&'a Mirrors> for Cow<'a, Mirrors> { + fn from(m: &'a Mirrors) -> Self { + Self::Borrowed(m) + } +} + +#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct Alternates { + pub spec_version: SpecVersion, + pub alternates: BTreeSet<Url>, + #[serde(default)] + pub custom: Custom, + pub expires: Option<DateTime>, +} + +impl Alternates { + pub fn canonicalise(&self) -> Result<Vec<u8>, canonical::error::Canonicalise> { + canonical::to_vec(Metadata::alternates(self)) + } +} + +impl From<Alternates> for Cow<'static, Alternates> { + fn from(a: Alternates) -> Self { + Self::Owned(a) + } +} + +impl<'a> From<&'a Alternates> for Cow<'a, Alternates> { + fn from(a: &'a Alternates) -> Self { + Self::Borrowed(a) + } +} |