//! The trust policy (spec §14.5): who may update which references, signed //! by a threshold of root keys. A repository without a policy is unverified. //! //! Encoding: `{0: version, 1: repo, 2: serial, 3: {0: root keys, 1: threshold}, //! 4: [rule], 5: [identity bytes], 6: [revoked], 7: expires}`; a rule is //! `{0: pattern, 1: keys, 2: threshold, 3: require, 4: allow_conflicts}` with //! `require` 0 = signed, 1 = sealed, 2 = any. use crate::cbor::{self, Value}; use crate::hash::{hash_object, KeyId, ObjectRef, ObjectType}; use crate::identity::{self, Domain, Identity, Signature}; use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, ObjectError, FORMAT_VERSION}; use std::collections::BTreeMap; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Require { Signed, Sealed, Any, } impl Require { fn tag(self) -> u64 { match self { Require::Signed => 0, Require::Sealed => 1, Require::Any => 2, } } fn from_tag(t: u64) -> Result { Ok(match t { 0 => Require::Signed, 1 => Require::Sealed, 2 => Require::Any, _ => return Err(ObjectError::Schema(format!("unknown require {t}"))), }) } pub fn name(self) -> &'static str { match self { Require::Signed => "signed", Require::Sealed => "sealed", Require::Any => "any", } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Rule { /// Glob over reference names; `*` matches any run of characters. pub pattern: String, pub keys: Vec, pub threshold: u32, pub require: Require, pub allow_conflicts: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Policy { pub repo: [u8; 16], pub serial: u64, pub roots: Vec, pub root_threshold: u32, pub rules: Vec, /// Embedded so verification needs no fetch. pub identities: Vec, pub revoked: Vec, pub expires: u64, } /// `*` matches any run of characters (including `/`); everything else is literal. pub fn glob_matches(pattern: &str, name: &str) -> bool { fn rec(p: &[u8], n: &[u8]) -> bool { match p.first() { None => n.is_empty(), Some(b'*') => (0..=n.len()).any(|i| rec(&p[1..], &n[i..])), Some(c) => n.first() == Some(c) && rec(&p[1..], &n[1..]), } } rec(pattern.as_bytes(), name.as_bytes()) } impl Policy { fn keys_value(keys: &[KeyId]) -> Value { Value::Array(keys.iter().map(|k| Value::Bytes(k.0.to_vec())).collect()) } fn keys_from(v: &Value, what: &str) -> Result, ObjectError> { let mut ks = array(v, what)?.iter().map(|k| oref(k, what)).collect::, _>>()?; ks.sort(); ks.dedup(); Ok(ks) } pub fn to_value(&self) -> Value { Value::map([ (0, Value::Uint(FORMAT_VERSION)), (1, Value::Bytes(self.repo.to_vec())), (2, Value::Uint(self.serial)), (3, Value::map([(0, Self::keys_value(&self.roots)), (1, Value::Uint(self.root_threshold as u64))])), ( 4, Value::Array( self.rules .iter() .map(|r| { Value::map([ (0, Value::Text(r.pattern.clone())), (1, Self::keys_value(&r.keys)), (2, Value::Uint(r.threshold as u64)), (3, Value::Uint(r.require.tag())), (4, Value::Bool(r.allow_conflicts)), ]) }) .collect(), ), ), (5, Value::Array(self.identities.iter().map(|i| Value::Bytes(i.encode())).collect())), (6, Self::keys_value(&self.revoked)), (7, Value::Uint(self.expires)), ]) } pub fn encode(&self) -> Vec { cbor::encode(&self.to_value()) } pub fn hash(&self) -> ObjectRef { hash_object(ObjectType::Policy, &self.encode()) } pub fn decode(b: &[u8]) -> Result { let v = cbor::decode(b)?; let m = map(&v, "policy")?; check_version(m)?; let roots = map(field(m, 3, "roots")?, "roots")?; let mut rules = Vec::new(); for r in array(field(m, 4, "rules")?, "rules")? { let rm = map(r, "rule")?; rules.push(Rule { pattern: text(field(rm, 0, "pattern")?, "pattern")?.to_owned(), keys: Self::keys_from(field(rm, 1, "rule keys")?, "rule keys")?, threshold: uint(field(rm, 2, "threshold")?, "threshold")? as u32, require: Require::from_tag(uint(field(rm, 3, "require")?, "require")?)?, allow_conflicts: field(rm, 4, "allow_conflicts")?.as_bool().ok_or_else(|| ObjectError::Schema("allow_conflicts: expected bool".into()))?, }); } let identities = array(field(m, 5, "identities")?, "identities")? .iter() .map(|i| Identity::decode(bytes(i, "identity")?)) .collect::, _>>()?; let p = Policy { repo: bytes(field(m, 1, "repo")?, "repo")?.try_into().map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?, serial: uint(field(m, 2, "serial")?, "serial")?, roots: Self::keys_from(field(roots, 0, "root keys")?, "root keys")?, root_threshold: uint(field(roots, 1, "root threshold")?, "root threshold")? as u32, rules, identities, revoked: Self::keys_from(field(m, 6, "revoked")?, "revoked")?, expires: uint(field(m, 7, "expires")?, "expires")?, }; if p.root_threshold == 0 || p.root_threshold as usize > p.roots.len() { return Err(ObjectError::Schema("root threshold must be between 1 and the number of root keys".into())); } for r in &p.rules { if r.threshold == 0 || r.threshold as usize > r.keys.len() { return Err(ObjectError::Schema(format!("rule {:?}: threshold must be between 1 and the number of keys", r.pattern))); } } Ok(p) } pub fn identity(&self, key: &KeyId) -> Option<&Identity> { self.identities.iter().find(|i| i.hash() == *key) } pub fn is_revoked(&self, key: &KeyId) -> bool { self.revoked.contains(key) } pub fn is_expired(&self, now_ms: u64) -> bool { now_ms > self.expires } /// The first rule whose pattern matches `reference`. pub fn rule_for(&self, reference: &str) -> Option<&Rule> { self.rules.iter().find(|r| glob_matches(&r.pattern, reference)) } /// Whether `key` may update `reference`: listed in the matching rule and not revoked. pub fn authorizes(&self, key: &KeyId, reference: &str) -> bool { !self.is_revoked(key) && self.rule_for(reference).is_some_and(|r| r.keys.contains(key)) } /// Verify that `sigs` over this policy's hash meet `trusted`'s root /// threshold (`trusted` is the previous policy, or this one when /// bootstrapping). Returns the signing root keys. pub fn verify_signatures(&self, sigs: &[Signature], trusted: &Policy) -> Result, String> { let target = self.hash(); let mut signers = Vec::new(); for sig in sigs { if !trusted.roots.contains(&sig.key) || trusted.is_revoked(&sig.key) { continue; } let Some(id) = trusted.identity(&sig.key).or_else(|| self.identity(&sig.key)) else { continue }; if identity::verify(id, sig, Domain::Policy, &target).is_ok() && !signers.contains(&sig.key) { signers.push(sig.key); } } if (signers.len() as u32) < trusted.root_threshold { return Err(format!("policy needs {} root signature(s), has {}", trusted.root_threshold, signers.len())); } Ok(signers) } /// Bootstrap check for the first policy a repository sees (trust on /// first use): at least one valid signature by one of its own roots. /// The full threshold applies only to updates from a trusted policy, /// whose signers are the previous roots and need not be the new ones. pub fn verify_bootstrap(&self, sigs: &[Signature]) -> Result { let target = self.hash(); for sig in sigs { if !self.roots.contains(&sig.key) || self.is_revoked(&sig.key) { continue; } if let Some(id) = self.identity(&sig.key) { if identity::verify(id, sig, Domain::Policy, &target).is_ok() { return Ok(sig.key); } } } Err("policy carries no valid signature by one of its own root keys".into()) } /// A single-root policy letting `root` do everything on every reference. pub fn single_root(repo: [u8; 16], root: Identity, expires: u64) -> Policy { let key = root.hash(); Policy { repo, serial: 1, roots: vec![key], root_threshold: 1, rules: vec![Rule { pattern: "*".into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }], identities: vec![root], revoked: vec![], expires, } } /// Whether `key` appears anywhere in the policy (roots, rules, identities). pub fn mentions(&self, key: &KeyId) -> bool { self.roots.contains(key) || self.rules.iter().any(|r| r.keys.contains(key)) || self.identity(key).is_some() } /// Whether `key` is `rule_key` or one of its predecessors in the /// rotation chain (`previous` links, spec §14.2), so that changes signed /// before a rotation remain authorized. Revoked keys never qualify. pub fn succeeds(&self, rule_key: &KeyId, key: &KeyId) -> bool { if self.is_revoked(key) { return false; } let mut cur = Some(*rule_key); for _ in 0..32 { match cur { Some(k) if k == *key => return true, Some(k) => cur = self.identity(&k).and_then(|i| i.previous), None => return false, } } false } /// Replace `old` by `new` in the roots and every rule and embed `new`'s /// identity (a rotation, spec §14.2). The old identity stays embedded so /// signatures made before the rotation still verify; it is superseded, /// not revoked. pub fn substitute_key(&mut self, old: &KeyId, new: Identity) { let nk = new.hash(); for k in self.roots.iter_mut().chain(self.rules.iter_mut().flat_map(|r| r.keys.iter_mut())) { if k == old { *k = nk; } } for r in &mut self.rules { r.keys.sort(); r.keys.dedup(); } self.roots.sort(); self.roots.dedup(); if self.identity(&nk).is_none() { self.identities.push(new); } } /// Identities keyed by KeyId. pub fn identity_map(&self) -> BTreeMap { self.identities.iter().map(|i| (i.hash(), i)).collect() } } #[cfg(test)] mod tests { use super::*; use crate::identity::SecretKey; fn sk(n: u8) -> SecretKey { SecretKey { ed_seed: [n; 32], ml_seed: [n + 100; 32] } } #[test] fn globs() { assert!(glob_matches("*", "main")); assert!(glob_matches("release/*", "release/1.0")); assert!(!glob_matches("release/*", "main")); assert!(glob_matches("main", "main")); assert!(!glob_matches("main", "main2")); assert!(glob_matches("*-rc*", "1.0-rc2")); assert!(glob_matches("", "")); } #[test] fn round_trip_and_rules() { let root = sk(1).identity("Root", "r@x", 0); let dev = sk(2).identity("Dev", "d@x", 0); let mut p = Policy::single_root([9; 16], root.clone(), 10); p.rules.insert(0, Rule { pattern: "release/*".into(), keys: vec![root.hash()], threshold: 1, require: Require::Signed, allow_conflicts: false }); p.rules[1].keys.push(dev.hash()); p.identities.push(dev.clone()); p.revoked.push(ObjectRef([7; 32])); let d = Policy::decode(&p.encode()).unwrap(); assert_eq!(d.rules.len(), 2); assert_eq!(d.rules[0].require, Require::Signed); assert_eq!(d.identity(&dev.hash()), Some(&dev)); assert!(d.authorizes(&dev.hash(), "main")); assert!(!d.authorizes(&dev.hash(), "release/1")); assert!(d.authorizes(&root.hash(), "release/1")); assert!(d.is_expired(11) && !d.is_expired(10)); assert_eq!(d.hash(), p.hash()); let mut bad = p.clone(); bad.root_threshold = 5; assert!(Policy::decode(&bad.encode()).is_err()); } #[test] fn signatures_need_root_threshold() { let r1 = sk(1); let r2 = sk(2); let i1 = r1.identity("a", "a", 0); let i2 = r2.identity("b", "b", 0); let mut p = Policy::single_root([1; 16], i1.clone(), 100); p.roots.push(i2.hash()); p.roots.sort(); p.root_threshold = 2; p.identities.push(i2.clone()); let h = p.hash(); let s1 = r1.sign(Domain::Policy, &i1.hash(), &h); let s2 = r2.sign(Domain::Policy, &i2.hash(), &h); assert!(p.verify_signatures(&[s1.clone()], &p).is_err()); assert_eq!(p.verify_signatures(&[s1.clone(), s2.clone()], &p).unwrap().len(), 2); // Duplicates do not count twice; a non-root signer does not count. assert!(p.verify_signatures(&[s1.clone(), s1.clone()], &p).is_err()); let stranger = sk(3); let si = stranger.identity("s", "s", 0); let s3 = stranger.sign(Domain::Policy, &si.hash(), &h); assert!(p.verify_signatures(&[s1.clone(), s3], &p).is_err()); // A successor policy is verified against the previous roots. let mut next = p.clone(); next.serial = 2; next.revoked.push(i2.hash()); let nh = next.hash(); let n1 = r1.sign(Domain::Policy, &i1.hash(), &nh); let n2 = r2.sign(Domain::Policy, &i2.hash(), &nh); assert_eq!(next.verify_signatures(&[n1, n2], &p).unwrap().len(), 2); } }