OXYGENHUB
◆ carried-not-removed oxygen / crates / oxy-core / src / repo / policy.rs
19.3 KiB · Rust · last changed Certified keys may push under a subject grant (keyless step 4b) 18 days ago source blame history raw
1
//! Trust policy management and enforcement (spec §14.5).
2
//!
3
//! A repository with a policy verifies everything it receives: every change
4
//! must be authenticated by a key the policy authorizes for the bookmark,
5
//! every bookmark update must carry an attestation by such a key, and a new
6
//! policy must be signed by a threshold of the roots of the policy it replaces.
7
8
use super::*;
9
use crate::identity::SecretKey;
10
use crate::policy::{Policy, Require, Rule};
11
use crate::protocol::SignedPolicy;
12
13
/// One year, the default policy lifetime.
14
const POLICY_LIFETIME_MS: u64 = 365 * 24 * 60 * 60 * 1000;
15
16
/// A signed policy update: applied when it met the root threshold, else a
17
/// proposal carrying the signatures gathered so far (spec §14.5).
18
#[derive(Clone, Debug)]
19
pub struct PolicyUpdate {
20
    pub policy: Policy,
21
    pub sigs: Vec<Signature>,
22
    pub applied: bool,
23
}
24
25
impl PolicyUpdate {
26
    pub fn signed(&self) -> SignedPolicy {
27
        SignedPolicy {
28
            policy: self.policy.clone(),
29
            sigs: self.sigs.clone(),
30
        }
31
    }
32
}
33
34
impl Repo {
35
    /// The current policy and its signatures, if the repository has one.
36
    pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> {
37
        let Some(r) = self.view()?.policy else {
38
            return Ok(None);
39
        };
40
        let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?;
41
        Ok(Some((
42
            r,
43
            SignedPolicy {
44
                policy,
45
                sigs: self.store.signatures(&r)?,
46
            },
47
        )))
48
    }
49
50
    /// Store a policy with its signatures and point the `policy` reference at it.
51
    pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> {
52
        let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?;
53
        for sig in &sp.sigs {
54
            self.store.put_signature(&r, sig)?;
55
        }
56
        for id in &sp.policy.identities {
57
            self.store.put_identity(id)?;
58
        }
59
        let dir = self.oxy.join("policies");
60
        fs::create_dir_all(&dir)?;
61
        write_atomic(
62
            &dir.join(format!("{:020}", sp.policy.serial)),
63
            r.to_hex().as_bytes(),
64
        )?;
65
        let (op_ref, op) = self.head_op()?;
66
        if op.view.policy != Some(r) {
67
            let mut view = op.view.clone();
68
            view.policy = Some(r);
69
            self.record_op(&[op_ref], command, view)?;
70
        }
71
        Ok(r)
72
    }
73
74
    /// Every policy this repository has applied, ascending by serial, with
75
    /// its signatures. Policies applied before this index existed are
76
    /// missing from it, except the current one.
77
    pub fn policy_history(&self) -> Result<Vec<SignedPolicy>> {
78
        let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new();
79
        if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) {
80
            for e in rd.flatten() {
81
                let Ok(hex) = fs::read_to_string(e.path()) else {
82
                    continue;
83
                };
84
                let Some(r) = ObjectRef::from_hex(hex.trim()) else {
85
                    continue;
86
                };
87
                let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else {
88
                    continue;
89
                };
90
                let policy = Policy::decode(&bytes)?;
91
                out.insert(
92
                    policy.serial,
93
                    SignedPolicy {
94
                        policy,
95
                        sigs: self.store.signatures(&r)?,
96
                    },
97
                );
98
            }
99
        }
100
        if let Some((_, sp)) = self.current_policy()? {
101
            out.insert(sp.policy.serial, sp);
102
        }
103
        Ok(out.into_values().collect())
104
    }
105
106
    /// Verify and apply a chain of policies ending in `newest`, each
107
    /// against the one before it (spec §14.5). `chain` holds earlier
