OXYGENHUB
# 99cdbaeea274 oxygen / crates / oxy-core / src / repo / policy.rs
7.9 KiB · Rust · last changed Trust policy: policy objects, init/grant/revoke, enforcement on push and fetch, propagation with downgrade protection (spec 0.6) 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::policy::{Policy, Require, Rule};
10
use crate::protocol::SignedPolicy;
11
12
/// One year, the default policy lifetime.
13
const POLICY_LIFETIME_MS: u64 = 365 * 24 * 60 * 60 * 1000;
14
15
impl Repo {
16
    /// The current policy and its signatures, if the repository has one.
17
    pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> {
18
        let Some(r) = self.view()?.policy else { return Ok(None) };
19
        let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?;
20
        Ok(Some((r, SignedPolicy { policy, sigs: self.store.signatures(&r)? })))
21
    }
22
23
    /// Store a policy with its signatures and point the `policy` reference at it.
24
    pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> {
25
        let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?;
26
        for sig in &sp.sigs {
27
            self.store.put_signature(&r, sig)?;
28
        }
29
        for id in &sp.policy.identities {
30
            self.store.put_identity(id)?;
31
        }
32
        let (op_ref, op) = self.head_op()?;
33
        if op.view.policy != Some(r) {
34
            let mut view = op.view.clone();
35
            view.policy = Some(r);
36
            self.record_op(&[op_ref], command, view)?;
37
        }
38
        Ok(r)
39
    }
40
41
    /// Validate an incoming policy against the one we trust (`None` when
42
    /// bootstrapping: the policy then vouches for itself).
43
    pub fn check_policy(&self, sp: &SignedPolicy, trusted: Option<&Policy>) -> std::result::Result<(), String> {
44
        if sp.policy.repo != self.id().map_err(|e| e.to_string())? {
45
            return Err("policy is for a different repository".into());
46
        }
47
        if let Some(t) = trusted {
48
            if sp.policy.serial <= t.serial {
49
                return Err(format!("policy serial {} is not greater than the current {}", sp.policy.serial, t.serial));
50
            }
51
        }
52
        sp.policy.verify_signatures(&sp.sigs, trusted.unwrap_or(&sp.policy))?;
53
        Ok(())
54
    }
55
56
    fn sign_policy(&self, policy: Policy) -> Result<SignedPolicy> {
57
        let Some((key, sk)) = self.secret_key()? else {
58
            return fail("no secret key: run `oxy identity create` first");
59
        };
60
        if !policy.roots.contains(&key) {
61
            return fail("your key is not a root key of this policy");
62
        }
63
        if policy.root_threshold > 1 {
64
            return fail(format!("this policy needs {} root signatures; multi-signer updates are not implemented yet", policy.root_threshold));
65
        }
66
        let sig = sk.sign(Domain::Policy, &key, &policy.hash());
67
        Ok(SignedPolicy { policy, sigs: vec![sig] })
68
    }
69
70
    /// Create the initial policy with the configured identity as sole root.
71
    pub fn policy_init(&self) -> Result<SignedPolicy> {
72
        if self.current_policy()?.is_some() {
73
            return fail("this repository already has a policy");
74
        }
75
        let Some((key, _)) = self.secret_key()? else {
76
            return fail("no secret key: run `oxy identity create` first");
77
        };
78
        let identity = self.identity(&key)?;
79
        let policy = Policy::single_root(self.id()?, identity, Hlc::now().wall_ms + POLICY_LIFETIME_MS);
80
        let sp = self.sign_policy(policy)?;
81
        self.set_policy(&sp, "oxy policy init")?;
82
        Ok(sp)
83
    }
84
85
    fn next_policy(&self) -> Result<Policy> {
86
        let Some((_, sp)) = self.current_policy()? else {
87
            return fail("this repository has no policy; run `oxy policy init`");
88
        };
89
        let mut p = sp.policy;
90
        p.serial += 1;
91
        p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS;
92
        Ok(p)
93
    }
94
95
    /// Let `identity` update references matching `pattern` (default every reference).
96
    pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result<SignedPolicy> {
97
        let mut p = self.next_policy()?;
98
        let key = identity.hash();
99
        let pattern = pattern.unwrap_or("*");
100
        p.revoked.retain(|k| k != &key);
101
        match p.rules.iter_mut().find(|r| r.pattern == pattern) {
102
            Some(rule) => {
103
                if !rule.keys.contains(&key) {
104
                    rule.keys.push(key);
105
                    rule.keys.sort();
106
                }
107
            }
108
            None => p.rules.push(Rule { pattern: pattern.into(), keys: vec![key], threshold: 1, require: Require::Any, allow_conflicts: true }),
109
        }
110
        if p.identity(&key).is_none() {
111
            p.identities.push(identity);
112
        }
113
        let sp = self.sign_policy(p)?;
114
        self.set_policy(&sp, &format!("oxy policy grant {}", key.short(12)))?;
115
        Ok(sp)
116
    }
117
118
    /// Revoke `key`: it may no longer update anything, and existing rules drop it.
119
    pub fn policy_revoke(&self, key: &KeyId) -> Result<SignedPolicy> {
120
        let mut p = self.next_policy()?;
121
        if p.roots.contains(key) {
122
            return fail("cannot revoke a root key");
123
        }
124
        for r in &mut p.rules {
125
            r.keys.retain(|k| k != key);
126
        }
127
        p.rules.retain(|r| !r.keys.is_empty());
128
        if !p.revoked.contains(key) {
129
            p.revoked.push(*key);
130
            p.revoked.sort();
131
        }
132
        let sp = self.sign_policy(p)?;
133
        self.set_policy(&sp, &format!("oxy policy revoke {}", key.short(12)))?;
134
        Ok(sp)
135
    }
136
137
    /// Whether `change` may be part of `reference` under `policy`: it must be
138
    /// authenticated (by its author or a delegate) with a key the matching
139
    /// rule lists, using the authenticator kind the rule requires.
140
    pub fn change_allowed(&self, hash: &ChangeHash, policy: &Policy, reference: &str) -> std::result::Result<(), String> {
141
        let rule = policy.rule_for(reference).ok_or_else(|| format!("no policy rule matches {reference:?}"))?;
142
        match self.authenticate_change(hash).map_err(|e| e.to_string())? {
143
            SignatureStatus::Valid { key, sealed, .. } => {
144
                if policy.is_revoked(&key) {
145
                    return Err(format!("change {} is signed by revoked key {}", hash.short(12), key.short(12)));
146
                }
147
                if !rule.keys.contains(&key) {
148
                    return Err(format!("change {} is signed by {}, which may not update {reference:?}", hash.short(12), key.short(12)));
149
                }
150
                match (rule.require, sealed) {
151
                    (Require::Signed, true) => Err(format!("{reference:?} requires individually signed changes; {} is only sealed", hash.short(12))),
152
                    (Require::Sealed, false) => Err(format!("{reference:?} requires sealed changes; {} is individually signed", hash.short(12))),
153
                    _ => Ok(()),
154
                }
155
            }
156
            SignatureStatus::Unsigned => Err(format!("change {} is not signed or sealed", hash.short(12))),
157
            SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))),
158
        }
159
    }
160
161
    /// Check every change that `new` adds over `old` for `reference`.
162
    pub(crate) fn state_allowed(&self, old_heads: &[ChangeHash], new_heads: &[ChangeHash], policy: &Policy, reference: &str) -> std::result::Result<(), String> {
163
        let added = self.negotiate(old_heads, new_heads).map_err(|e| e.to_string())?;
164
        for h in &added {
165
            self.change_allowed(h, policy, reference)?;
166
        }
167
        if let Some(rule) = policy.rule_for(reference) {
168
            if !rule.allow_conflicts && !added.is_empty() {
169
                let st = self.load_state(new_heads).map_err(|e| e.to_string())?;
170
                if st.materialize().is_conflicted() {
171
                    return Err(format!("{reference:?} does not allow conflicted states"));
172
                }
173
            }
174
        }
175
        Ok(())
176
    }
177
}