//! Trust policy management and enforcement (spec ยง14.5). //! //! A repository with a policy verifies everything it receives: every change //! must be authenticated by a key the policy authorizes for the bookmark, //! every bookmark update must carry an attestation by such a key, and a new //! policy must be signed by a threshold of the roots of the policy it replaces. use super::*; use crate::policy::{Policy, Require, Rule}; use crate::protocol::SignedPolicy; /// One year, the default policy lifetime. const POLICY_LIFETIME_MS: u64 = 365 * 24 * 60 * 60 * 1000; impl Repo { /// The current policy and its signatures, if the repository has one. pub fn current_policy(&self) -> Result> { let Some(r) = self.view()?.policy else { return Ok(None) }; let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?; Ok(Some((r, SignedPolicy { policy, sigs: self.store.signatures(&r)? }))) } /// Store a policy with its signatures and point the `policy` reference at it. pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result { let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?; for sig in &sp.sigs { self.store.put_signature(&r, sig)?; } for id in &sp.policy.identities { self.store.put_identity(id)?; } let (op_ref, op) = self.head_op()?; if op.view.policy != Some(r) { let mut view = op.view.clone(); view.policy = Some(r); self.record_op(&[op_ref], command, view)?; } Ok(r) } /// Validate an incoming policy against the one we trust (`None` when /// bootstrapping: the policy then vouches for itself). pub fn check_policy(&self, sp: &SignedPolicy, trusted: Option<&Policy>) -> std::result::Result<(), String> { if sp.policy.repo != self.id().map_err(|e| e.to_string())? { return Err("policy is for a different repository".into()); } if let Some(t) = trusted { if sp.policy.serial <= t.serial { return Err(format!("policy serial {} is not greater than the current {}", sp.policy.serial, t.serial)); } } sp.policy.verify_signatures(&sp.sigs, trusted.unwrap_or(&sp.policy))?; Ok(()) } fn sign_policy(&self, policy: Policy) -> Result { let Some((key, sk)) = self.secret_key()? else { return fail("no secret key: run `oxy identity create` first"); }; if !policy.roots.contains(&key) { return fail("your key is not a root key of this policy"); } if policy.root_threshold > 1 { return fail(format!("this policy needs {} root signatures; multi-signer updates are not implemented yet", policy.root_threshold)); } let sig = sk.sign(Domain::Policy, &key, &policy.hash()); Ok(SignedPolicy { policy, sigs: vec![sig] }) } /// Create the initial policy with the configured identity as sole root. pub fn policy_init(&self) -> Result { if self.current_policy()?.is_some() { return fail("this repository already has a policy"); } let Some((key, _)) = self.secret_key()? else { return fail("no secret key: run `oxy identity create` first"); }; let identity = self.identity(&key)?; let policy = Policy::single_root(self.id()?, identity, Hlc::now().wall_ms + POLICY_LIFETIME_MS); let sp = self.sign_policy(policy)?; self.set_policy(&sp, "oxy policy init")?; Ok(sp) } fn next_policy(&self) -> Result { let Some((_, sp)) = self.current_policy()? else { return fail("this repository has no policy; run `oxy policy init`"); }; let mut p = sp.policy; p.serial += 1; p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS; Ok(p) } /// Let `identity` update references matching `pattern` (default every reference). pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result { let mut p = self.next_policy()?; let key = identity.hash(); let pattern = pattern.unwrap_or("*"); p.revoked.retain(|k| k != &key); match p.rules.iter_mut().find(|r| r.pattern == pattern) { Some(rule) => { if !rule.keys.contains(&key) { rule.keys.push(key); rule.keys.sort(); } } None => p.rules.push(Rule { pattern: pattern.into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }), } if p.identity(&key).is_none() { p.identities.push(identity); } let sp = self.sign_policy(p)?; self.set_policy(&sp, &format!("oxy policy grant {}", key.short(12)))?; Ok(sp) } /// Revoke `key`: it may no longer update anything, and existing rules drop it. pub fn policy_revoke(&self, key: &KeyId) -> Result { let mut p = self.next_policy()?; if p.roots.contains(key) { return fail("cannot revoke a root key"); } for r in &mut p.rules { r.keys.retain(|k| k != key); } p.rules.retain(|r| !r.keys.is_empty()); if !p.revoked.contains(key) { p.revoked.push(*key); p.revoked.sort(); } let sp = self.sign_policy(p)?; self.set_policy(&sp, &format!("oxy policy revoke {}", key.short(12)))?; Ok(sp) } /// Whether `change` may be part of `reference` under `policy`: it must be /// authenticated (by its author or a delegate) with a key the matching /// rule lists, using the authenticator kind the rule requires. pub fn change_allowed(&self, hash: &ChangeHash, policy: &Policy, reference: &str) -> std::result::Result<(), String> { let rule = policy.rule_for(reference).ok_or_else(|| format!("no policy rule matches {reference:?}"))?; match self.authenticate_change(hash).map_err(|e| e.to_string())? { SignatureStatus::Valid { key, sealed, .. } => { if policy.is_revoked(&key) { return Err(format!("change {} is signed by revoked key {}", hash.short(12), key.short(12))); } if !rule.keys.contains(&key) { return Err(format!("change {} is signed by {}, which may not update {reference:?}", hash.short(12), key.short(12))); } match (rule.require, sealed) { (Require::Signed, true) => Err(format!("{reference:?} requires individually signed changes; {} is only sealed", hash.short(12))), (Require::Sealed, false) => Err(format!("{reference:?} requires sealed changes; {} is individually signed", hash.short(12))), _ => Ok(()), } } SignatureStatus::Unsigned => Err(format!("change {} is not signed or sealed", hash.short(12))), SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))), } } /// Check every change that `new` adds over `old` for `reference`. pub(crate) fn state_allowed(&self, old_heads: &[ChangeHash], new_heads: &[ChangeHash], policy: &Policy, reference: &str) -> std::result::Result<(), String> { let added = self.negotiate(old_heads, new_heads).map_err(|e| e.to_string())?; for h in &added { self.change_allowed(h, policy, reference)?; } if let Some(rule) = policy.rule_for(reference) { if !rule.allow_conflicts && !added.is_empty() { let st = self.load_state(new_heads).map_err(|e| e.to_string())?; if st.materialize().is_conflicted() { return Err(format!("{reference:?} does not allow conflicted states")); } } } Ok(()) } }