108
    /// policies in any order; only those after the current serial are used.
109
    pub(crate) fn adopt_policy_chain(
110
        &self,
111
        chain: &[SignedPolicy],
112
        newest: &SignedPolicy,
113
        command: &str,
114
    ) -> std::result::Result<(), String> {
115
        let mut current = self
116
            .current_policy()
117
            .map_err(|e| e.to_string())?
118
            .map(|(_, sp)| sp.policy);
119
        let mut steps: Vec<&SignedPolicy> = chain
120
            .iter()
121
            .filter(|sp| {
122
                current.as_ref().is_none_or(|c| sp.policy.serial > c.serial)
123
                    && sp.policy.serial < newest.policy.serial
124
            })
125
            .collect();
126
        steps.sort_by_key(|sp| sp.policy.serial);
127
        steps.push(newest);
128
        for sp in steps {
129
            self.check_policy(sp, current.as_ref())?;
130
            self.set_policy(
131
                sp,
132
                &format!("{command} (policy serial {})", sp.policy.serial),
133
            )
134
            .map_err(|e| e.to_string())?;
135
            current = Some(sp.policy.clone());
136
        }
137
        Ok(())
138
    }
139
140
    /// Validate an incoming policy against the one we trust (`None` when
141
    /// bootstrapping: the policy then vouches for itself).
142
    pub fn check_policy(
143
        &self,
144
        sp: &SignedPolicy,
145
        trusted: Option<&Policy>,
146
    ) -> std::result::Result<(), String> {
147
        if sp.policy.repo != self.id().map_err(|e| e.to_string())? {
148
            return Err("policy is for a different repository".into());
149
        }
150
        if let Some(t) = trusted {
151
            if sp.policy.serial <= t.serial {
152
                return Err(format!(
153
                    "policy serial {} is not greater than the current {}",
154
                    sp.policy.serial, t.serial
155
                ));
156
            }
157
        }
158
        match trusted {
159
            Some(t) => sp.policy.verify_signatures(&sp.sigs, t)?,
160
            None => vec![sp.policy.verify_bootstrap(&sp.sigs)?],
161
        };
162
        Ok(())
163
    }
164
165
    /// Sign `policy` with the configured key and apply it if the current
166
    /// policy's root threshold is met; otherwise return it as a proposal.
167
    fn sign_policy(&self, policy: Policy, command: &str) -> Result<PolicyUpdate> {
168
        let Some((key, sk)) = self.secret_key()? else {
169
            return fail("no secret key: run `oxy identity create` first");
170
        };
171
        self.sign_policy_with(policy, &[(key, &sk)], command)
172
    }
173
174
    /// Sign with every listed key; the first must be a root of the current
175
    /// policy (the others may be roots of the new policy only, so that a
176
    /// fresh clone can bootstrap from it).
177
    pub(crate) fn sign_policy_with(
178
        &self,
179
        policy: Policy,
180
        signers: &[(KeyId, &SecretKey)],
181
        command: &str,
182
    ) -> Result<PolicyUpdate> {
183
        let trusted = self
184
            .current_policy()?
185
            .map(|(_, sp)| sp.policy)
186
            .unwrap_or_else(|| policy.clone());
187
        if !signers
188
            .first()
189
            .is_some_and(|(k, _)| trusted.roots.contains(k))
190
        {
191
            return fail("your key is not a root key of the current policy");
192
        }
193
        let target = policy.hash();
194
        let sigs = signers
195
            .iter()
196
            .map(|(k, sk)| sk.sign(Domain::Policy, k, &target))
197
            .collect();
198
        self.gather(SignedPolicy { policy, sigs }, &trusted, command)
199
    }
200
201
    /// Apply `sp` when its signatures meet `trusted`'s threshold.
