OXYGENHUB
# acadb5ed28a1 oxygen / crates / oxy-core / src / repo / policy.rs
17.2 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 1 month 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
            }),
340
        }
341
        if p.identity(&key).is_none() {
342
            p.identities.push(identity);
343
        }
344
        self.sign_policy(p, &format!("oxy policy grant {}", key.short(12)))
345
    }
346
347
    /// Revoke `key`: it may no longer update anything, and existing rules
348
    /// drop it. A root key can be revoked by another root as long as the
349
    /// remaining roots still meet the threshold.
350
    pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> {
351
        let mut p = self.next_policy()?;
352
        if p.roots.contains(key) {
353
            let own = self.secret_key()?.map(|(k, _)| k);
354
            if own == Some(*key) {
355
                return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first");
356
            }
357
            p.roots.retain(|k| k != key);
358
            if (p.roots.len() as u32) < p.root_threshold {
359
                return fail(format!(
360
                    "revoking it would leave {} root(s) for a threshold of {}",
361
                    p.roots.len(),
362
                    p.root_threshold
363
                ));
364
            }
365
        }
366
        for r in &mut p.rules {
367
            r.keys.retain(|k| k != key);
368
        }
369
        p.rules.retain(|r| !r.keys.is_empty());
370
        if !p.revoked.contains(key) {
371
            p.revoked.push(*key);
372
            p.revoked.sort();
373
        }
374
        self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12)))
375
    }
376
377
    /// Whether `change` may be part of `reference` under `policy`: it must be
378
    /// authenticated (by its author or a delegate) with a key the matching
379
    /// rule lists, using the authenticator kind the rule requires. A rule key
380
    /// covers its rotation predecessors, and a delegate's signature counts
381
    /// for the author it acts for.
382
    pub fn change_allowed(
383
        &self,
384
        hash: &ChangeHash,
385
        policy: &Policy,
386
        reference: &str,
387
    ) -> std::result::Result<(), String> {
388
        let rule = policy
389
            .rule_for(reference)
390
            .ok_or_else(|| format!("no policy rule matches {reference:?}"))?;
391
        match self.authenticate_change(hash).map_err(|e| e.to_string())? {
392
            SignatureStatus::Valid { key, sealed, .. } => {
393
                if policy.is_revoked(&key) {
394
                    return Err(format!(
395
                        "change {} is signed by revoked key {}",
396
                        hash.short(12),
397
                        key.short(12)
398
                    ));
399
                }
400
                let author = self
401
                    .store
402
                    .get_change(hash)
403
                    .map(|c| c.author)
404
                    .map_err(|e| e.to_string())?;
405
                let principal = if author.is_zero() { key } else { author };
406
                let covered = rule
407
                    .keys
408
                    .iter()
409
                    .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal));
410
                if !covered {
411
                    return Err(format!(
412
                        "change {} is signed by {}, which may not update {reference:?}",
413
                        hash.short(12),
414
                        key.short(12)
415
                    ));
416
                }
417
                match (rule.require, sealed) {
418
                    (Require::Signed, true) => Err(format!(
419
                        "{reference:?} requires individually signed changes; {} is only sealed",
420
                        hash.short(12)
421
                    )),
422
                    (Require::Sealed, false) => Err(format!(
423
                        "{reference:?} requires sealed changes; {} is individually signed",
424
                        hash.short(12)
425
                    )),
426
                    _ => Ok(()),
427
                }
428
            }
429
            SignatureStatus::Unsigned => {
430
                Err(format!("change {} is not signed or sealed", hash.short(12)))
431
            }
432
            SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))),
433
        }
434
    }
435
436
    /// Check every change that `new` adds over `old` for `reference`.
437
    pub(crate) fn state_allowed(
438
        &self,
439
        old_heads: &[ChangeHash],
440
        new_heads: &[ChangeHash],
441
        policy: &Policy,
442
        reference: &str,
443
    ) -> std::result::Result<(), String> {
444
        let added = self
445
            .negotiate(old_heads, new_heads)
446
            .map_err(|e| e.to_string())?;
447
        for h in &added {
448
            self.change_allowed(h, policy, reference)?;
449
        }
450
        if let Some(rule) = policy.rule_for(reference) {
451
            if !rule.allow_conflicts && !added.is_empty() {
452
                let st = self.load_state(new_heads).map_err(|e| e.to_string())?;
453
                if st.materialize().is_conflicted() {
454
                    return Err(format!("{reference:?} does not allow conflicted states"));
455
                }
456
            }
457
        }
458
        Ok(())
459
    }
460
}