OXYGENHUB
# ee06a6c557b7 oxygen / crates / oxy-core / src / repo / policy.rs
15.3 KiB · Rust · last changed Identity lifecycle: rotate, renew, delegates, revoke; expiry checks; policy roots, thresholds, proposals and oxy policy sign; policy chains served on refs and carried in push bundles (spec 0.6n) 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 { policy: self.policy.clone(), sigs: self.sigs.clone() }
28
    }
29
}
30
31
impl Repo {
32
    /// The current policy and its signatures, if the repository has one.
33
    pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> {
34
        let Some(r) = self.view()?.policy else { return Ok(None) };
35
        let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?;
36
        Ok(Some((r, SignedPolicy { policy, sigs: self.store.signatures(&r)? })))
37
    }
38
39
    /// Store a policy with its signatures and point the `policy` reference at it.
40
    pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> {
41
        let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?;
42
        for sig in &sp.sigs {
43
            self.store.put_signature(&r, sig)?;
44
        }
45
        for id in &sp.policy.identities {
46
            self.store.put_identity(id)?;
47
        }
48
        let dir = self.oxy.join("policies");
49
        fs::create_dir_all(&dir)?;
50
        write_atomic(&dir.join(format!("{:020}", sp.policy.serial)), r.to_hex().as_bytes())?;
51
        let (op_ref, op) = self.head_op()?;
52
        if op.view.policy != Some(r) {
53
            let mut view = op.view.clone();
54
            view.policy = Some(r);
55
            self.record_op(&[op_ref], command, view)?;
56
        }
57
        Ok(r)
58
    }
59
60
    /// Every policy this repository has applied, ascending by serial, with
61
    /// its signatures. Policies applied before this index existed are
62
    /// missing from it, except the current one.
63
    pub fn policy_history(&self) -> Result<Vec<SignedPolicy>> {
64
        let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new();
65
        if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) {
66
            for e in rd.flatten() {
67
                let Ok(hex) = fs::read_to_string(e.path()) else { continue };
68
                let Some(r) = ObjectRef::from_hex(hex.trim()) else { continue };
69
                let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else { continue };
70
                let policy = Policy::decode(&bytes)?;
71
                out.insert(policy.serial, SignedPolicy { policy, sigs: self.store.signatures(&r)? });
72
            }
73
        }
74
        if let Some((_, sp)) = self.current_policy()? {
75
            out.insert(sp.policy.serial, sp);
76
        }
77
        Ok(out.into_values().collect())
78
    }
79
80
    /// Verify and apply a chain of policies ending in `newest`, each
81
    /// against the one before it (spec §14.5). `chain` holds earlier
82
    /// policies in any order; only those after the current serial are used.
83
    pub(crate) fn adopt_policy_chain(&self, chain: &[SignedPolicy], newest: &SignedPolicy, command: &str) -> std::result::Result<(), String> {
84
        let mut current = self.current_policy().map_err(|e| e.to_string())?.map(|(_, sp)| sp.policy);
85
        let mut steps: Vec<&SignedPolicy> = chain
86
            .iter()
87
            .filter(|sp| current.as_ref().is_none_or(|c| sp.policy.serial > c.serial) && sp.policy.serial < newest.policy.serial)
88
            .collect();
89
        steps.sort_by_key(|sp| sp.policy.serial);
90
        steps.push(newest);
91
        for sp in steps {
92
            self.check_policy(sp, current.as_ref())?;
93
            self.set_policy(sp, &format!("{command} (policy serial {})", sp.policy.serial)).map_err(|e| e.to_string())?;
94
            current = Some(sp.policy.clone());
95
        }
96
        Ok(())
97
    }
98
99
    /// Validate an incoming policy against the one we trust (`None` when
100
    /// bootstrapping: the policy then vouches for itself).
101
    pub fn check_policy(&self, sp: &SignedPolicy, trusted: Option<&Policy>) -> std::result::Result<(), String> {
102
        if sp.policy.repo != self.id().map_err(|e| e.to_string())? {
103
            return Err("policy is for a different repository".into());
104
        }
105
        if let Some(t) = trusted {
106
            if sp.policy.serial <= t.serial {
107
                return Err(format!("policy serial {} is not greater than the current {}", sp.policy.serial, t.serial));
108
            }
109
        }
110
        match trusted {
111
            Some(t) => sp.policy.verify_signatures(&sp.sigs, t)?,
112
            None => vec![sp.policy.verify_bootstrap(&sp.sigs)?],
113
        };
114
        Ok(())
115
    }
116
117
    /// Sign `policy` with the configured key and apply it if the current
118
    /// policy's root threshold is met; otherwise return it as a proposal.
119
    fn sign_policy(&self, policy: Policy, command: &str) -> Result<PolicyUpdate> {
120
        let Some((key, sk)) = self.secret_key()? else {
121
            return fail("no secret key: run `oxy identity create` first");
122
        };
123
        self.sign_policy_with(policy, &[(key, &sk)], command)
124
    }