202
    fn gather(&self, sp: SignedPolicy, trusted: &Policy, command: &str) -> Result<PolicyUpdate> {
203
        let applied = sp.policy.verify_signatures(&sp.sigs, trusted).is_ok();
204
        if applied {
205
            self.set_policy(&sp, command)?;
206
        }
207
        Ok(PolicyUpdate {
208
            policy: sp.policy,
209
            sigs: sp.sigs,
210
            applied,
211
        })
212
    }
213
214
    /// Add your signature to a proposal made by another root holder; the
215
    /// policy is applied once the current root threshold is met.
216
    pub fn policy_sign(&self, proposal: SignedPolicy) -> Result<PolicyUpdate> {
217
        let Some((key, sk)) = self.secret_key()? else {
218
            return fail("no secret key: run `oxy identity create` first");
219
        };
220
        let Some((_, current)) = self.current_policy()? else {
221
            return fail("this repository has no policy; run `oxy policy init` or fetch one");
222
        };
223
        if proposal.policy.repo != self.id()? {
224
            return fail("the proposal is for a different repository");
225
        }
226
        if proposal.policy.serial <= current.policy.serial {
227
            return fail(format!(
228
                "the proposal's serial {} is not greater than the current {}",
229
                proposal.policy.serial, current.policy.serial
230
            ));
231
        }
232
        if !current.policy.roots.contains(&key) {
233
            return fail("your key is not a root key of the current policy");
234
        }
235
        let mut sigs: Vec<Signature> = proposal.sigs.into_iter().filter(|s| s.key != key).collect();
236
        sigs.push(sk.sign(Domain::Policy, &key, &proposal.policy.hash()));
237
        self.gather(
238
            SignedPolicy {
239
                policy: proposal.policy,
240
                sigs,
241
            },
242
            &current.policy,
243
            "oxy policy sign",
244
        )
245
    }
246
247
    /// Create the initial policy with the configured identity as sole root.
248
    pub fn policy_init(&self) -> Result<PolicyUpdate> {
249
        if self.current_policy()?.is_some() {
250
            return fail("this repository already has a policy");
251
        }
252
        let Some((key, _)) = self.secret_key()? else {
253
            return fail("no secret key: run `oxy identity create` first");
254
        };
255
        let identity = self.identity(&key)?;
256
        let policy = Policy::single_root(
257
            self.id()?,
258
            identity,
259
            Hlc::now().wall_ms + POLICY_LIFETIME_MS,
260
        );
261
        self.sign_policy(policy, "oxy policy init")
262
    }
263
264
    /// Make `identity` a root key.
265
    pub fn policy_root_add(&self, identity: Identity) -> Result<PolicyUpdate> {
266
        let mut p = self.next_policy()?;
267
        let key = identity.hash();
268
        if p.roots.contains(&key) {
269
            return fail(format!("{} is already a root", key.short(12)));
270
        }
271
        p.roots.push(key);
272
        p.roots.sort();
273
        p.revoked.retain(|k| k != &key);
274
        if p.identity(&key).is_none() {
275
            p.identities.push(identity);
276
        }
277
        self.sign_policy(p, &format!("oxy policy root add {}", key.short(12)))
278
    }
279
280
    /// Drop a root key; the remaining roots must still meet the threshold.
281
    pub fn policy_root_remove(&self, key: &KeyId) -> Result<PolicyUpdate> {
282
        let mut p = self.next_policy()?;
283
        if !p.roots.contains(key) {
284
            return fail(format!("{} is not a root", key.short(12)));
285
        }
286
        p.roots.retain(|k| k != key);
287
        if (p.roots.len() as u32) < p.root_threshold {
288
            return fail(format!(
289
                "removing it would leave {} root(s) for a threshold of {}",
290
                p.roots.len(),
291
                p.root_threshold
292
            ));
293
        }
294
        self.sign_policy(p, &format!("oxy policy root remove {}", key.short(12)))
295
    }
296
297
    /// Require `n` root signatures on future policy updates.
