//! 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, /// Who else may sign, by what an issuer certified (§14.7): a short-lived /// key whose certificate is the issuer's, for a subject the pattern /// matches, counts as one signer within the scope given. pub subjects: Vec, } /// A rule's grant to the holders of certificates an issuer gives (§14.7): /// `repo:andy/demo:bookmark:main`, `agent:claude@andy`, with `*` as in /// reference patterns. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjectGrant { pub pattern: String, pub issuer: KeyId, pub scope: identity::Scope, } impl SubjectGrant { fn to_value(&self) -> Value { Value::map([ (0, Value::Text(self.pattern.clone())), (1, Value::Bytes(self.issuer.0.to_vec())), ( 2, Value::Array( self.scope .names() .into_iter() .map(|n| Value::Text(n.to_string())) .collect(), ), ), ]) } fn from_value(v: &Value) -> Result { let m = map(v, "subject")?; let mut scope = identity::Scope::NONE; for n in array(field(m, 2, "subject scope")?, "subject scope")? { let n = text(n, "subject scope")?; scope = scope | identity::Scope::named(n) .ok_or_else(|| ObjectError::Schema(format!("subject: unknown scope {n:?}")))?; } Ok(SubjectGrant { pattern: text(field(m, 0, "subject pattern")?, "subject pattern")?.to_owned(), issuer: oref(field(m, 1, "subject issuer")?, "subject issuer")?, scope, }) } } #[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| { let mut v = 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)), ]); // Only when there are any, so a policy without // them encodes, and hashes, as before. if !r.subjects.is_empty() { if let Value::Map(m) = &mut v { m.insert( 5, Value::Array( r.subjects.iter().map(SubjectGrant::to_value).collect(), ), ); } } v }) .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()))?, subjects: match rm.get(&5) { None => vec![], Some(v) => array(v, "subjects")? .iter() .map(SubjectGrant::from_value) .collect::>()?, }, }); } 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() + r.subjects.len() { return Err(ObjectError::Schema(format!( "rule {:?}: threshold must be between 1 and the number of keys and subjects", 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 the rule for `reference` lets a certificate from `issuer` /// for `subject` sign within `scope` (§14.7). The certificate itself, /// and the time it is checked at, are the caller's to verify. pub fn subject_grant( &self, reference: &str, issuer: &KeyId, subject: &str, scope: identity::Scope, ) -> bool { !self.is_revoked(issuer) && self.rule_for(reference).is_some_and(|r| { r.subjects.iter().any(|g| { g.issuer == *issuer && g.scope.has(scope) && glob_matches(&g.pattern, subject) }) }) } /// 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, subjects: vec![], }], 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("", "")); } /// Subjects an issuer certifies (oxygenhub#241): kept through /// encoding, absent from a policy without them, and granted only for /// their issuer, pattern and scope, and not when the issuer is revoked. #[test] fn a_rule_grants_certified_subjects() { use crate::identity::Scope; let root = sk(1).identity("Root", "r@x", 0); let hub = sk(3).identity("Hub", "h@x", 0); let mut p = Policy::single_root([9; 16], root.clone(), 10); let plain = p.encode(); p.rules[0].subjects.push(SubjectGrant { pattern: "repo:andy/demo:bookmark:main".into(), issuer: hub.hash(), scope: Scope::REFS | Scope::CHANGES, }); p.rules[0].subjects.push(SubjectGrant { pattern: "agent:*@andy".into(), issuer: hub.hash(), scope: Scope::CHANGES, }); let back = Policy::decode(&p.encode()).unwrap(); assert_eq!(back, p); let mut again = back.clone(); again.rules[0].subjects.clear(); assert_eq!( again.encode(), plain, "no subjects, the same bytes as before" ); let main = "repo:andy/demo:bookmark:main"; assert!(p.subject_grant("main", &hub.hash(), main, Scope::REFS)); assert!(p.subject_grant("main", &hub.hash(), "agent:claude@andy", Scope::CHANGES)); assert!( !p.subject_grant("main", &hub.hash(), "agent:claude@andy", Scope::REFS), "not its scope" ); assert!( !p.subject_grant( "main", &hub.hash(), "repo:andy/demo:bookmark:dev", Scope::REFS ), "not its pattern" ); assert!( !p.subject_grant("main", &root.hash(), main, Scope::REFS), "not its issuer" ); p.revoked.push(hub.hash()); assert!( !p.subject_grant("main", &hub.hash(), main, Scope::REFS), "a revoked issuer grants nothing" ); // A rule of subjects alone is a rule. let mut only = Policy::single_root([9; 16], root, 10); only.rules[0].keys.clear(); only.rules[0].subjects.push(SubjectGrant { pattern: "*".into(), issuer: hub.hash(), scope: Scope::REFS, }); assert!(Policy::decode(&only.encode()).is_ok()); only.rules[0].subjects.clear(); assert!(Policy::decode(&only.encode()).is_err(), "nobody to sign"); } #[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, subjects: vec![], }, ); 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(std::slice::from_ref(&s1), &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); } }