125
126
    /// Sign with every listed key; the first must be a root of the current
127
    /// policy (the others may be roots of the new policy only, so that a
128
    /// fresh clone can bootstrap from it).
129
    pub(crate) fn sign_policy_with(&self, policy: Policy, signers: &[(KeyId, &SecretKey)], command: &str) -> Result<PolicyUpdate> {
130
        let trusted = self.current_policy()?.map(|(_, sp)| sp.policy).unwrap_or_else(|| policy.clone());
131
        if !signers.first().is_some_and(|(k, _)| trusted.roots.contains(k)) {
132
            return fail("your key is not a root key of the current policy");
133
        }
134
        let target = policy.hash();
135
        let sigs = signers.iter().map(|(k, sk)| sk.sign(Domain::Policy, k, &target)).collect();
136
        self.gather(SignedPolicy { policy, sigs }, &trusted, command)
137
    }
138
139
    /// Apply `sp` when its signatures meet `trusted`'s threshold.
140
    fn gather(&self, sp: SignedPolicy, trusted: &Policy, command: &str) -> Result<PolicyUpdate> {
141
        let applied = sp.policy.verify_signatures(&sp.sigs, trusted).is_ok();
142
        if applied {
143
            self.set_policy(&sp, command)?;
144
        }
145
        Ok(PolicyUpdate { policy: sp.policy, sigs: sp.sigs, applied })
146
    }
147
148
    /// Add your signature to a proposal made by another root holder; the
149
    /// policy is applied once the current root threshold is met.
150
    pub fn policy_sign(&self, proposal: SignedPolicy) -> Result<PolicyUpdate> {
151
        let Some((key, sk)) = self.secret_key()? else {
152
            return fail("no secret key: run `oxy identity create` first");
153
        };
154
        let Some((_, current)) = self.current_policy()? else {
155
            return fail("this repository has no policy; run `oxy policy init` or fetch one");
156
        };
157
        if proposal.policy.repo != self.id()? {
158
            return fail("the proposal is for a different repository");
159
        }
160
        if proposal.policy.serial <= current.policy.serial {
161
            return fail(format!("the proposal's serial {} is not greater than the current {}", proposal.policy.serial, current.policy.serial));
162
        }
163
        if !current.policy.roots.contains(&key) {
164
            return fail("your key is not a root key of the current policy");
165
        }
166
        let mut sigs: Vec<Signature> = proposal.sigs.into_iter().filter(|s| s.key != key).collect();
167
        sigs.push(sk.sign(Domain::Policy, &key, &proposal.policy.hash()));
168
        self.gather(SignedPolicy { policy: proposal.policy, sigs }, &current.policy, "oxy policy sign")
169
    }
170
171
    /// Create the initial policy with the configured identity as sole root.
172
    pub fn policy_init(&self) -> Result<PolicyUpdate> {
173
        if self.current_policy()?.is_some() {
174
            return fail("this repository already has a policy");
175
        }
176
        let Some((key, _)) = self.secret_key()? else {
177
            return fail("no secret key: run `oxy identity create` first");
178
        };
179
        let identity = self.identity(&key)?;
180
        let policy = Policy::single_root(self.id()?, identity, Hlc::now().wall_ms + POLICY_LIFETIME_MS);
181
        self.sign_policy(policy, "oxy policy init")
182
    }
183
184
    /// Make `identity` a root key.
185
    pub fn policy_root_add(&self, identity: Identity) -> Result<PolicyUpdate> {
186
        let mut p = self.next_policy()?;
187
        let key = identity.hash();
188
        if p.roots.contains(&key) {
189
            return fail(format!("{} is already a root", key.short(12)));
190
        }
191
        p.roots.push(key);
192
        p.roots.sort();
193
        p.revoked.retain(|k| k != &key);
194
        if p.identity(&key).is_none() {
195
            p.identities.push(identity);
196
        }
197
        self.sign_policy(p, &format!("oxy policy root add {}", key.short(12)))
198
    }
199
200
    /// Drop a root key; the remaining roots must still meet the threshold.
201
    pub fn policy_root_remove(&self, key: &KeyId) -> Result<PolicyUpdate> {
202
        let mut p = self.next_policy()?;
203
        if !p.roots.contains(key) {
204
            return fail(format!("{} is not a root", key.short(12)));
205
        }
206
        p.roots.retain(|k| k != key);
207
        if (p.roots.len() as u32) < p.root_threshold {
208
            return fail(format!("removing it would leave {} root(s) for a threshold of {}", p.roots.len(), p.root_threshold));
209
        }
210
        self.sign_policy(p, &format!("oxy policy root remove {}", key.short(12)))
211
    }
212
213
    /// Require `n` root signatures on future policy updates.
214
    pub fn policy_set_threshold(&self, n: u32) -> Result<PolicyUpdate> {
215
        let mut p = self.next_policy()?;
216
        if n == 0 || n as usize > p.roots.len() {
217
            return fail(format!("the threshold must be between 1 and the number of roots ({})", p.roots.len()));
218
        }
219
        p.root_threshold = n;
220
        self.sign_policy(p, &format!("oxy policy root threshold {n}"))
221
    }