298
    pub fn policy_set_threshold(&self, n: u32) -> Result<PolicyUpdate> {
299
        let mut p = self.next_policy()?;
300
        if n == 0 || n as usize > p.roots.len() {
301
            return fail(format!(
302
                "the threshold must be between 1 and the number of roots ({})",
303
                p.roots.len()
304
            ));
305
        }
306
        p.root_threshold = n;
307
        self.sign_policy(p, &format!("oxy policy root threshold {n}"))
308
    }
309
310
    pub(crate) fn next_policy(&self) -> Result<Policy> {
311
        let Some((_, sp)) = self.current_policy()? else {
312
            return fail("this repository has no policy; run `oxy policy init`");
313
        };
314
        let mut p = sp.policy;
315
        p.serial += 1;
316
        p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS;
317
        Ok(p)
318
    }
319
320
    /// Let `identity` update references matching `pattern` (default every reference).
321
    pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result<PolicyUpdate> {
322
        let mut p = self.next_policy()?;
323
        let key = identity.hash();
324
        let pattern = pattern.unwrap_or("*");
325
        p.revoked.retain(|k| k != &key);
326
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
327
            Some(rule) => {
328
                if !rule.keys.contains(&key) {
329
                    rule.keys.push(key);
330
                    rule.keys.sort();
331
                }
332
            }
333
            None => p.rules.push(Rule {
334
                pattern: pattern.into(),
335
                keys: vec![key],
336
                threshold: 1,
337
                require: Require::Any,
338
                allow_conflicts: true,
339
                subjects: vec![],
340
            }),
341
        }
342
        if p.identity(&key).is_none() {
343
            p.identities.push(identity);
344
        }
345
        self.sign_policy(p, &format!("oxy policy grant {}", key.short(12)))
346
    }
347
348
    /// Let holders of certificates `issuer` gives, for subjects matching
349
    /// `subject`, sign within `scope` on `pattern` (default every
350
    /// bookmark; §14.7). The issuer's identity is embedded so verifiers need
351
    /// no fetch.
352
    pub fn policy_grant_subject(
353
        &self,
354
        subject: &str,
355
        issuer: Identity,
356
        scope: crate::identity::Scope,
357
        pattern: Option<&str>,
358
    ) -> Result<PolicyUpdate> {
359
        if subject.trim().is_empty() {
360
            return fail("a subject pattern is needed, such as repo:OWNER/NAME:bookmark:main");
361
        }
362
        let mut p = self.next_policy()?;
363
        let key = issuer.hash();
364
        let pattern = pattern.unwrap_or("*");
365
        let grant = crate::policy::SubjectGrant {
366
            pattern: subject.into(),
367
            issuer: key,
368
            scope,
369
        };
370
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
371
            Some(rule) => {
372
                rule.subjects
373
                    .retain(|g| !(g.pattern == grant.pattern && g.issuer == grant.issuer));
374
                rule.subjects.push(grant);
375
            }
376
            None => p.rules.push(Rule {
377
                pattern: pattern.into(),
378
                keys: vec![],
379
                threshold: 1,
380
                require: Require::Any,
381
                allow_conflicts: true,
382
                subjects: vec![grant],
383
            }),
384
        }
385
        if p.identity(&key).is_none() {
386
            p.identities.push(issuer);
387
        }
388
        self.sign_policy(p, &format!("oxy policy grant-subject {subject}"))
389
    }
390
391
    /// Revoke `key`: it may no longer update anything, and existing rules
392
    /// drop it. A root key can be revoked by another root as long as the
393
    /// remaining roots still meet the threshold.
