OXYGENHUB

Coverage / crates/oxy-core/src/repo/policy.rs 79.7%

322 of 404 lines run, 49 of 88 functions · run #342 on proposal/125 · run missed

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 {
263×
    pub fn signed(&self) -> SignedPolicy {
273×
        SignedPolicy {
283×
            policy: self.policy.clone(),
293×
            sigs: self.sigs.clone(),
303×
        }
313×
    }
32
}
33
34
impl Repo {
35
    /// The current policy and its signatures, if the repository has one.
363684×
    pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> {
373684×
        let Some(r) = self.view()?.policy else {
383273×
            return Ok(None);
39
        };
40411×
        let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?;
41
        Ok(Some((
42411×
            r,
43
            SignedPolicy {
44411×
                policy,
45411×
                sigs: self.store.signatures(&r)?,
46
            },
47
        )))
483684×
    }
49
50
    /// Store a policy with its signatures and point the `policy` reference at it.
5182×
    pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> {
5282×
        let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?;
53106×
        for sig in &sp.sigs {
54106×
            self.store.put_signature(&r, sig)?;
55
        }
56175×
        for id in &sp.policy.identities {
57175×
            self.store.put_identity(id)?;
58
        }
5982×
        let dir = self.oxy.join("policies");
6082×
        fs::create_dir_all(&dir)?;
6182×
        write_atomic(
6282×
            &dir.join(format!("{:020}", sp.policy.serial)),
6382×
            r.to_hex().as_bytes(),
640×
        )?;
6582×
        let (op_ref, op) = self.head_op()?;
6682×
        if op.view.policy != Some(r) {
6782×
            let mut view = op.view.clone();
6882×
            view.policy = Some(r);
6982×
            self.record_op(&[op_ref], command, view)?;
700×
        }
7182×
        Ok(r)
7282×
    }
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.
771048×
    pub fn policy_history(&self) -> Result<Vec<SignedPolicy>> {
781048×
        let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new();
791048×
        if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) {
80176×
            for e in rd.flatten() {
81176×
                let Ok(hex) = fs::read_to_string(e.path()) else {
820×
                    continue;
83
                };
84176×
                let Some(r) = ObjectRef::from_hex(hex.trim()) else {
850×
                    continue;
86
                };
87176×
                let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else {
880×
                    continue;
89
                };
90176×
                let policy = Policy::decode(&bytes)?;
91176×
                out.insert(
92176×
                    policy.serial,
93
                    SignedPolicy {
94176×
                        policy,
95176×
                        sigs: self.store.signatures(&r)?,
96
                    },
97
                );
98
            }
99966×
        }
1001048×
        if let Some((_, sp)) = self.current_policy()? {
10182×
            out.insert(sp.policy.serial, sp);
102966×
        }
1031048×
        Ok(out.into_values().collect())
1041048×
    }
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.
10940×
    pub(crate) fn adopt_policy_chain(
11040×
        &self,
11140×
        chain: &[SignedPolicy],
11240×
        newest: &SignedPolicy,
11340×
        command: &str,
11440×
    ) -> std::result::Result<(), String> {
11540×
        let mut current = self
11640×
            .current_policy()
11740×
            .map_err(|e| e.to_string())?
11840×
            .map(|(_, sp)| sp.policy);
11940×
        let mut steps: Vec<&SignedPolicy> = chain
12040×
            .iter()
12140×
            .filter(|sp| {
12227×
                current.as_ref().is_none_or(|c| sp.policy.serial > c.serial)
12316×
                    && sp.policy.serial < newest.policy.serial
12427×
            })
12540×
            .collect();
12640×
        steps.sort_by_key(|sp| sp.policy.serial);
12740×
        steps.push(newest);
12856×
        for sp in steps {
12956×
            self.check_policy(sp, current.as_ref())?;
13054×
            self.set_policy(
13154×
                sp,
13254×
                &format!("{command} (policy serial {})", sp.policy.serial),
133
            )
13454×
            .map_err(|e| e.to_string())?;
13554×
            current = Some(sp.policy.clone());
136
        }
13738×
        Ok(())
13840×
    }
139
140
    /// Validate an incoming policy against the one we trust (`None` when
141
    /// bootstrapping: the policy then vouches for itself).