222
223
    pub(crate) fn next_policy(&self) -> Result<Policy> {
224
        let Some((_, sp)) = self.current_policy()? else {
225
            return fail("this repository has no policy; run `oxy policy init`");
226
        };
227
        let mut p = sp.policy;
228
        p.serial += 1;
229
        p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS;
230
        Ok(p)
231
    }
232
233
    /// Let `identity` update references matching `pattern` (default every reference).
234
    pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result<PolicyUpdate> {
235
        let mut p = self.next_policy()?;
236
        let key = identity.hash();
237
        let pattern = pattern.unwrap_or("*");
238
        p.revoked.retain(|k| k != &key);
239
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
240
            Some(rule) => {
241
                if !rule.keys.contains(&key) {
242
                    rule.keys.push(key);
243
                    rule.keys.sort();
244
                }
245
            }
246
            None => p.rules.push(Rule { pattern: pattern.into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }),
247
        }
248
        if p.identity(&key).is_none() {
249
            p.identities.push(identity);
250
        }
251
        self.sign_policy(p, &format!("oxy policy grant {}", key.short(12)))
252
    }
253
254
    /// Revoke `key`: it may no longer update anything, and existing rules
255
    /// drop it. A root key can be revoked by another root as long as the
256
    /// remaining roots still meet the threshold.
257
    pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> {
258
        let mut p = self.next_policy()?;
259
        if p.roots.contains(key) {
260
            let own = self.secret_key()?.map(|(k, _)| k);
261
            if own == Some(*key) {
262
                return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first");
263
            }
264
            p.roots.retain(|k| k != key);
265
            if (p.roots.len() as u32) < p.root_threshold {
266
                return fail(format!("revoking it would leave {} root(s) for a threshold of {}", p.roots.len(), p.root_threshold));
267
            }
268
        }
269
        for r in &mut p.rules {
270
            r.keys.retain(|k| k != key);
271
        }
272
        p.rules.retain(|r| !r.keys.is_empty());
273
        if !p.revoked.contains(key) {
274
            p.revoked.push(*key);
275
            p.revoked.sort();
276
        }
277
        self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12)))
278
    }
279
280
    /// Whether `change` may be part of `reference` under `policy`: it must be
281
    /// authenticated (by its author or a delegate) with a key the matching
282
    /// rule lists, using the authenticator kind the rule requires. A rule key
283
    /// covers its rotation predecessors, and a delegate's signature counts
284
    /// for the author it acts for.
285
    pub fn change_allowed(&self, hash: &ChangeHash, policy: &Policy, reference: &str) -> std::result::Result<(), String> {
286
        let rule = policy.rule_for(reference).ok_or_else(|| format!("no policy rule matches {reference:?}"))?;
287
        match self.authenticate_change(hash).map_err(|e| e.to_string())? {
288
            SignatureStatus::Valid { key, sealed, .. } => {
289
                if policy.is_revoked(&key) {
290
                    return Err(format!("change {} is signed by revoked key {}", hash.short(12), key.short(12)));
291
                }
292
                let author = self.store.get_change(hash).map(|c| c.author).map_err(|e| e.to_string())?;
293
                let principal = if author.is_zero() { key } else { author };
294
                let covered = rule.keys.iter().any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal));
295
                if !covered {
296
                    return Err(format!("change {} is signed by {}, which may not update {reference:?}", hash.short(12), key.short(12)));
297
                }
298
                match (rule.require, sealed) {
299
                    (Require::Signed, true) => Err(format!("{reference:?} requires individually signed changes; {} is only sealed", hash.short(12))),
300
                    (Require::Sealed, false) => Err(format!("{reference:?} requires sealed changes; {} is individually signed", hash.short(12))),
301
                    _ => Ok(()),
302
                }
303
            }
304
            SignatureStatus::Unsigned => Err(format!("change {} is not signed or sealed", hash.short(12))),
305
            SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))),
306
        }
307
    }
308
309
    /// Check every change that `new` adds over `old` for `reference`.
310
    pub(crate) fn state_allowed(&self, old_heads: &[ChangeHash], new_heads: &[ChangeHash], policy: &Policy, reference: &str) -> std::result::Result<(), String> {
311
        let added = self.negotiate(old_heads, new_heads).map_err(|e| e.to_string())?;
312
        for h in &added {
313
            self.change_allowed(h, policy, reference)?;
314
        }
315
        if let Some(rule) = policy.rule_for(reference) {
316
            if !rule.allow_conflicts && !added.is_empty() {
317
                let st = self.load_state(new_heads).map_err(|e| e.to_string())?;
318
                if st.materialize().is_conflicted() {
319
                    return Err(format!("{reference:?} does not allow conflicted states"));
320
                }
321
            }
322
        }
323
        Ok(())
324
    }
325
}