394
    pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> {
395
        let mut p = self.next_policy()?;
396
        if p.roots.contains(key) {
397
            let own = self.secret_key()?.map(|(k, _)| k);
398
            if own == Some(*key) {
399
                return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first");
400
            }
401
            p.roots.retain(|k| k != key);
402
            if (p.roots.len() as u32) < p.root_threshold {
403
                return fail(format!(
404
                    "revoking it would leave {} root(s) for a threshold of {}",
405
                    p.roots.len(),
406
                    p.root_threshold
407
                ));
408
            }
409
        }
410
        for r in &mut p.rules {
411
            r.keys.retain(|k| k != key);
412
        }
413
        // A rule of certified subjects alone stays.
414
        p.rules
415
            .retain(|r| !r.keys.is_empty() || !r.subjects.is_empty());
416
        if !p.revoked.contains(key) {
417
            p.revoked.push(*key);
418
            p.revoked.sort();
419
        }
420
        self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12)))
421
    }
422
423
    /// Whether `change` may be part of `reference` under `policy`: it must be
424
    /// authenticated (by its author or a delegate) with a key the matching
425
    /// rule lists, using the authenticator kind the rule requires. A rule key
426
    /// covers its rotation predecessors, and a delegate's signature counts
427
    /// for the author it acts for.
428
    pub fn change_allowed(
429
        &self,
430
        hash: &ChangeHash,
431
        policy: &Policy,
432
        reference: &str,
433
    ) -> std::result::Result<(), String> {
434
        let rule = policy
435
            .rule_for(reference)
436
            .ok_or_else(|| format!("no policy rule matches {reference:?}"))?;
437
        match self.authenticate_change(hash).map_err(|e| e.to_string())? {
438
            SignatureStatus::Valid { key, sealed, .. } => {
439
                if policy.is_revoked(&key) {
440
                    return Err(format!(
441
                        "change {} is signed by revoked key {}",
442
                        hash.short(12),
443
                        key.short(12)
444
                    ));
445
                }
446
                let author = self
447
                    .store
448
                    .get_change(hash)
449
                    .map(|c| c.author)
450
                    .map_err(|e| e.to_string())?;
451
                let principal = if author.is_zero() { key } else { author };
452
                // A change is checked when it is received: now is a time
453
                // its signer did not choose.
454
                let covered = rule
455
                    .keys
456
                    .iter()
457
                    .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal))
458
                    || self.certified_authorized(
459
                        policy,
460
                        &key,
461
                        reference,
462
                        crate::identity::Scope::CHANGES,
463
                        crate::objects::Hlc::now().wall_ms,
464
                    );
465
                if !covered {
466
                    return Err(format!(
467
                        "change {} is signed by {}, which may not update {reference:?}",
468
                        hash.short(12),
469
                        key.short(12)
470
                    ));
471
                }
472
                match (rule.require, sealed) {
473
                    (Require::Signed, true) => Err(format!(
474
                        "{reference:?} requires individually signed changes; {} is only sealed",
475
                        hash.short(12)
476
                    )),
477
                    (Require::Sealed, false) => Err(format!(
478
                        "{reference:?} requires sealed changes; {} is individually signed",
479
                        hash.short(12)
480
                    )),
481
                    _ => Ok(()),
482
                }
483
            }
484
            SignatureStatus::Unsigned => {
485
                Err(format!("change {} is not signed or sealed", hash.short(12)))
486
            }
487
            SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))),
488
        }
489
    }
490
491
    /// Check every change that `new` adds over `old` for `reference`.
492
    pub(crate) fn state_allowed(
493
        &self,
494
        old_heads: &[ChangeHash],
495
        new_heads: &[ChangeHash],
496
        policy: &Policy,
497
        reference: &str,
498
    ) -> std::result::Result<(), String> {
499
        let added = self
500
            .negotiate(old_heads, new_heads)
501
            .map_err(|e| e.to_string())?;
502
        for h in &added {
503
            self.change_allowed(h, policy, reference)?;
504
        }
505
        if let Some(rule) = policy.rule_for(reference) {
506
            if !rule.allow_conflicts && !added.is_empty() {
507
                let st = self.load_state(new_heads).map_err(|e| e.to_string())?;
508
                if st.materialize().is_conflicted() {
509
                    return Err(format!("{reference:?} does not allow conflicted states"));
510
                }
511
            }
512
        }
513
        Ok(())
514
    }
515
}