14256×
    pub fn check_policy(
14356×
        &self,
14456×
        sp: &SignedPolicy,
14556×
        trusted: Option<&Policy>,
14656×
    ) -> std::result::Result<(), String> {
14756×
        if sp.policy.repo != self.id().map_err(|e| e.to_string())? {
1480×
            return Err("policy is for a different repository".into());
14956×
        }
15056×
        if let Some(t) = trusted {
15135×
            if sp.policy.serial <= t.serial {
1521×
                return Err(format!(
1531×
                    "policy serial {} is not greater than the current {}",
1541×
                    sp.policy.serial, t.serial
1551×
                ));
15634×
            }
15721×
        }
15855×
        match trusted {
15934×
            Some(t) => sp.policy.verify_signatures(&sp.sigs, t)?,
16021×
            None => vec![sp.policy.verify_bootstrap(&sp.sigs)?],
161
        };
16254×
        Ok(())
16356×
    }
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.
16720×
    fn sign_policy(&self, policy: Policy, command: &str) -> Result<PolicyUpdate> {
16820×
        let Some((key, sk)) = self.secret_key()? else {
1690×
            return fail("no secret key: run `oxy identity create` first");
170
        };
17120×
        self.sign_policy_with(policy, &[(key, &sk)], command)
17220×
    }
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).
17728×
    pub(crate) fn sign_policy_with(
17828×
        &self,
17928×
        policy: Policy,
18028×
        signers: &[(KeyId, &SecretKey)],
18128×
        command: &str,
18228×
    ) -> Result<PolicyUpdate> {
18328×
        let trusted = self
18428×
            .current_policy()?
18528×
            .map(|(_, sp)| sp.policy)
18628×
            .unwrap_or_else(|| policy.clone());
18728×
        if !signers
18828×
            .first()
18928×
            .is_some_and(|(k, _)| trusted.roots.contains(k))
190
        {
1910×
            return fail("your key is not a root key of the current policy");
19228×
        }
19328×
        let target = policy.hash();
19428×
        let sigs = signers
19528×
            .iter()
19636×
            .map(|(k, sk)| sk.sign(Domain::Policy, k, &target))
19728×
            .collect();
19828×
        self.gather(SignedPolicy { policy, sigs }, &trusted, command)
19928×
    }
200
201
    /// Apply `sp` when its signatures meet `trusted`'s threshold.
20230×
    fn gather(&self, sp: SignedPolicy, trusted: &Policy, command: &str) -> Result<PolicyUpdate> {
20330×
        let applied = sp.policy.verify_signatures(&sp.sigs, trusted).is_ok();
20430×
        if applied {
20528×
            self.set_policy(&sp, command)?;
2062×
        }
20730×
        Ok(PolicyUpdate {
20830×
            policy: sp.policy,
20930×
            sigs: sp.sigs,
21030×
            applied,
21130×
        })
21230×
    }
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.
2163×
    pub fn policy_sign(&self, proposal: SignedPolicy) -> Result<PolicyUpdate> {
2173×
        let Some((key, sk)) = self.secret_key()? else {
2180×
            return fail("no secret key: run `oxy identity create` first");
219
        };
2203×
        let Some((_, current)) = self.current_policy()? else {
2210×
            return fail("this repository has no policy; run `oxy policy init` or fetch one");
222
        };
2233×
        if proposal.policy.repo != self.id()? {
2240×
            return fail("the proposal is for a different repository");
2253×
        }
2263×
        if proposal.policy.serial <= current.policy.serial {
2271×
            return fail(format!(
228
                "the proposal's serial {} is not greater than the current {}",
229
                proposal.policy.serial, current.policy.serial
230
            ));
2312×
        }
2322×
        if !current.policy.roots.contains(&key) {
2330×
            return fail("your key is not a root key of the current policy");
2342×
        }
2352×
        let mut sigs: Vec<Signature> = proposal.sigs.into_iter().filter(|s| s.key != key).collect();
2362×
        sigs.push(sk.sign(Domain::Policy, &key, &proposal.policy.hash()));
2372×
        self.gather(
2382×
            SignedPolicy {
2392×
                policy: proposal.policy,
2402×
                sigs,
2412×
            },
2422×
            &current.policy,
2432×
            "oxy policy sign",
244
        )
2453×
    }
246
247
    /// Create the initial policy with the configured identity as sole root.
24811×
    pub fn policy_init(&self) -> Result<PolicyUpdate> {
24911×
        if self.current_policy()?.is_some() {
2500×
            return fail("this repository already has a policy");
25111×
        }
25211×
        let Some((key, _)) = self.secret_key()? else {
2530×
            return fail("no secret key: run `oxy identity create` first");
254
        };
25511×
        let identity = self.identity(&key)?;
25611×
        let policy = Policy::single_root(
25711×
            self.id()?,
25811×
            identity,
25911×
            Hlc::now().wall_ms + POLICY_LIFETIME_MS,
260
        );
26111×
        self.sign_policy(policy, "oxy policy init")
26211×
    }
263
264
    /// Make `identity` a root key.
