OXYGENHUB
# b4fa2bc1ca6a oxygen / crates / oxy-core / src / repo / identity.rs
14.5 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
//! Identities, signing, and signature verification (spec §14).
2
3
use super::*;
4
5
/// Outcome of verifying the signatures on a change (spec §14.3, §14.4).
6
#[derive(Clone, Debug, PartialEq, Eq)]
7
pub enum SignatureStatus {
8
    Unsigned,
9
    /// `revoked` marks a signature that verifies but whose key has since
10
    /// been revoked by policy or by a revocation this repository recorded.
11
    Valid { key: KeyId, name: String, email: String, sealed: bool, revoked: bool },
12
    Invalid(String),
13
}
14
15
/// The outcome of replacing the configured identity (spec §14.2).
16
#[derive(Clone, Debug)]
17
pub struct Rotation {
18
    pub old: KeyId,
19
    pub new: KeyId,
20
    pub identity: Identity,
21
    /// The policy update that moved the old key's grants to the new key,
22
    /// when the repository has a policy naming the old key.
23
    pub policy: Option<PolicyUpdate>,
24
}
25
26
impl Repo {
27
    // ------------------------------------------------------------ identity
28
29
    /// Generate a composite key pair, store its identity object, keep the
30
    /// secret in the key directory, and record the author at `scope`.
31
    pub fn create_identity(&mut self, name: &str, email: &str, scope: IdentityScope) -> Result<(KeyId, Identity)> {
32
        self.create_identity_with(name, email, scope, None)
33
    }
34
35
    /// `create_identity` with an expiry (milliseconds since the epoch).
36
    pub fn create_identity_with(&mut self, name: &str, email: &str, scope: IdentityScope, expires: Option<u64>) -> Result<(KeyId, Identity)> {
37
        let sk = SecretKey::generate();
38
        let mut id = sk.identity(name, email, Hlc::now().wall_ms);
39
        id.expires = expires;
40
        let key = self.install_key(&id, &sk)?;
41
        self.set_author(&id, &key, scope)?;
42
        Ok((key, id))
43
    }
44
45
    /// Store an identity and keep its secret (and a copy of the identity)
46
    /// in the key directory.
47
    fn install_key(&self, id: &Identity, sk: &SecretKey) -> Result<KeyId> {
48
        let key = self.store.put_identity(id)?;
49
        fs::create_dir_all(&self.keys_dir)?;
50
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
51
        write_atomic(&path, &sk.encode(&key))?;
52
        write_atomic(&self.keys_dir.join(format!("{}.identity", key.to_hex())), &id.encode())?;
53
        #[cfg(unix)]
54
        {
55
            use std::os::unix::fs::PermissionsExt;
56
            fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
57
        }
58
        Ok(key)
59
    }
60
61
    fn set_author(&mut self, id: &Identity, key: &KeyId, scope: IdentityScope) -> Result<()> {
62
        match scope {
63
            IdentityScope::Repo => {
64
                self.config.set("author.name", &id.name)?;
65
                self.config.set("author.email", &id.email)?;
66
                self.config.set("author.key", &key.to_hex())?;
67
                self.save_config()?;
68
            }
69
            IdentityScope::User => {
70
                Config::set_user(&self.user_config, "author.name", &id.name)?;
71
                Config::set_user(&self.user_config, "author.email", &id.email)?;
72
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
73
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
74
            }
75
        }
76
        Ok(())
77
    }
78
79
    /// The scope that holds the configured author key.
80
    fn author_scope(&self) -> IdentityScope {
81
        if self.config.repo_layer().contains_key("author.key") { IdentityScope::Repo } else { IdentityScope::User }
82
    }
83
84
    /// Replace the configured identity by `new` signed with `sk`: store it,
85
    /// record a `rotate` attestation signed by the old key, move the old
86
    /// key's policy grants to the new key, and switch the author config.
87
    fn replace_identity(&mut self, new: Identity, sk: SecretKey, command: &str) -> Result<Rotation> {
88
        let Some((old, old_sk)) = self.secret_key()? else {
89
            return fail("no secret key: run `oxy identity create` first");
90
        };
91
        if new.previous != Some(old) {
92
            return fail("the successor identity must name the current key as previous");
93
        }
94
        let new_key = self.install_key(&new, &sk)?;
95
        let attest = Attest { repo: self.id()?, hlc: Hlc::now(), body: AttestBody::Rotate { new: new_key } };
96
        let r = self.store.put_attest(&attest)?;
97
        self.store.put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?;
98
        let policy = match self.current_policy()? {
99
            Some((_, sp)) if sp.policy.mentions(&old) => {
100
                let mut p = self.next_policy()?;
101
                p.substitute_key(&old, new.clone());
102
                Some(self.sign_policy_with(p, &[(old, &old_sk), (new_key, &sk)], command)?)
103
            }
104
            _ => None,
105
        };
106
        self.set_author(&new, &new_key, self.author_scope())?;
107
        Ok(Rotation { old, new: new_key, identity: new, policy })
108
    }
109
110
    /// Rotate to a fresh key pair; the new identity names the old one as
111
    /// `previous` and keeps its name, email, delegates, and expiry.
112
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
113
        let Some((old, _)) = self.secret_key()? else {
114
            return fail("no secret key: run `oxy identity create` first");
115
        };
116
        let cur = self.identity(&old)?;
117
        let sk = SecretKey::generate();
118
        let mut id = sk.identity(&cur.name, &cur.email, Hlc::now().wall_ms);
119
        id.previous = Some(old);
120
        id.delegates = cur.delegates.clone();
121
        id.expires = cur.expires;
122
        self.replace_identity(id, sk, "oxy identity rotate")
123
    }
