| 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 | 396× | fn tag(self) -> u64 { |
| 26 | 396× | match self { |
| 27 | 4× | Require::Signed => 0, |
| 28 | 0× | Require::Sealed => 1, |
| 29 | 392× | Require::Any => 2, |
| 30 | | } |
| 31 | 396× | } |
| 32 | 982× | fn from_tag(t: u64) -> Result<Require, ObjectError> { |
| 33 | 982× | Ok(match t { |
| 34 | 2× | 0 => Require::Signed, |
| 35 | 0× | 1 => Require::Sealed, |
| 36 | 980× | 2 => Require::Any, |
| 37 | 0× | _ => return Err(ObjectError::Schema(format!("unknown require {t}"))), |
| 38 | | }) |
| 39 | 982× | } |
| 40 | 2× | pub fn name(self) -> &'static str { |
| 41 | 2× | match self { |
| 42 | 0× | Require::Signed => "signed", |
| 43 | 0× | Require::Sealed => "sealed", |
| 44 | 2× | Require::Any => "any", |
| 45 | | } |
| 46 | 2× | } |
| 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 | | |
| 60 | | pub subjects: Vec<SubjectGrant>, |
| 61 | | } |
| 62 | |
|
| 63 | |
|
| 64 | |
|
| 65 | |
|
| 66 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 67 | | pub struct SubjectGrant { |
| 68 | | pub pattern: String, |
| 69 | | pub issuer: KeyId, |
| 70 | | pub scope: identity::Scope, |
| 71 | | } |
| 72 | |
|
| 73 | | impl SubjectGrant { |
| 74 | 102× | fn to_value(&self) -> Value { |
| 75 | 102× | Value::map([ |
| 76 | 102× | (0, Value::Text(self.pattern.clone())), |
| 77 | 102× | (1, Value::Bytes(self.issuer.0.to_vec())), |
| 78 | | ( |
| 79 | | 2, |
| 80 | | Value::Array( |
| 81 | 102× | self.scope |
| 82 | 102× | .names() |
| 83 | 102× | .into_iter() |
| 84 | 199× | .map(|n| Value::Text(n.to_string())) |
| 85 | 102× | .collect(), |
| 86 | | ), |
| 87 | | ), |
| 88 | | ]) |
| 89 | 102× | } |
| 90 | |
|
| 91 | 301× | fn from_value(v: &Value) -> Result<SubjectGrant, ObjectError> { |
| 92 | 301× | let m = map(v, "subject")?; |
| 93 | 301× | let mut scope = identity::Scope::NONE; |
| 94 | 599× | for n in array(field(m, 2, "subject scope")?, "subject scope")? { |
| 95 | 599× | let n = text(n, "subject scope")?; |
| 96 | 599× | scope = scope |
| 97 | 599× | | identity::Scope::named(n) |
| 98 | 599× | .ok_or_else(|| ObjectError::Schema(format!("subject: unknown scope {n:?}")))?; |
| 99 | | } |
| 100 | | Ok(SubjectGrant { |
| 101 | 301× | pattern: text(field(m, 0, "subject pattern")?, "subject pattern")?.to_owned(), |
| 102 | 301× | issuer: oref(field(m, 1, "subject issuer")?, "subject issuer")?, |
| 103 | 301× | scope, |
| 104 | | }) |
| 105 | 301× | } |
| 106 | | } |
| 107 | |
|
| 108 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 109 | | pub struct Policy { |
| 110 | | pub repo: [u8; 16], |
| 111 | | pub serial: u64, |
| 112 | | pub roots: Vec<KeyId>, |
| 113 | | pub root_threshold: u32, |
| 114 | | pub rules: Vec<Rule>, |
| 115 | | |
| 116 | | pub identities: Vec<Identity>, |
| 117 | | pub revoked: Vec<KeyId>, |
| 118 | | pub expires: u64, |
| 119 | | } |
| 120 | |
|
| 121 | |
|
| 122 | 426× | pub fn glob_matches(pattern: &str, name: &str) -> bool { |
| 123 | 3459× | fn rec(p: &[u8], n: &[u8]) -> bool { |
| 124 | 3459× | match p.first() { |
| 125 | 2308× | None => n.is_empty(), |
| 126 | 2273× | Some(b'*') => (0..=n.len()).any(|i| rec(&p[1..], &n[i..])), |
| 127 | 775× | Some(c) => n.first() == Some(c) && rec(&p[1..], &n[1..]), |
| 128 | | } |
| 129 | 3459× | } |
| 130 | 426× | rec(pattern.as_bytes(), name.as_bytes()) |
| 131 | 426× | } |
| 132 | |
|
| 133 | | impl Policy { |
| 134 | 1180× | fn keys_value(keys: &[KeyId]) -> Value { |
| 135 | 1180× | Value::Array(keys.iter().map(|k| Value::Bytes(k.0.to_vec())).collect()) |
| 136 | 1180× | } |
| 137 | 2942× | fn keys_from(v: &Value, what: &str) -> Result<Vec<KeyId>, ObjectError> { |
| 138 | 2942× | let mut ks = array(v, what)? |
| 139 | 2942× | .iter() |
| 140 | 2942× | .map(|k| oref(k, what)) |
| 141 | 2942× | .collect::<Result<Vec<_>, _>>()?; |
| 142 | 2942× | ks.sort(); |
| 143 | 2942× | ks.dedup(); |
| 144 | 2942× | Ok(ks) |
| 145 | 2942× | } |
| 146 | |
|
| 147 | 392× | pub fn to_value(&self) -> Value { |
| 148 | 392× | Value::map([ |
| 149 | 392× | (0, Value::Uint(FORMAT_VERSION)), |
| 150 | 392× | (1, Value::Bytes(self.repo.to_vec())), |
| 151 | 392× | (2, Value::Uint(self.serial)), |
| 152 | 392× | ( |
| 153 | 392× | 3, |
| 154 | 392× | Value::map([ |
| 155 | 392× | (0, Self::keys_value(&self.roots)), |
| 156 | 392× | (1, Value::Uint(self.root_threshold as u64)), |
| 157 | 392× | ]), |
| 158 | 392× | ), |
| 159 | | ( |
| 160 | | 4, |
| 161 | | Value::Array( |
| 162 | 392× | self.rules |
| 163 | 392× | .iter() |
| 164 | 396× | .map(|r| { |
| 165 | 396× | let mut v = Value::map([ |
| 166 | 396× | (0, Value::Text(r.pattern.clone())), |
| 167 | 396× | (1, Self::keys_value(&r.keys)), |
| 168 | 396× | (2, Value::Uint(r.threshold as u64)), |
| 169 | 396× | (3, Value::Uint(r.require.tag())), |
| 170 | 396× | (4, Value::Bool(r.allow_conflicts)), |
| 171 | 396× | ]); |
| 172 | | |
| 173 | | |
| 174 | 396× | if !r.subjects.is_empty() { |
| 175 | 101× | if let Value::Map(m) = &mut v { |
| 176 | 101× | m.insert( |
| 177 | 101× | 5, |
| 178 | 101× | Value::Array( |
| 179 | 101× | r.subjects.iter().map(SubjectGrant::to_value).collect(), |
| 180 | 101× | ), |
| 181 | 101× | ); |
| 182 | 101× | } |
| 183 | 295× | } |
| 184 | 396× | v |
| 185 | 396× | }) |
| 186 | 392× | .collect(), |
| 187 | | ), |
| 188 | | ), |
| 189 | | ( |
| 190 | | 5, |
| 191 | | Value::Array( |
| 192 | 392× | self.identities |
| 193 | 392× | .iter() |
| 194 | 733× | .map(|i| Value::Bytes(i.encode())) |
| 195 | 392× | .collect(), |
| 196 | | ), |
| 197 | | ), |
| 198 | 392× | (6, Self::keys_value(&self.revoked)), |
| 199 | 392× | (7, Value::Uint(self.expires)), |
| 200 | | ]) |
| 201 | 392× | } |
| 202 | |
|
| 203 | 392× | pub fn encode(&self) -> Vec<u8> { |
| 204 | 392× | cbor::encode(&self.to_value()) |
| 205 | 392× | } |
| 206 | |
|
| 207 | 209× | pub fn hash(&self) -> ObjectRef { |
| 208 | 209× | hash_object(ObjectType::Policy, &self.encode()) |
| 209 | 209× | } |
| 210 | |
|
| 211 | 980× | pub fn decode(b: &[u8]) -> Result<Policy, ObjectError> { |
| 212 | 980× | let v = cbor::decode(b)?; |
| 213 | 980× | let m = map(&v, "policy")?; |
| 214 | 980× | check_version(m)?; |
| 215 | 980× | let roots = map(field(m, 3, "roots")?, "roots")?; |
| 216 | 980× | let mut rules = Vec::new(); |
| 217 | 982× | for r in array(field(m, 4, "rules")?, "rules")? { |
| 218 | 982× | let rm = map(r, "rule")?; |
| 219 | 982× | rules.push(Rule { |
| 220 | 982× | pattern: text(field(rm, 0, "pattern")?, "pattern")?.to_owned(), |
| 221 | 982× | keys: Self::keys_from(field(rm, 1, "rule keys")?, "rule keys")?, |
| 222 | 982× | threshold: uint(field(rm, 2, "threshold")?, "threshold")? as u32, |
| 223 | 982× | require: Require::from_tag(uint(field(rm, 3, "require")?, "require")?)?, |
| 224 | 982× | allow_conflicts: field(rm, 4, "allow_conflicts")? |
| 225 | 982× | .as_bool() |
| 226 | 982× | .ok_or_else(|| ObjectError::Schema("allow_conflicts: expected bool".into()))?, |
| 227 | 982× | subjects: match rm.get(&5) { |
| 228 | 682× | None => vec![], |
| 229 | 300× | Some(v) => array(v, "subjects")? |
| 230 | 300× | .iter() |
| 231 | 300× | .map(SubjectGrant::from_value) |
| 232 | 300× | .collect::<Result<_, _>>()?, |
| 233 | | }, |
| 234 | | }); |
| 235 | | } |
| 236 | 980× | let identities = array(field(m, 5, "identities")?, "identities")? |
| 237 | 980× | .iter() |
| 238 | 1794× | .map(|i| Identity::decode(bytes(i, "identity")?)) |
| 239 | 980× | .collect::<Result<Vec<_>, _>>()?; |
| 240 | 980× | let p = Policy { |
| 241 | 980× | repo: bytes(field(m, 1, "repo")?, "repo")? |
| 242 | 980× | .try_into() |
| 243 | 980× | .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?, |
| 244 | 980× | serial: uint(field(m, 2, "serial")?, "serial")?, |
| 245 | 980× | roots: Self::keys_from(field(roots, 0, "root keys")?, "root keys")?, |
| 246 | 980× | root_threshold: uint(field(roots, 1, "root threshold")?, "root threshold")? as u32, |
| 247 | 980× | rules, |
| 248 | 980× | identities, |
| 249 | 980× | revoked: Self::keys_from(field(m, 6, "revoked")?, "revoked")?, |
| 250 | 980× | expires: uint(field(m, 7, "expires")?, "expires")?, |
| 251 | | }; |
| 252 | 980× | if p.root_threshold == 0 || p.root_threshold as usize > p.roots.len() { |
| 253 | 1× | return Err(ObjectError::Schema( |
| 254 | 1× | "root threshold must be between 1 and the number of root keys".into(), |
| 255 | 1× | )); |
| 256 | 979× | } |
| 257 | 980× | for r in &p.rules { |
| 258 | 980× | if r.threshold == 0 || r.threshold as usize > r.keys.len() + r.subjects.len() { |
| 259 | 1× | return Err(ObjectError::Schema(format!( |
| 260 | 1× | "rule {:?}: threshold must be between 1 and the number of keys and subjects", |
| 261 | 1× | r.pattern |
| 262 | 1× | ))); |
| 263 | 979× | } |
| 264 | | } |
| 265 | 978× | Ok(p) |
| 266 | 980× | } |
| 267 | |
|
| 268 | 346× | pub fn identity(&self, key: &KeyId) -> Option<&Identity> { |
| 269 | 515× | self.identities.iter().find(|i| i.hash() == *key) |
| 270 | 346× | } |
| 271 | |
|
| 272 | 701× | pub fn is_revoked(&self, key: &KeyId) -> bool { |
| 273 | 701× | self.revoked.contains(key) |
| 274 | 701× | } |
| 275 | |
|
| 276 | 7× | pub fn is_expired(&self, now_ms: u64) -> bool { |
| 277 | 7× | now_ms > self.expires |
| 278 | 7× | } |
| 279 | |
|
| 280 | | |
| 281 | 371× | pub fn rule_for(&self, reference: &str) -> Option<&Rule> { |
| 282 | 371× | self.rules |
| 283 | 371× | .iter() |
| 284 | 372× | .find(|r| glob_matches(&r.pattern, reference)) |
| 285 | 371× | } |
| 286 | |
|
| 287 | | |
| 288 | | |
| 289 | | |
| 290 | 48× | pub fn subject_grant( |
| 291 | 48× | &self, |
| 292 | 48× | reference: &str, |
| 293 | 48× | issuer: &KeyId, |
| 294 | 48× | subject: &str, |
| 295 | 48× | scope: identity::Scope, |
| 296 | 48× | ) -> bool { |
| 297 | 48× | !self.is_revoked(issuer) |
| 298 | 47× | && self.rule_for(reference).is_some_and(|r| { |
| 299 | 51× | r.subjects.iter().any(|g| { |
| 300 | 51× | g.issuer == *issuer && g.scope.has(scope) && glob_matches(&g.pattern, subject) |
| 301 | 51× | }) |
| 302 | 47× | }) |
| 303 | 48× | } |
| 304 | |
|
| 305 | | |
| 306 | 149× | pub fn authorizes(&self, key: &KeyId, reference: &str) -> bool { |
| 307 | 149× | !self.is_revoked(key) |
| 308 | 148× | && self |
| 309 | 148× | .rule_for(reference) |
| 310 | 148× | .is_some_and(|r| r.keys.contains(key)) |
| 311 | 149× | } |
| 312 | |
|
| 313 | | |
| 314 | | |
| 315 | | |
| 316 | 99× | pub fn verify_signatures( |
| 317 | 99× | &self, |
| 318 | 99× | sigs: &[Signature], |
| 319 | 99× | trusted: &Policy, |
| 320 | 99× | ) -> Result<Vec<KeyId>, String> { |
| 321 | 99× | let target = self.hash(); |
| 322 | 99× | let mut signers = Vec::new(); |
| 323 | 130× | for sig in sigs { |
| 324 | 130× | if !trusted.roots.contains(&sig.key) || trusted.is_revoked(&sig.key) { |
| 325 | 25× | continue; |
| 326 | 105× | } |
| 327 | 105× | let Some(id) = trusted |
| 328 | 105× | .identity(&sig.key) |
| 329 | 105× | .or_else(|| self.identity(&sig.key)) |
| 330 | | else { |
| 331 | 0× | continue; |
| 332 | | }; |
| 333 | 105× | if identity::verify(id, sig, Domain::Policy, &target).is_ok() |
| 334 | 105× | && !signers.contains(&sig.key) |
| 335 | 104× | { |
| 336 | 104× | signers.push(sig.key); |
| 337 | 104× | } |
| 338 | | } |
| 339 | 99× | if (signers.len() as u32) < trusted.root_threshold { |
| 340 | 6× | return Err(format!( |
| 341 | 6× | "policy needs {} root signature(s), has {}", |
| 342 | 6× | trusted.root_threshold, |
| 343 | 6× | signers.len() |
| 344 | 6× | )); |
| 345 | 93× | } |
| 346 | 93× | Ok(signers) |
| 347 | 99× | } |
| 348 | |
|
| 349 | | |
| 350 | | |
| 351 | | |
| 352 | | |
| 353 | 40× | pub fn verify_bootstrap(&self, sigs: &[Signature]) -> Result<KeyId, String> { |
| 354 | 40× | let target = self.hash(); |
| 355 | 40× | for sig in sigs { |
| 356 | 40× | if !self.roots.contains(&sig.key) || self.is_revoked(&sig.key) { |
| 357 | 0× | continue; |
| 358 | 40× | } |
| 359 | 40× | if let Some(id) = self.identity(&sig.key) { |
| 360 | 40× | if identity::verify(id, sig, Domain::Policy, &target).is_ok() { |
| 361 | 40× | return Ok(sig.key); |
| 362 | 0× | } |
| 363 | 0× | } |
| 364 | | } |
| 365 | 0× | Err("policy carries no valid signature by one of its own root keys".into()) |
| 366 | 40× | } |
| 367 | |
|
| 368 | | |
| 369 | 24× | pub fn single_root(repo: [u8; 16], root: Identity, expires: u64) -> Policy { |
| 370 | 24× | let key = root.hash(); |
| 371 | 24× | Policy { |
| 372 | 24× | repo, |
| 373 | 24× | serial: 1, |
| 374 | 24× | roots: vec![key], |
| 375 | 24× | root_threshold: 1, |
| 376 | 24× | rules: vec![Rule { |
| 377 | 24× | pattern: "*".into(), |
| 378 | 24× | keys: vec![key], |
| 379 | 24× | threshold: 1, |
| 380 | 24× | require: Require::Any, |
| 381 | 24× | allow_conflicts: true, |
| 382 | 24× | subjects: vec![], |
| 383 | 24× | }], |
| 384 | 24× | identities: vec![root], |
| 385 | 24× | revoked: vec![], |
| 386 | 24× | expires, |
| 387 | 24× | } |
| 388 | 24× | } |
| 389 | |
|
| 390 | | |
| 391 | 10× | pub fn mentions(&self, key: &KeyId) -> bool { |
| 392 | 10× | self.roots.contains(key) |
| 393 | 1× | || self.rules.iter().any(|r| r.keys.contains(key)) |
| 394 | 0× | || self.identity(key).is_some() |
| 395 | 10× | } |
| 396 | |
|
| 397 | | |
| 398 | | |
| 399 | | |
| 400 | 42× | pub fn succeeds(&self, rule_key: &KeyId, key: &KeyId) -> bool { |
| 401 | 42× | if self.is_revoked(key) { |
| 402 | 0× | return false; |
| 403 | 42× | } |
| 404 | 42× | let mut cur = Some(*rule_key); |
| 405 | 42× | for _ in 0..32 { |
| 406 | 53× | match cur { |
| 407 | 53× | Some(k) if k == *key => return true, |
| 408 | 23× | Some(k) => cur = self.identity(&k).and_then(|i| i.previous), |
| 409 | 12× | None => return false, |
| 410 | | } |
| 411 | | } |
| 412 | 0× | false |
| 413 | 42× | } |
| 414 | |
|
| 415 | | |
| 416 | | |
| 417 | | |
| 418 | | |
| 419 | 9× | pub fn substitute_key(&mut self, old: &KeyId, new: Identity) { |
| 420 | 9× | let nk = new.hash(); |
| 421 | 18× | for k in self |
| 422 | 9× | .roots |
| 423 | 9× | .iter_mut() |
| 424 | 9× | .chain(self.rules.iter_mut().flat_map(|r| r.keys.iter_mut())) |
| 425 | | { |
| 426 | 18× | if k == old { |
| 427 | 18× | *k = nk; |
| 428 | 18× | } |
| 429 | | } |
| 430 | 9× | for r in &mut self.rules { |
| 431 | 9× | r.keys.sort(); |
| 432 | 9× | r.keys.dedup(); |
| 433 | 9× | } |
| 434 | 9× | self.roots.sort(); |
| 435 | 9× | self.roots.dedup(); |
| 436 | 9× | if self.identity(&nk).is_none() { |
| 437 | 9× | self.identities.push(new); |
| 438 | 9× | } |
| 439 | 9× | } |
| 440 | |
|
| 441 | | |
| 442 | 0× | pub fn identity_map(&self) -> BTreeMap<KeyId, &Identity> { |
| 443 | 0× | self.identities.iter().map(|i| (i.hash(), i)).collect() |
| 444 | 0× | } |
| 445 | | } |
| 446 | |
|
| 447 | | #[cfg(test)] |
| 448 | | mod tests { |
| 449 | | use super::*; |
| 450 | | use crate::identity::SecretKey; |
| 451 | |
|
| 452 | 7× | fn sk(n: u8) -> SecretKey { |
| 453 | 7× | SecretKey { |
| 454 | 7× | ed_seed: [n; 32], |
| 455 | 7× | ml_seed: [n + 100; 32], |
| 456 | 7× | } |
| 457 | 7× | } |
| 458 | |
|
| 459 | | #[test] |
| 460 | 1× | fn globs() { |
| 461 | 1× | assert!(glob_matches("*", "main")); |
| 462 | 1× | assert!(glob_matches("release/*", "release/1.0")); |
| 463 | 1× | assert!(!glob_matches("release/*", "main")); |
| 464 | 1× | assert!(glob_matches("main", "main")); |
| 465 | 1× | assert!(!glob_matches("main", "main2")); |
| 466 | 1× | assert!(glob_matches("*-rc*", "1.0-rc2")); |
| 467 | 1× | assert!(glob_matches("", "")); |
| 468 | 1× | } |
| 469 | |
|
| 470 | | |
| 471 | | |
| 472 | | |
| 473 | | #[test] |
| 474 | 1× | fn a_rule_grants_certified_subjects() { |
| 475 | | use crate::identity::Scope; |
| 476 | 1× | let root = sk(1).identity("Root", "r@x", 0); |
| 477 | 1× | let hub = sk(3).identity("Hub", "h@x", 0); |
| 478 | 1× | let mut p = Policy::single_root([9; 16], root.clone(), 10); |
| 479 | 1× | let plain = p.encode(); |
| 480 | 1× | p.rules[0].subjects.push(SubjectGrant { |
| 481 | 1× | pattern: "repo:andy/demo:bookmark:main".into(), |
| 482 | 1× | issuer: hub.hash(), |
| 483 | 1× | scope: Scope::REFS | Scope::CHANGES, |
| 484 | 1× | }); |
| 485 | 1× | p.rules[0].subjects.push(SubjectGrant { |
| 486 | 1× | pattern: "agent:*@andy".into(), |
| 487 | 1× | issuer: hub.hash(), |
| 488 | 1× | scope: Scope::CHANGES, |
| 489 | 1× | }); |
| 490 | 1× | let back = Policy::decode(&p.encode()).unwrap(); |
| 491 | 1× | assert_eq!(back, p); |
| 492 | 1× | let mut again = back.clone(); |
| 493 | 1× | again.rules[0].subjects.clear(); |
| 494 | 1× | assert_eq!( |
| 495 | 1× | again.encode(), |
| 496 | | plain, |
| 497 | | "no subjects, the same bytes as before" |
| 498 | | ); |
| 499 | |
|
| 500 | 1× | let main = "repo:andy/demo:bookmark:main"; |
| 501 | 1× | assert!(p.subject_grant("main", &hub.hash(), main, Scope::REFS)); |
| 502 | 1× | assert!(p.subject_grant("main", &hub.hash(), "agent:claude@andy", Scope::CHANGES)); |
| 503 | 1× | assert!( |
| 504 | 1× | !p.subject_grant("main", &hub.hash(), "agent:claude@andy", Scope::REFS), |
| 505 | | "not its scope" |
| 506 | | ); |
| 507 | 1× | assert!( |
| 508 | 1× | !p.subject_grant( |
| 509 | 1× | "main", |
| 510 | 1× | &hub.hash(), |
| 511 | 1× | "repo:andy/demo:bookmark:dev", |
| 512 | 1× | Scope::REFS |
| 513 | 1× | ), |
| 514 | | "not its pattern" |
| 515 | | ); |
| 516 | 1× | assert!( |
| 517 | 1× | !p.subject_grant("main", &root.hash(), main, Scope::REFS), |
| 518 | | "not its issuer" |
| 519 | | ); |
| 520 | 1× | p.revoked.push(hub.hash()); |
| 521 | 1× | assert!( |
| 522 | 1× | !p.subject_grant("main", &hub.hash(), main, Scope::REFS), |
| 523 | | "a revoked issuer grants nothing" |
| 524 | | ); |
| 525 | |
|
| 526 | | |
| 527 | 1× | let mut only = Policy::single_root([9; 16], root, 10); |
| 528 | 1× | only.rules[0].keys.clear(); |
| 529 | 1× | only.rules[0].subjects.push(SubjectGrant { |
| 530 | 1× | pattern: "*".into(), |
| 531 | 1× | issuer: hub.hash(), |
| 532 | 1× | scope: Scope::REFS, |
| 533 | 1× | }); |
| 534 | 1× | assert!(Policy::decode(&only.encode()).is_ok()); |
| 535 | 1× | only.rules[0].subjects.clear(); |
| 536 | 1× | assert!(Policy::decode(&only.encode()).is_err(), "nobody to sign"); |
| 537 | 1× | } |
| 538 | |
|
| 539 | | #[test] |
| 540 | 1× | fn round_trip_and_rules() { |
| 541 | 1× | let root = sk(1).identity("Root", "r@x", 0); |
| 542 | 1× | let dev = sk(2).identity("Dev", "d@x", 0); |
| 543 | 1× | let mut p = Policy::single_root([9; 16], root.clone(), 10); |
| 544 | 1× | p.rules.insert( |
| 545 | | 0, |
| 546 | 1× | Rule { |
| 547 | 1× | pattern: "release/*".into(), |
| 548 | 1× | keys: vec![root.hash()], |
| 549 | 1× | threshold: 1, |
| 550 | 1× | require: Require::Signed, |
| 551 | 1× | allow_conflicts: false, |
| 552 | 1× | subjects: vec![], |
| 553 | 1× | }, |
| 554 | | ); |
| 555 | 1× | p.rules[1].keys.push(dev.hash()); |
| 556 | 1× | p.identities.push(dev.clone()); |
| 557 | 1× | p.revoked.push(ObjectRef([7; 32])); |
| 558 | 1× | let d = Policy::decode(&p.encode()).unwrap(); |
| 559 | 1× | assert_eq!(d.rules.len(), 2); |
| 560 | 1× | assert_eq!(d.rules[0].require, Require::Signed); |
| 561 | 1× | assert_eq!(d.identity(&dev.hash()), Some(&dev)); |
| 562 | 1× | assert!(d.authorizes(&dev.hash(), "main")); |
| 563 | 1× | assert!(!d.authorizes(&dev.hash(), "release/1")); |
| 564 | 1× | assert!(d.authorizes(&root.hash(), "release/1")); |
| 565 | 1× | assert!(d.is_expired(11) && !d.is_expired(10)); |
| 566 | 1× | assert_eq!(d.hash(), p.hash()); |
| 567 | |
|
| 568 | 1× | let mut bad = p.clone(); |
| 569 | 1× | bad.root_threshold = 5; |
| 570 | 1× | assert!(Policy::decode(&bad.encode()).is_err()); |
| 571 | 1× | } |
| 572 | |
|
| 573 | | #[test] |
| 574 | 1× | fn signatures_need_root_threshold() { |
| 575 | 1× | let r1 = sk(1); |
| 576 | 1× | let r2 = sk(2); |
| 577 | 1× | let i1 = r1.identity("a", "a", 0); |
| 578 | 1× | let i2 = r2.identity("b", "b", 0); |
| 579 | 1× | let mut p = Policy::single_root([1; 16], i1.clone(), 100); |
| 580 | 1× | p.roots.push(i2.hash()); |
| 581 | 1× | p.roots.sort(); |
| 582 | 1× | p.root_threshold = 2; |
| 583 | 1× | p.identities.push(i2.clone()); |
| 584 | 1× | let h = p.hash(); |
| 585 | 1× | let s1 = r1.sign(Domain::Policy, &i1.hash(), &h); |
| 586 | 1× | let s2 = r2.sign(Domain::Policy, &i2.hash(), &h); |
| 587 | 1× | assert!(p.verify_signatures(std::slice::from_ref(&s1), &p).is_err()); |
| 588 | 1× | assert_eq!( |
| 589 | 1× | p.verify_signatures(&[s1.clone(), s2.clone()], &p) |
| 590 | 1× | .unwrap() |
| 591 | 1× | .len(), |
| 592 | | 2 |
| 593 | | ); |
| 594 | | |
| 595 | 1× | assert!(p.verify_signatures(&[s1.clone(), s1.clone()], &p).is_err()); |
| 596 | 1× | let stranger = sk(3); |
| 597 | 1× | let si = stranger.identity("s", "s", 0); |
| 598 | 1× | let s3 = stranger.sign(Domain::Policy, &si.hash(), &h); |
| 599 | 1× | assert!(p.verify_signatures(&[s1.clone(), s3], &p).is_err()); |
| 600 | | |
| 601 | 1× | let mut next = p.clone(); |
| 602 | 1× | next.serial = 2; |
| 603 | 1× | next.revoked.push(i2.hash()); |
| 604 | 1× | let nh = next.hash(); |
| 605 | 1× | let n1 = r1.sign(Domain::Policy, &i1.hash(), &nh); |
| 606 | 1× | let n2 = r2.sign(Domain::Policy, &i2.hash(), &nh); |
| 607 | 1× | assert_eq!(next.verify_signatures(&[n1, n2], &p).unwrap().len(), 2); |
| 608 | 1× | } |
| 609 | | } |