2652×
    pub fn policy_root_add(&self, identity: Identity) -> Result<PolicyUpdate> {
2662×
        let mut p = self.next_policy()?;
2672×
        let key = identity.hash();
2682×
        if p.roots.contains(&key) {
2691×
            return fail(format!("{} is already a root", key.short(12)));
2701×
        }
2711×
        p.roots.push(key);
2721×
        p.roots.sort();
2731×
        p.revoked.retain(|k| k != &key);
2741×
        if p.identity(&key).is_none() {
2751×
            p.identities.push(identity);
2761×
        }
2771×
        self.sign_policy(p, &format!("oxy policy root add {}", key.short(12)))
2782×
    }
279
280
    /// Drop a root key; the remaining roots must still meet the threshold.
2810×
    pub fn policy_root_remove(&self, key: &KeyId) -> Result<PolicyUpdate> {
2820×
        let mut p = self.next_policy()?;
2830×
        if !p.roots.contains(key) {
2840×
            return fail(format!("{} is not a root", key.short(12)));
2850×
        }
2860×
        p.roots.retain(|k| k != key);
2870×
        if (p.roots.len() as u32) < p.root_threshold {
2880×
            return fail(format!(
289
                "removing it would leave {} root(s) for a threshold of {}",
2900×
                p.roots.len(),
291
                p.root_threshold
292
            ));
2930×
        }
2940×
        self.sign_policy(p, &format!("oxy policy root remove {}", key.short(12)))
2950×
    }
296
297
    /// Require `n` root signatures on future policy updates.
2982×
    pub fn policy_set_threshold(&self, n: u32) -> Result<PolicyUpdate> {
2992×
        let mut p = self.next_policy()?;
3002×
        if n == 0 || n as usize > p.roots.len() {
3011×
            return fail(format!(
302
                "the threshold must be between 1 and the number of roots ({})",
3031×
                p.roots.len()
304
            ));
3051×
        }
3061×
        p.root_threshold = n;
3071×
        self.sign_policy(p, &format!("oxy policy root threshold {n}"))
3082×
    }
309
31021×
    pub(crate) fn next_policy(&self) -> Result<Policy> {
31121×
        let Some((_, sp)) = self.current_policy()? else {
3120×
            return fail("this repository has no policy; run `oxy policy init`");
313
        };
31421×
        let mut p = sp.policy;
31521×
        p.serial += 1;
31621×
        p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS;
31721×
        Ok(p)
31821×
    }
319
320
    /// Let `identity` update references matching `pattern` (default every reference).
3212×
    pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result<PolicyUpdate> {
3222×
        let mut p = self.next_policy()?;
3232×
        let key = identity.hash();
3242×
        let pattern = pattern.unwrap_or("*");
3252×
        p.revoked.retain(|k| k != &key);
3262×
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
3272×
            Some(rule) => {
3282×
                if !rule.keys.contains(&key) {
3292×
                    rule.keys.push(key);
3302×
                    rule.keys.sort();
3312×
                }
332
            }
3330×
            None => p.rules.push(Rule {
3340×
                pattern: pattern.into(),
3350×
                keys: vec![key],
3360×
                threshold: 1,
3370×
                require: Require::Any,
3380×
                allow_conflicts: true,
3390×
                subjects: vec![],
3400×
            }),
341
        }
3422×
        if p.identity(&key).is_none() {
3432×
            p.identities.push(identity);
3442×
        }
3452×
        self.sign_policy(p, &format!("oxy policy grant {}", key.short(12)))
3462×
    }
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.
3523×
    pub fn policy_grant_subject(
3533×
        &self,
3543×
        subject: &str,
3553×
        issuer: Identity,
3563×
        scope: crate::identity::Scope,
3573×
        pattern: Option<&str>,
3583×
    ) -> Result<PolicyUpdate> {
3593×
        if subject.trim().is_empty() {
3600×
            return fail("a subject pattern is needed, such as repo:OWNER/NAME:bookmark:main");
3613×
        }
3623×
        let mut p = self.next_policy()?;
3633×
        let key = issuer.hash();
3643×
        let pattern = pattern.unwrap_or("*");
3653×
        let grant = crate::policy::SubjectGrant {
3663×
            pattern: subject.into(),
3673×
            issuer: key,
3683×
            scope,
3693×
        };
3703×
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
3713×
            Some(rule) => {
3723×
                rule.subjects
3733×
                    .retain(|g| !(g.pattern == grant.pattern && g.issuer == grant.issuer));
3743×
                rule.subjects.push(grant);
375
            }
3760×
            None => p.rules.push(Rule {
3770×
                pattern: pattern.into(),
3780×
                keys: vec![],
3790×
                threshold: 1,
3800×
                require: Require::Any,
3810×
                allow_conflicts: true,
3820×
                subjects: vec![grant],
3830×
            }),
384
        }