124
125
    /// Re-issue the identity with the same keys and a new expiry.
126
    pub fn renew_identity(&mut self, expires: Option<u64>) -> Result<Rotation> {
127
        let Some((old, sk)) = self.secret_key()? else {
128
            return fail("no secret key: run `oxy identity create` first");
129
        };
130
        let cur = self.identity(&old)?;
131
        let id = cur.successor(cur.delegates.clone(), expires, Hlc::now().wall_ms);
132
        self.replace_identity(id, sk, "oxy identity renew")
133
    }
134
135
    /// Let `delegate` sign or seal on this identity's behalf. Delegates are
136
    /// part of the identity object, so this re-issues it with the same keys.
137
    pub fn delegate_add(&mut self, delegate: Identity) -> Result<Rotation> {
138
        let Some((old, sk)) = self.secret_key()? else {
139
            return fail("no secret key: run `oxy identity create` first");
140
        };
141
        let dk = self.store.put_identity(&delegate)?;
142
        if dk == old {
143
            return fail("an identity cannot delegate to itself");
144
        }
145
        let cur = self.identity(&old)?;
146
        if cur.delegates.contains(&dk) {
147
            return fail(format!("{} is already a delegate", dk.short(12)));
148
        }
149
        let mut delegates = cur.delegates.clone();
150
        delegates.push(dk);
151
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
152
        self.replace_identity(id, sk, &format!("oxy identity delegate add {}", dk.short(12)))
153
    }
154
155
    /// Withdraw a delegation; re-issues the identity with the same keys.
156
    pub fn delegate_remove(&mut self, delegate: &KeyId) -> Result<Rotation> {
157
        let Some((old, sk)) = self.secret_key()? else {
158
            return fail("no secret key: run `oxy identity create` first");
159
        };
160
        let cur = self.identity(&old)?;
161
        if !cur.delegates.contains(delegate) {
162
            return fail(format!("{} is not a delegate", delegate.short(12)));
163
        }
164
        let delegates = cur.delegates.iter().copied().filter(|d| d != delegate).collect();
165
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
166
        self.replace_identity(id, sk, &format!("oxy identity delegate remove {}", delegate.short(12)))
167
    }
168
169
    /// Revoke `key` (your own, or any key when you hold a policy root):
170
    /// record a signed `revoke` attestation and a local marker, and drop
171
    /// the key from the policy where it has one.
172
    pub fn revoke_identity(&self, key: &KeyId, reason: &str) -> Result<Option<PolicyUpdate>> {
173
        let Some((own, sk)) = self.secret_key()? else {
174
            return fail("no secret key: run `oxy identity create` first");
175
        };
176
        let policy = self.current_policy()?.map(|(_, sp)| sp.policy);
177
        let is_root = policy.as_ref().is_some_and(|p| p.roots.contains(&own));
178
        if *key != own && !is_root {
179
            return fail("only the key's holder or a policy root can revoke it");
180
        }
181
        let attest = Attest { repo: self.id()?, hlc: Hlc::now(), body: AttestBody::Revoke { key: *key, reason: reason.into() } };
182
        let r = self.store.put_attest(&attest)?;
183
        self.store.put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?;
184
        let dir = self.oxy.join("revoked");
185
        fs::create_dir_all(&dir)?;
186
        write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?;
187
        match policy {
188
            Some(p) if p.mentions(key) => Ok(Some(self.policy_revoke(key)?)),
189
            _ => Ok(None),
190
        }
191
    }
192
193
    /// Whether `key` is revoked by the policy or by a recorded revocation.
194
    pub fn is_revoked(&self, key: &KeyId) -> Result<bool> {
195
        if self.oxy.join("revoked").join(key.to_hex()).exists() {
196
            return Ok(true);
197
        }
198
        Ok(self.current_policy()?.is_some_and(|(_, sp)| sp.policy.is_revoked(key)))
199
    }
