| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | | use crate::cbor::{self, Value}; |
| 10 | | use crate::hash::{hash_object, KeyId, ObjectRef, ObjectType}; |
| 11 | | use crate::identity::{self, Domain, Identity, Signature}; |
| 12 | | use crate::objects::{ |
| 13 | | array, bytes, check_version, field, map, oref, text, uint, ObjectError, FORMAT_VERSION, |
| 14 | | }; |
| 15 | | use std::collections::BTreeMap; |
| 16 | |
|
| 17 | | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 18 | | pub enum Require { |
| 19 | | Signed, |
| 20 | | Sealed, |
| 21 | | Any, |
| 22 | | } |
| 23 | |
|
| 24 | | impl Require { |
| 25 | 163× | fn tag(self) -> u64 { |
| 26 | 163× | match self { |
| 27 | 4× | Require::Signed => 0, |
| 28 | 0× | Require::Sealed => 1, |
| 29 | 159× | Require::Any => 2, |
| 30 | | } |
| 31 | 163× | } |
| 32 | 404× | fn from_tag(t: u64) -> Result<Require, ObjectError> { |
| 33 | 404× | Ok(match t { |
| 34 | 2× | 0 => Require::Signed, |
| 35 | 0× | 1 => Require::Sealed, |
| 36 | 402× | 2 => Require::Any, |
| 37 | 0× | _ => return Err(ObjectError::Schema(format!("unknown require {t}"))), |
| 38 | | }) |
| 39 | 404× | } |
| 40 | 1× | pub fn name(self) -> &'static str { |
| 41 | 1× | match self { |
| 42 | 0× | Require::Signed => "signed", |
| 43 | 0× | Require::Sealed => "sealed", |
| 44 | 1× | Require::Any => "any", |
| 45 | | } |
| 46 | 1× | } |
| 47 | | } |
| 48 | |
|
| 49 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 50 | | pub struct Rule { |
| 51 | | |
| 52 | | pub pattern: String, |
| 53 | | pub keys: Vec<KeyId>, |
| 54 | | pub threshold: u32, |
| 55 | | pub require: Require, |
| 56 | | pub allow_conflicts: bool, |
| 57 | | } |
| 58 | |
|
| 59 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 60 | | pub struct Policy { |
| 61 | | pub repo: [u8; 16], |
| 62 | | pub serial: u64, |
| 63 | | pub roots: Vec<KeyId>, |
| 64 | | pub root_threshold: u32, |
| 65 | | pub rules: Vec<Rule>, |
| 66 | | |
| 67 | | pub identities: Vec<Identity>, |
| 68 | | pub revoked: Vec<KeyId>, |
| 69 | | pub expires: u64, |
| 70 | | } |
| 71 | |
|
| 72 | |
|
| 73 | 95× | pub fn glob_matches(pattern: &str, name: &str) -> bool { |
| 74 | 589× | fn rec(p: &[u8], n: &[u8]) -> bool { |
| 75 | 589× | match p.first() { |
| 76 | 458× | None => n.is_empty(), |
| 77 | 459× | Some(b'*') => (0..=n.len()).any(|i| rec(&p[1..], &n[i..])), |
| 78 | 40× | Some(c) => n.first() == Some(c) && rec(&p[1..], &n[1..]), |
| 79 | | } |
| 80 | 589× | } |
| 81 | 95× | rec(pattern.as_bytes(), name.as_bytes()) |
| 82 | 95× | } |
| 83 | |
|
| 84 | | impl Policy { |
| 85 | 481× | fn keys_value(keys: &[KeyId]) -> Value { |
| 86 | 481× | Value::Array(keys.iter().map(|k| Value::Bytes(k.0.to_vec())).collect()) |
| 87 | 481× | } |
| 88 | 1208× | fn keys_from(v: &Value, what: &str) -> Result<Vec<KeyId>, ObjectError> { |
| 89 | 1208× | let mut ks = array(v, what)? |
| 90 | 1208× | .iter() |
| 91 | 1208× | .map(|k| oref(k, what)) |
| 92 | 1208× | .collect::<Result<Vec<_>, _>>()?; |
| 93 | 1208× | ks.sort(); |
| 94 | 1208× | ks.dedup(); |
| 95 | 1208× | Ok(ks) |
| 96 | 1208× | } |
| 97 | |
|
| 98 | 159× | pub fn to_value(&self) -> Value { |
| 99 | 159× | Value::map([ |
| 100 | 159× | (0, Value::Uint(FORMAT_VERSION)), |
| 101 | 159× | (1, Value::Bytes(self.repo.to_vec())), |
| 102 | 159× | (2, Value::Uint(self.serial)), |
| 103 | 159× | ( |
| 104 | 159× | 3, |
| 105 | 159× | Value::map([ |
| 106 | 159× | (0, Self::keys_value(&self.roots)), |
| 107 | 159× | (1, Value::Uint(self.root_threshold as u64)), |
| 108 | 159× | ]), |
| 109 | 159× | ), |
| 110 | | ( |
| 111 | | 4, |
| 112 | | Value::Array( |
| 113 | 159× | self.rules |
| 114 | 159× | .iter() |
| 115 | 163× | .map(|r| { |
| 116 | 163× | Value::map([ |
| 117 | 163× | (0, Value::Text(r.pattern.clone())), |
| 118 | 163× | (1, Self::keys_value(&r.keys)), |
| 119 | 163× | (2, Value::Uint(r.threshold as u64)), |
| 120 | 163× | (3, Value::Uint(r.require.tag())), |
| 121 | 163× | (4, Value::Bool(r.allow_conflicts)), |
| 122 | 163× | ]) |
| 123 | 163× | }) |
| 124 | 159× | .collect(), |
| 125 | | ), |
| 126 | | ), |
| 127 | | ( |
| 128 | | 5, |
| 129 | | Value::Array( |
| 130 | 159× | self.identities |
| 131 | 159× | .iter() |
| 132 | 385× | .map(|i| Value::Bytes(i.encode())) |
| 133 | 159× | .collect(), |
| 134 | | ), |
| 135 | | ), |
| 136 | 159× | (6, Self::keys_value(&self.revoked)), |
| 137 | 159× | (7, Value::Uint(self.expires)), |
| 138 | | ]) |
| 139 | 159× | } |
| 140 | |
|
| 141 | 159× | pub fn encode(&self) -> Vec<u8> { |
| 142 | 159× | cbor::encode(&self.to_value()) |
| 143 | 159× | } |
| 144 | |
|
| 145 | 95× | pub fn hash(&self) -> ObjectRef { |
| 146 | 95× | hash_object(ObjectType::Policy, &self.encode()) |
| 147 | 95× | } |
| 148 | |
|
| 149 | 402× | pub fn decode(b: &[u8]) -> Result<Policy, ObjectError> { |
| 150 | 402× | let v = cbor::decode(b)?; |
| 151 | 402× | let m = map(&v, "policy")?; |
| 152 | 402× | check_version(m)?; |
| 153 | 402× | let roots = map(field(m, 3, "roots")?, "roots")?; |
| 154 | 402× | let mut rules = Vec::new(); |
| 155 | 404× | for r in array(field(m, 4, "rules")?, "rules")? { |
| 156 | 404× | let rm = map(r, "rule")?; |
| 157 | 404× | rules.push(Rule { |
| 158 | 404× | pattern: text(field(rm, 0, "pattern")?, "pattern")?.to_owned(), |
| 159 | 404× | keys: Self::keys_from(field(rm, 1, "rule keys")?, "rule keys")?, |
| 160 | 404× | threshold: uint(field(rm, 2, "threshold")?, "threshold")? as u32, |
| 161 | 404× | require: Require::from_tag(uint(field(rm, 3, "require")?, "require")?)?, |
| 162 | 404× | allow_conflicts: field(rm, 4, "allow_conflicts")? |
| 163 | 404× | .as_bool() |
| 164 | 404× | .ok_or_else(|| ObjectError::Schema("allow_conflicts: expected bool".into()))?, |
| 165 | | }); |
| 166 | | } |
| 167 | 402× | let identities = array(field(m, 5, "identities")?, "identities")? |
| 168 | 402× | .iter() |
| 169 | 884× | .map(|i| Identity::decode(bytes(i, "identity")?)) |
| 170 | 402× | .collect::<Result<Vec<_>, _>>()?; |
| 171 | 402× | let p = Policy { |
| 172 | 402× | repo: bytes(field(m, 1, "repo")?, "repo")? |
| 173 | 402× | .try_into() |
| 174 | 402× | .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?, |
| 175 | 402× | serial: uint(field(m, 2, "serial")?, "serial")?, |
| 176 | 402× | roots: Self::keys_from(field(roots, 0, "root keys")?, "root keys")?, |
| 177 | 402× | root_threshold: uint(field(roots, 1, "root threshold")?, "root threshold")? as u32, |
| 178 | 402× | rules, |
| 179 | 402× | identities, |
| 180 | 402× | revoked: Self::keys_from(field(m, 6, "revoked")?, "revoked")?, |
| 181 | 402× | expires: uint(field(m, 7, "expires")?, "expires")?, |
| 182 | | }; |
| 183 | 402× | if p.root_threshold == 0 || p.root_threshold as usize > p.roots.len() { |
| 184 | 1× | return Err(ObjectError::Schema( |
| 185 | 1× | "root threshold must be between 1 and the number of root keys".into(), |
| 186 | 1× | )); |
| 187 | 401× | } |
| 188 | 402× | for r in &p.rules { |
| 189 | 402× | if r.threshold == 0 || r.threshold as usize > r.keys.len() { |
| 190 | 0× | return Err(ObjectError::Schema(format!( |
| 191 | 0× | "rule {:?}: threshold must be between 1 and the number of keys", |
| 192 | 0× | r.pattern |
| 193 | 0× | ))); |
| 194 | 402× | } |
| 195 | | } |
| 196 | 401× | Ok(p) |
| 197 | 402× | } |
| 198 | |
|
| 199 | 108× | pub fn identity(&self, key: &KeyId) -> Option<&Identity> { |
| 200 | 211× | self.identities.iter().find(|i| i.hash() == *key) |
| 201 | 108× | } |
| 202 | |
|
| 203 | 228× | pub fn is_revoked(&self, key: &KeyId) -> bool { |
| 204 | 228× | self.revoked.contains(key) |
| 205 | 228× | } |
| 206 | |
|
| 207 | 3× | pub fn is_expired(&self, now_ms: u64) -> bool { |
| 208 | 3× | now_ms > self.expires |
| 209 | 3× | } |
| 210 | |
|
| 211 | | |
| 212 | 87× | pub fn rule_for(&self, reference: &str) -> Option<&Rule> { |
| 213 | 87× | self.rules |
| 214 | 87× | .iter() |
| 215 | 88× | .find(|r| glob_matches(&r.pattern, reference)) |
| 216 | 87× | } |
| 217 | |
|
| 218 | | |
| 219 | 42× | pub fn authorizes(&self, key: &KeyId, reference: &str) -> bool { |
| 220 | 42× | !self.is_revoked(key) |
| 221 | 41× | && self |
| 222 | 41× | .rule_for(reference) |
| 223 | 41× | .is_some_and(|r| r.keys.contains(key)) |
| 224 | 42× | } |
| 225 | |
|
| 226 | | |
| 227 | | |
| 228 | | |
| 229 | 52× | pub fn verify_signatures( |
| 230 | 52× | &self, |
| 231 | 52× | sigs: &[Signature], |
| 232 | 52× | trusted: &Policy, |
| 233 | 52× | ) -> Result<Vec<KeyId>, String> { |
| 234 | 52× | let target = self.hash(); |
| 235 | 52× | let mut signers = Vec::new(); |
| 236 | 77× | for sig in sigs { |
| 237 | 77× | if !trusted.roots.contains(&sig.key) || trusted.is_revoked(&sig.key) { |
| 238 | 19× | continue; |
| 239 | 58× | } |
| 240 | 58× | let Some(id) = trusted |
| 241 | 58× | .identity(&sig.key) |
| 242 | 58× | .or_else(|| self.identity(&sig.key)) |
| 243 | | else { |
| 244 | 0× | continue; |
| 245 | | }; |
| 246 | 58× | if identity::verify(id, sig, Domain::Policy, &target).is_ok() |
| 247 | 58× | && !signers.contains(&sig.key) |
| 248 | 57× | { |
| 249 | 57× | signers.push(sig.key); |
| 250 | 57× | } |
| 251 | | } |
| 252 | 52× | if (signers.len() as u32) < trusted.root_threshold { |
| 253 | 6× | return Err(format!( |
| 254 | 6× | "policy needs {} root signature(s), has {}", |
| 255 | 6× | trusted.root_threshold, |
| 256 | 6× | signers.len() |
| 257 | 6× | )); |
| 258 | 46× | } |
| 259 | 46× | Ok(signers) |
| 260 | 52× | } |
| 261 | |
|
| 262 | | |
| 263 | | |
| 264 | | |
| 265 | | |
| 266 | 11× | pub fn verify_bootstrap(&self, sigs: &[Signature]) -> Result<KeyId, String> { |
| 267 | 11× | let target = self.hash(); |
| 268 | 11× | for sig in sigs { |
| 269 | 11× | if !self.roots.contains(&sig.key) || self.is_revoked(&sig.key) { |
| 270 | 0× | continue; |
| 271 | 11× | } |
| 272 | 11× | if let Some(id) = self.identity(&sig.key) { |
| 273 | 11× | if identity::verify(id, sig, Domain::Policy, &target).is_ok() { |
| 274 | 11× | return Ok(sig.key); |
| 275 | 0× | } |
| 276 | 0× | } |
| 277 | | } |
| 278 | 0× | Err("policy carries no valid signature by one of its own root keys".into()) |
| 279 | 11× | } |
| 280 | |
|
| 281 | | |
| 282 | 12× | pub fn single_root(repo: [u8; 16], root: Identity, expires: u64) -> Policy { |
| 283 | 12× | let key = root.hash(); |
| 284 | 12× | Policy { |
| 285 | 12× | repo, |
| 286 | 12× | serial: 1, |
| 287 | 12× | roots: vec![key], |
| 288 | 12× | root_threshold: 1, |
| 289 | 12× | rules: vec![Rule { |
| 290 | 12× | pattern: "*".into(), |
| 291 | 12× | keys: vec![key], |
| 292 | 12× | threshold: 1, |
| 293 | 12× | require: Require::Any, |
| 294 | 12× | allow_conflicts: true, |
| 295 | 12× | }], |
| 296 | 12× | identities: vec![root], |
| 297 | 12× | revoked: vec![], |
| 298 | 12× | expires, |
| 299 | 12× | } |
| 300 | 12× | } |
| 301 | |
|
| 302 | | |
| 303 | 8× | pub fn mentions(&self, key: &KeyId) -> bool { |
| 304 | 8× | self.roots.contains(key) |
| 305 | 1× | || self.rules.iter().any(|r| r.keys.contains(key)) |
| 306 | 0× | || self.identity(key).is_some() |
| 307 | 8× | } |
| 308 | |
|
| 309 | | |
| 310 | | |
| 311 | | |
| 312 | 27× | pub fn succeeds(&self, rule_key: &KeyId, key: &KeyId) -> bool { |
| 313 | 27× | if self.is_revoked(key) { |
| 314 | 0× | return false; |
| 315 | 27× | } |
| 316 | 27× | let mut cur = Some(*rule_key); |
| 317 | 27× | for _ in 0..32 { |
| 318 | 37× | match cur { |
| 319 | 37× | Some(k) if k == *key => return true, |
| 320 | 19× | Some(k) => cur = self.identity(&k).and_then(|i| i.previous), |
| 321 | 9× | None => return false, |
| 322 | | } |
| 323 | | } |
| 324 | 0× | false |
| 325 | 27× | } |
| 326 | |
|
| 327 | | |
| 328 | | |
| 329 | | |
| 330 | | |
| 331 | 7× | pub fn substitute_key(&mut self, old: &KeyId, new: Identity) { |
| 332 | 7× | let nk = new.hash(); |
| 333 | 14× | for k in self |
| 334 | 7× | .roots |
| 335 | 7× | .iter_mut() |
| 336 | 7× | .chain(self.rules.iter_mut().flat_map(|r| r.keys.iter_mut())) |
| 337 | | { |
| 338 | 14× | if k == old { |
| 339 | 14× | *k = nk; |
| 340 | 14× | } |
| 341 | | } |
| 342 | 7× | for r in &mut self.rules { |
| 343 | 7× | r.keys.sort(); |
| 344 | 7× | r.keys.dedup(); |
| 345 | 7× | } |
| 346 | 7× | self.roots.sort(); |
| 347 | 7× | self.roots.dedup(); |
| 348 | 7× | if self.identity(&nk).is_none() { |
| 349 | 7× | self.identities.push(new); |
| 350 | 7× | } |
| 351 | 7× | } |
| 352 | |
|
| 353 | | |
| 354 | 0× | pub fn identity_map(&self) -> BTreeMap<KeyId, &Identity> { |
| 355 | 0× | self.identities.iter().map(|i| (i.hash(), i)).collect() |
| 356 | 0× | } |
| 357 | | } |
| 358 | |
|
| 359 | | #[cfg(test)] |
| 360 | | mod tests { |
| 361 | | use super::*; |
| 362 | | use crate::identity::SecretKey; |
| 363 | |
|
| 364 | 5× | fn sk(n: u8) -> SecretKey { |
| 365 | 5× | SecretKey { |
| 366 | 5× | ed_seed: [n; 32], |
| 367 | 5× | ml_seed: [n + 100; 32], |
| 368 | 5× | } |
| 369 | 5× | } |
| 370 | |
|
| 371 | | #[test] |
| 372 | 1× | fn globs() { |
| 373 | 1× | assert!(glob_matches("*", "main")); |
| 374 | 1× | assert!(glob_matches("release/*", "release/1.0")); |
| 375 | 1× | assert!(!glob_matches("release/*", "main")); |
| 376 | 1× | assert!(glob_matches("main", "main")); |
| 377 | 1× | assert!(!glob_matches("main", "main2")); |
| 378 | 1× | assert!(glob_matches("*-rc*", "1.0-rc2")); |
| 379 | 1× | assert!(glob_matches("", "")); |
| 380 | 1× | } |
| 381 | |
|
| 382 | | #[test] |
| 383 | 1× | fn round_trip_and_rules() { |
| 384 | 1× | let root = sk(1).identity("Root", "r@x", 0); |
| 385 | 1× | let dev = sk(2).identity("Dev", "d@x", 0); |
| 386 | 1× | let mut p = Policy::single_root([9; 16], root.clone(), 10); |
| 387 | 1× | p.rules.insert( |
| 388 | | 0, |
| 389 | 1× | Rule { |
| 390 | 1× | pattern: "release/*".into(), |
| 391 | 1× | keys: vec![root.hash()], |
| 392 | 1× | threshold: 1, |
| 393 | 1× | require: Require::Signed, |
| 394 | 1× | allow_conflicts: false, |
| 395 | 1× | }, |
| 396 | | ); |
| 397 | 1× | p.rules[1].keys.push(dev.hash()); |
| 398 | 1× | p.identities.push(dev.clone()); |
| 399 | 1× | p.revoked.push(ObjectRef([7; 32])); |
| 400 | 1× | let d = Policy::decode(&p.encode()).unwrap(); |
| 401 | 1× | assert_eq!(d.rules.len(), 2); |
| 402 | 1× | assert_eq!(d.rules[0].require, Require::Signed); |
| 403 | 1× | assert_eq!(d.identity(&dev.hash()), Some(&dev)); |
| 404 | 1× | assert!(d.authorizes(&dev.hash(), "main")); |
| 405 | 1× | assert!(!d.authorizes(&dev.hash(), "release/1")); |
| 406 | 1× | assert!(d.authorizes(&root.hash(), "release/1")); |
| 407 | 1× | assert!(d.is_expired(11) && !d.is_expired(10)); |
| 408 | 1× | assert_eq!(d.hash(), p.hash()); |
| 409 | |
|
| 410 | 1× | let mut bad = p.clone(); |
| 411 | 1× | bad.root_threshold = 5; |
| 412 | 1× | assert!(Policy::decode(&bad.encode()).is_err()); |
| 413 | 1× | } |
| 414 | |
|
| 415 | | #[test] |
| 416 | 1× | fn signatures_need_root_threshold() { |
| 417 | 1× | let r1 = sk(1); |
| 418 | 1× | let r2 = sk(2); |
| 419 | 1× | let i1 = r1.identity("a", "a", 0); |
| 420 | 1× | let i2 = r2.identity("b", "b", 0); |
| 421 | 1× | let mut p = Policy::single_root([1; 16], i1.clone(), 100); |
| 422 | 1× | p.roots.push(i2.hash()); |
| 423 | 1× | p.roots.sort(); |
| 424 | 1× | p.root_threshold = 2; |
| 425 | 1× | p.identities.push(i2.clone()); |
| 426 | 1× | let h = p.hash(); |
| 427 | 1× | let s1 = r1.sign(Domain::Policy, &i1.hash(), &h); |
| 428 | 1× | let s2 = r2.sign(Domain::Policy, &i2.hash(), &h); |
| 429 | 1× | assert!(p.verify_signatures(std::slice::from_ref(&s1), &p).is_err()); |
| 430 | 1× | assert_eq!( |
| 431 | 1× | p.verify_signatures(&[s1.clone(), s2.clone()], &p) |
| 432 | 1× | .unwrap() |
| 433 | 1× | .len(), |
| 434 | | 2 |
| 435 | | ); |
| 436 | | |
| 437 | 1× | assert!(p.verify_signatures(&[s1.clone(), s1.clone()], &p).is_err()); |
| 438 | 1× | let stranger = sk(3); |
| 439 | 1× | let si = stranger.identity("s", "s", 0); |
| 440 | 1× | let s3 = stranger.sign(Domain::Policy, &si.hash(), &h); |
| 441 | 1× | assert!(p.verify_signatures(&[s1.clone(), s3], &p).is_err()); |
| 442 | | |
| 443 | 1× | let mut next = p.clone(); |
| 444 | 1× | next.serial = 2; |
| 445 | 1× | next.revoked.push(i2.hash()); |
| 446 | 1× | let nh = next.hash(); |
| 447 | 1× | let n1 = r1.sign(Domain::Policy, &i1.hash(), &nh); |
| 448 | 1× | let n2 = r2.sign(Domain::Policy, &i2.hash(), &nh); |
| 449 | 1× | assert_eq!(next.verify_signatures(&[n1, n2], &p).unwrap().len(), 2); |
| 450 | 1× | } |
| 451 | | } |