3853×
        if p.identity(&key).is_none() {
3863×
            p.identities.push(issuer);
3873×
        }
3883×
        self.sign_policy(p, &format!("oxy policy grant-subject {subject}"))
3893×
    }
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.
3944×
    pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> {
3954×
        let mut p = self.next_policy()?;
3964×
        if p.roots.contains(key) {
3972×
            let own = self.secret_key()?.map(|(k, _)| k);
3982×
            if own == Some(*key) {
3991×
                return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first");
4001×
            }
4012×
            p.roots.retain(|k| k != key);
4021×
            if (p.roots.len() as u32) < p.root_threshold {
4031×
                return fail(format!(
404
                    "revoking it would leave {} root(s) for a threshold of {}",
4051×
                    p.roots.len(),
406
                    p.root_threshold
407
                ));
4080×
            }
4092×
        }
4102×
        for r in &mut p.rules {
4114×
            r.keys.retain(|k| k != key);
412
        }
413
        // A rule of certified subjects alone stays.
4142×
        p.rules
4152×
            .retain(|r| !r.keys.is_empty() || !r.subjects.is_empty());
4162×
        if !p.revoked.contains(key) {
4172×
            p.revoked.push(*key);
4182×
            p.revoked.sort();
4192×
        }
4202×
        self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12)))
4214×
    }
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.
42821×
    pub fn change_allowed(
42921×
        &self,
43021×
        hash: &ChangeHash,
43121×
        policy: &Policy,
43221×
        reference: &str,
43321×
    ) -> std::result::Result<(), String> {
43421×
        let rule = policy
43521×
            .rule_for(reference)
43621×
            .ok_or_else(|| format!("no policy rule matches {reference:?}"))?;
43721×
        match self.authenticate_change(hash).map_err(|e| e.to_string())? {
43821×
            SignatureStatus::Valid { key, sealed, .. } => {
43921×
                if policy.is_revoked(&key) {
4400×
                    return Err(format!(
4410×
                        "change {} is signed by revoked key {}",
4420×
                        hash.short(12),
4430×
                        key.short(12)
4440×
                    ));
44521×
                }
44621×
                let author = self
44721×
                    .store
44821×
                    .get_change(hash)
44921×
                    .map(|c| c.author)
45021×
                    .map_err(|e| e.to_string())?;
45121×
                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.
45421×
                let covered = rule
45521×
                    .keys
45621×
                    .iter()
45721×
                    .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal))
4581×
                    || self.certified_authorized(
4591×
                        policy,
4601×
                        &key,
4611×
                        reference,
462
                        crate::identity::Scope::CHANGES,
4631×
                        crate::objects::Hlc::now().wall_ms,
464
                    );
46521×
                if !covered {
4660×
                    return Err(format!(
4670×
                        "change {} is signed by {}, which may not update {reference:?}",
4680×
                        hash.short(12),
4690×
                        key.short(12)
4700×
                    ));
47121×
                }
47221×
                match (rule.require, sealed) {
4730×
                    (Require::Signed, true) => Err(format!(
4740×
                        "{reference:?} requires individually signed changes; {} is only sealed",
4750×
                        hash.short(12)
4760×
                    )),
4770×
                    (Require::Sealed, false) => Err(format!(
4780×
                        "{reference:?} requires sealed changes; {} is individually signed",
4790×
                        hash.short(12)
4800×
                    )),
48121×
                    _ => Ok(()),
482
                }
483
            }
484
            SignatureStatus::Unsigned => {
4850×
                Err(format!("change {} is not signed or sealed", hash.short(12)))
486
            }
4870×
            SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))),
488
        }
48921×
    }
490
491
    /// Check every change that `new` adds over `old` for `reference`.
49230×
    pub(crate) fn state_allowed(
49330×
        &self,
49430×
        old_heads: &[ChangeHash],
49530×
        new_heads: &[ChangeHash],
49630×
        policy: &Policy,
49730×
        reference: &str,
49830×
    ) -> std::result::Result<(), String> {
49930×
        let added = self
50030×
            .negotiate(old_heads, new_heads)
50130×
            .map_err(|e| e.to_string())?;
50230×
        for h in &added {
50320×
            self.change_allowed(h, policy, reference)?;
504
        }
50530×
        if let Some(rule) = policy.rule_for(reference) {
50630×
            if !rule.allow_conflicts && !added.is_empty() {
5070×
                let st = self.load_state(new_heads).map_err(|e| e.to_string())?;
5080×
                if st.materialize().is_conflicted() {
5090×
                    return Err(format!("{reference:?} does not allow conflicted states"));
5100×
                }
51130×
            }
5120×
        }
51330×
        Ok(())
51430×
    }
515
}