200
201
    /// The secret key for `author.key`, if configured and present on disk.
202
    /// The identity object is kept beside the secret so a repository that has
203
    /// never seen it (a fresh clone) can store it when it first signs.
204
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
205
        let Some(key) = self.config.author_key else { return Ok(None) };
206
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
207
        match fs::read(&path) {
208
            Ok(b) => {
209
                let (k, sk) = SecretKey::decode(&b)?;
210
                if k != key {
211
                    return fail(format!("{} holds a different key than its name", path.display()));
212
                }
213
                if !self.store.has(&key) {
214
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
215
                    if let Ok(bytes) = fs::read(&id_path) {
216
                        let id = Identity::decode(&bytes)?;
217
                        if id.hash() == key {
218
                            self.store.put_identity(&id)?;
219
                        }
220
                    }
221
                }
222
                Ok(Some((k, sk)))
223
            }
224
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
225
            Err(e) => Err(e.into()),
226
        }
227
    }
228
229
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
230
        Ok(self.store.get_identity(key)?)
231
    }
232
233
    /// Sign a stored change with the configured secret key.
234
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
235
        let Some((key, sk)) = self.secret_key()? else {
236
            return fail("no secret key: run `oxy identity create` first");
237
        };
238
        let sig = sk.sign(Domain::Change, &key, hash);
239
        self.store.put_signature(hash, &sig)?;
240
        Ok(sig)
241
    }
242
243
    /// Check the signatures on a change against the author-or-delegate rule.
244
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
245
        let change = self.store.get_change(hash)?;
246
        let sigs = self.store.signatures(hash)?;
247
        if sigs.is_empty() {
248
            return Ok(SignatureStatus::Unsigned);
249
        }
250
        let mut last_err = String::new();
251
        for sig in &sigs {
252
            match self.check_signer(&change, sig, Domain::Change, hash) {
253
                Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: false, revoked: self.is_revoked(&sig.key)? }),
254
                Err(e) => last_err = e,
255
            }
256
        }
257
        Ok(SignatureStatus::Invalid(last_err))
258
    }
259
260
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
261
    fn check_signer(&self, change: &Change, sig: &Signature, domain: Domain, target: &ObjectRef) -> std::result::Result<Identity, String> {
262
        let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
263
        let authorised =
264
            sig.key == change.author || self.store.get_identity(&change.author).map(|a| a.delegates.contains(&sig.key)).unwrap_or(false);
265
        if !authorised {
266
            return Err(format!("key {} is neither the author nor a delegate", sig.key.short(12)));
267
        }
268
        if id.is_expired_at(change.hlc.wall_ms) {
269
            return Err(format!("key {} had expired when the change was made", sig.key.short(12)));
270
        }
271
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
272
        Ok(id)
273
    }
274
275
    /// Like `verify_change`, but a change with no valid individual signature
276
    /// is also authenticated by a valid seal covering it (spec §14.4).
277
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
278
        let direct = self.verify_change(hash)?;
279
        if matches!(direct, SignatureStatus::Valid { .. }) {
280
            return Ok(direct);
281
        }
282
        let change = self.store.get_change(hash)?;
283
        let mut last_err = match &direct {
284
            SignatureStatus::Invalid(e) => e.clone(),
285
            _ => String::new(),
286
        };
287
        for seal_ref in self.store.seals_for(hash)? {
288
            let attest = self.store.get_attest(&seal_ref)?;
289
            let AttestBody::Seal { hashes } = &attest.body else { continue };
290
            if !hashes.contains(hash) {
291
                continue;
292
            }
293
            for sig in self.store.signatures(&seal_ref)? {
294
                match self.check_signer(&change, &sig, Domain::Attest, &seal_ref) {
295
                    Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: true, revoked: self.is_revoked(&sig.key)? }),
296
                    Err(e) => last_err = e,
297
                }
298
            }
299
        }
300
        if last_err.is_empty() { Ok(SignatureStatus::Unsigned) } else { Ok(SignatureStatus::Invalid(last_err)) }
301
    }
302
303
    /// Store and sign an attestation with the configured key.
304
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
305
        let Some((key, sk)) = self.secret_key()? else {
306
            return fail("no secret key: run `oxy identity create` first");
307
        };
308
        let r = self.store.put_attest(attest)?;
309
        let sig = sk.sign(Domain::Attest, &key, &r);
310
        self.store.put_signature(&r, &sig)?;
311
        if let AttestBody::Seal { hashes } = &attest.body {
312
            for h in hashes {
313
                self.store.index_seal(h, &r)?;
314
            }
315
        }
316
        Ok(SignedAttest { attest: attest.clone(), sigs: vec![sig] })
317
    }
318
}