OXYGENHUB
# 02e691881a4e oxygen / crates / oxy-core / src / repo / identity.rs
20.8 KiB · Rust · last changed Scoped, expiring delegates, and revocation by the delegator 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 {
12
        key: KeyId,
13
        name: String,
14
        email: String,
15
        sealed: bool,
16
        revoked: bool,
17
    },
18
    Invalid(String),
19
}
20
21
/// The outcome of replacing the configured identity (spec §14.2).
22
#[derive(Clone, Debug)]
23
pub struct Rotation {
24
    pub old: KeyId,
25
    pub new: KeyId,
26
    pub identity: Identity,
27
    /// The policy update that moved the old key's grants to the new key,
28
    /// when the repository has a policy naming the old key.
29
    pub policy: Option<PolicyUpdate>,
30
}
31
32
impl Repo {
33
    // ------------------------------------------------------------ identity
34
35
    /// Generate a composite key pair, store its identity object, keep the
36
    /// secret in the key directory, and record the author at `scope`.
37
    pub fn create_identity(
38
        &mut self,
39
        name: &str,
40
        email: &str,
41
        scope: IdentityScope,
42
    ) -> Result<(KeyId, Identity)> {
43
        self.create_identity_with(name, email, scope, None)
44
    }
45
46
    /// `create_identity` with an expiry (milliseconds since the epoch).
47
    pub fn create_identity_with(
48
        &mut self,
49
        name: &str,
50
        email: &str,
51
        scope: IdentityScope,
52
        expires: Option<u64>,
53
    ) -> Result<(KeyId, Identity)> {
54
        let sk = SecretKey::generate();
55
        let mut id = sk.identity(name, email, Hlc::now().wall_ms);
56
        id.expires = expires;
57
        let key = self.install_key(&id, &sk)?;
58
        self.set_author(&id, &key, scope)?;
59
        Ok((key, id))
60
    }
61
62
    /// Store an identity and keep its secret (and a copy of the identity)
63
    /// in the key directory.
64
    fn install_key(&self, id: &Identity, sk: &SecretKey) -> Result<KeyId> {
65
        let key = self.store.put_identity(id)?;
66
        fs::create_dir_all(&self.keys_dir)?;
67
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
68
        write_atomic(&path, &sk.encode(&key))?;
69
        write_atomic(
70
            &self.keys_dir.join(format!("{}.identity", key.to_hex())),
71
            &id.encode(),
72
        )?;
73
        #[cfg(unix)]
74
        {
75
            use std::os::unix::fs::PermissionsExt;
76
            fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
77
        }
78
        Ok(key)
79
    }
80
81
    fn set_author(&mut self, id: &Identity, key: &KeyId, scope: IdentityScope) -> Result<()> {
82
        match scope {
83
            IdentityScope::Repo => {
84
                self.config.set("author.name", &id.name)?;
85
                self.config.set("author.email", &id.email)?;
86
                self.config.set("author.key", &key.to_hex())?;
87
                self.save_config()?;
88
            }
89
            IdentityScope::User => {
90
                Config::set_user(&self.user_config, "author.name", &id.name)?;
91
                Config::set_user(&self.user_config, "author.email", &id.email)?;
92
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
93
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
94
            }
95
        }
96
        Ok(())
97
    }
98
99
    /// The scope that holds the configured author key.
100
    fn author_scope(&self) -> IdentityScope {
101
        if self.config.repo_layer().contains_key("author.key") {
102
            IdentityScope::Repo
103
        } else {
104
            IdentityScope::User
105
        }
106
    }
107
108
    /// Replace the configured identity by `new` signed with `sk`: store it,
109
    /// record a `rotate` attestation signed by the old key, move the old
110
    /// key's policy grants to the new key, and switch the author config.
111
    fn replace_identity(
112
        &mut self,
113
        new: Identity,
114
        sk: SecretKey,
115
        command: &str,
116
    ) -> Result<Rotation> {
117
        let Some((old, old_sk)) = self.secret_key()? else {
118
            return fail("no secret key: run `oxy identity create` first");
119
        };
120
        if new.previous != Some(old) {
121
            return fail("the successor identity must name the current key as previous");
122
        }
123
        let new_key = self.install_key(&new, &sk)?;
124
        let attest = Attest {
125
            repo: self.id()?,
126
            hlc: Hlc::now(),
127
            body: AttestBody::Rotate { new: new_key },
128
        };
129
        let r = self.store.put_attest(&attest)?;
130
        self.store
131
            .put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?;
132
        let policy = match self.current_policy()? {
133
            Some((_, sp)) if sp.policy.mentions(&old) => {
134
                let mut p = self.next_policy()?;
135
                p.substitute_key(&old, new.clone());
136
                Some(self.sign_policy_with(p, &[(old, &old_sk), (new_key, &sk)], command)?)
137
            }
138
            _ => None,
139
        };
140
        self.set_author(&new, &new_key, self.author_scope())?;
141
        Ok(Rotation {
142
            old,
143
            new: new_key,
144
            identity: new,
145
            policy,
146
        })
147
    }
148
149
    /// Rotate to a fresh key pair; the new identity names the old one as
150
    /// `previous` and keeps its name, email, delegates, and expiry.
151
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
152
        let Some((old, _)) = self.secret_key()? else {
153
            return fail("no secret key: run `oxy identity create` first");
154
        };
155
        let cur = self.identity(&old)?;
156
        let sk = SecretKey::generate();
157
        let mut id = sk.identity(&cur.name, &cur.email, Hlc::now().wall_ms);
158
        id.previous = Some(old);
159
        id.delegates = cur.delegates.clone();
160
        id.expires = cur.expires;
161
        self.replace_identity(id, sk, "oxy identity rotate")
162
    }
163
164
    /// Re-issue the identity with the same keys and a new expiry.
165
    pub fn renew_identity(&mut self, expires: Option<u64>) -> Result<Rotation> {
166
        let Some((old, sk)) = self.secret_key()? else {
167
            return fail("no secret key: run `oxy identity create` first");
168
        };
169
        let cur = self.identity(&old)?;
170
        let id = cur.successor(cur.delegates.clone(), expires, Hlc::now().wall_ms);
171
        self.replace_identity(id, sk, "oxy identity renew")
172
    }
173
174
    /// Let `delegate` act on this identity's behalf within `scope` until
175
    /// `expires` (spec §14.2). Delegates are part of the identity object,
176
    /// so this re-issues it with the same keys.
177
    pub fn delegate_add(
178
        &mut self,
179
        delegate: Identity,
180
        scope: Scope,
181
        expires: Option<u64>,
182
    ) -> Result<Rotation> {
183
        let Some((old, sk)) = self.secret_key()? else {
184
            return fail("no secret key: run `oxy identity create` first");
185
        };
186
        if scope.is_empty() {
187
            return fail("a delegate needs at least one scope: changes, refs or seal");
188
        }
189
        let dk = self.store.put_identity(&delegate)?;
190
        if dk == old {
191
            return fail("an identity cannot delegate to itself");
192
        }
193
        let cur = self.identity(&old)?;
194
        if cur.delegate(&dk).is_some() {
195
            return fail(format!(
196
                "{} is already a delegate; remove it first to change its scope or expiry",
197
                dk.short(12)
198
            ));
199
        }
200
        let mut delegates = cur.delegates.clone();
201
        delegates.push(Delegate {
202
            key: dk,
203
            scope,
204
            expires,
205
        });
206
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
207
        self.replace_identity(
208
            id,
209
            sk,
210
            &format!("oxy identity delegate add {} --scope {scope}", dk.short(12)),
211
        )
212
    }
213
214
    /// Withdraw a delegation; re-issues the identity with the same keys.
215
    pub fn delegate_remove(&mut self, delegate: &KeyId) -> Result<Rotation> {
216
        let Some((old, sk)) = self.secret_key()? else {
217
            return fail("no secret key: run `oxy identity create` first");
218
        };
219
        let cur = self.identity(&old)?;
220
        if cur.delegate(delegate).is_none() {
221
            return fail(format!("{} is not a delegate", delegate.short(12)));
222
        }
223
        let delegates = cur
224
            .delegates
225
            .iter()
226
            .copied()
227
            .filter(|d| d.key != *delegate)
228
            .collect();
229
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
230
        self.replace_identity(
231
            id,
232
            sk,
233
            &format!("oxy identity delegate remove {}", delegate.short(12)),
234
        )
235
    }
236
237
    /// Revoke `key` (your own, one you delegated to, or any key when you
238
    /// hold a policy root): record a signed `revoke` attestation and a
239
    /// local marker, and drop the key from the policy where it has one.
240
    /// Revoking a delegate does not re-issue the identity; `delegate_remove`
241
    /// does that, and the CLI does both.
242
    pub fn revoke_identity(&self, key: &KeyId, reason: &str) -> Result<Option<PolicyUpdate>> {
243
        let Some((own, sk)) = self.secret_key()? else {
244
            return fail("no secret key: run `oxy identity create` first");
245
        };
246
        let policy = self.current_policy()?.map(|(_, sp)| sp.policy);
247
        let is_root = policy.as_ref().is_some_and(|p| p.roots.contains(&own));
248
        let is_delegate = self.identity(&own)?.delegate(key).is_some();
249
        if *key != own && !is_root && !is_delegate {
250
            return fail("only the key's holder, the identity that delegated to it, or a policy root can revoke it");
251
        }
252
        let attest = Attest {
253
            repo: self.id()?,
254
            hlc: Hlc::now(),
255
            body: AttestBody::Revoke {
256
                key: *key,
257
                reason: reason.into(),
258
            },
259
        };
260
        let r = self.store.put_attest(&attest)?;
261
        self.store
262
            .put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?;
263
        let dir = self.oxy.join("revoked");
264
        fs::create_dir_all(&dir)?;
265
        write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?;
266
        match policy {
267
            Some(p) if p.mentions(key) => Ok(Some(self.policy_revoke(key)?)),
268
            _ => Ok(None),
269
        }
270
    }
271
272
    /// Whether `key` is revoked by the policy or by a recorded revocation.
273
    pub fn is_revoked(&self, key: &KeyId) -> Result<bool> {
274
        if self.oxy.join("revoked").join(key.to_hex()).exists() {
275
            return Ok(true);
276
        }
277
        Ok(self
278
            .current_policy()?
279
            .is_some_and(|(_, sp)| sp.policy.is_revoked(key)))
280
    }
281
282
    /// The secret key for `author.key`, if configured and present on disk.
283
    /// The identity object is kept beside the secret so a repository that has
284
    /// never seen it (a fresh clone) can store it when it first signs.
285
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
286
        let Some(key) = self.config.author_key else {
287
            return Ok(None);
288
        };
289
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
290
        match fs::read(&path) {
291
            Ok(b) => {
292
                let (k, sk) = SecretKey::decode(&b)?;
293
                if k != key {
294
                    return fail(format!(
295
                        "{} holds a different key than its name",
296
                        path.display()
297
                    ));
298
                }
299
                if !self.store.has(&key) {
300
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
301
                    if let Ok(bytes) = fs::read(&id_path) {
302
                        let id = Identity::decode(&bytes)?;
303
                        if id.hash() == key {
304
                            self.store.put_identity(&id)?;
305
                        }
306
                    }
307
                }
308
                Ok(Some((k, sk)))
309
            }
310
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
311
            Err(e) => Err(e.into()),
312
        }
313
    }
314
315
    /// An identity: from this repository's store, or else from the key
316
    /// directory, where `oxy identity create` keeps the user's own (a new
317
    /// repository has not stored it yet). The file must hash to `key`.
318
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
319
        match self.store.get_identity(key) {
320
            Ok(id) => Ok(id),
321
            Err(e) => {
322
                let path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
323
                match fs::read(&path) {
324
                    Ok(bytes) => {
325
                        let id = Identity::decode(&bytes)?;
326
                        if id.hash() != *key {
327
                            return fail(format!(
328
                                "{} does not hold identity {}",
329
                                path.display(),
330
                                key.short(12)
331
                            ));
332
                        }
333
                        Ok(id)
334
                    }
335
                    Err(_) => Err(e.into()),
336
                }
337
            }
338
        }
339
    }
340
341
    /// Sign a stored change with the configured secret key.
342
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
343
        let Some((key, sk)) = self.secret_key()? else {
344
            return fail("no secret key: run `oxy identity create` first");
345
        };
346
        let sig = sk.sign(Domain::Change, &key, hash);
347
        self.store.put_signature(hash, &sig)?;
348
        Ok(sig)
349
    }
350
351
    /// Check the signatures on a change against the author-or-delegate rule.
352
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
353
        let change = self.store.get_change(hash)?;
354
        let sigs = self.store.signatures(hash)?;
355
        if sigs.is_empty() {
356
            return Ok(SignatureStatus::Unsigned);
357
        }
358
        let mut last_err = String::new();
359
        for sig in &sigs {
360
            match self.check_signer(&change, sig, Domain::Change, hash) {
361
                Ok(id) => {
362
                    return Ok(SignatureStatus::Valid {
363
                        key: sig.key,
364
                        name: id.name,
365
                        email: id.email,
366
                        sealed: false,
367
                        revoked: self.is_revoked(&sig.key)?,
368
                    })
369
                }
370
                Err(e) => last_err = e,
371
            }
372
        }
373
        Ok(SignatureStatus::Invalid(last_err))
374
    }
375
376
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
377
    fn check_signer(
378
        &self,
379
        change: &Change,
380
        sig: &Signature,
381
        domain: Domain,
382
        target: &ObjectRef,
383
    ) -> std::result::Result<Identity, String> {
384
        let id = self
385
            .store
386
            .get_identity(&sig.key)
387
            .map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
388
        // The author-or-delegate rule (spec §14.4). A change whose author is
389
        // the zero key names nobody (Git imports, history from before an
390
        // identity existed), so any key may adopt it with a seal; policy
391
        // then authorises that key for the reference as usual. A delegate
392
        // acts within its scope, signing changes or sealing them, and only
393
        // until its delegation expires (§14.2).
394
        if !change.author.is_zero() && sig.key != change.author {
395
            let needed = match domain {
396
                Domain::Change => Scope::CHANGES,
397
                _ => Scope::SEAL,
398
            };
399
            let delegation = self
400
                .store
401
                .get_identity(&change.author)
402
                .ok()
403
                .and_then(|a| a.delegate(&sig.key).copied());
404
            match delegation {
405
                None => {
406
                    return Err(format!(
407
                        "key {} is neither the author nor a delegate",
408
                        sig.key.short(12)
409
                    ))
410
                }
411
                Some(d) if !d.scope.has(needed) => {
412
                    return Err(format!(
413
                        "key {} is a delegate for {} only and may not {}",
414
                        sig.key.short(12),
415
                        d.scope,
416
                        if needed == Scope::SEAL {
417
                            "seal"
418
                        } else {
419
                            "sign changes"
420
                        }
421
                    ))
422
                }
423
                Some(d) if !d.allows(needed, change.hlc.wall_ms) => {
424
                    return Err(format!(
425
                        "key {}'s delegation had expired when the change was made",
426
                        sig.key.short(12)
427
                    ))
428
                }
429
                Some(_) => {}
430
            }
431
        }
432
        if id.is_expired_at(change.hlc.wall_ms) {
433
            return Err(format!(
434
                "key {} had expired when the change was made",
435
                sig.key.short(12)
436
            ));
437
        }
438
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
439
        Ok(id)
440
    }
441
442
    /// Like `verify_change`, but a change with no valid individual signature
443
    /// is also authenticated by a valid seal covering it (spec §14.4).
444
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
445
        let direct = self.verify_change(hash)?;
446
        if matches!(direct, SignatureStatus::Valid { .. }) {
447
            return Ok(direct);
448
        }
449
        let change = self.store.get_change(hash)?;
450
        let mut last_err = match &direct {
451
            SignatureStatus::Invalid(e) => e.clone(),
452
            _ => String::new(),
453
        };
454
        for seal_ref in self.store.seals_for(hash)? {
455
            let attest = self.store.get_attest(&seal_ref)?;
456
            let AttestBody::Seal { hashes } = &attest.body else {
457
                continue;
458
            };
459
            if !hashes.contains(hash) {
460
                continue;
461
            }
462
            for sig in self.store.signatures(&seal_ref)? {
463
                match self.check_signer(&change, &sig, Domain::Attest, &seal_ref) {
464
                    Ok(id) => {
465
                        return Ok(SignatureStatus::Valid {
466
                            key: sig.key,
467
                            name: id.name,
468
                            email: id.email,
469
                            sealed: true,
470
                            revoked: self.is_revoked(&sig.key)?,
471
                        })
472
                    }
473
                    Err(e) => last_err = e,
474
                }
475
            }
476
        }
477
        if last_err.is_empty() {
478
            Ok(SignatureStatus::Unsigned)
479
        } else {
480
            Ok(SignatureStatus::Invalid(last_err))
481
        }
482
    }
483
484
    /// Store and sign an attestation with the configured key.
485
    /// Seal changes with the identity key (spec §14.4): a stored, indexed
486
    /// seal attestation over `hashes`, pushed with the next push. Returns
487
    /// the attestation's reference.
488
    pub fn seal(&self, hashes: &[ChangeHash]) -> Result<ObjectRef> {
489
        if hashes.is_empty() {
490
            return fail("nothing to seal");
491
        }
492
        if hashes.len() > crate::attest::MAX_SEAL_HASHES {
493
            return fail(format!(
494
                "a seal covers at most {} changes",
495
                crate::attest::MAX_SEAL_HASHES
496
            ));
497
        }
498
        for h in hashes {
499
            if !self.store.has(h) {
500
                return fail(format!("unknown change {}", h.short(12)));
501
            }
502
        }
503
        let mut sorted = hashes.to_vec();
504
        sorted.sort();
505
        sorted.dedup();
506
        let sa = self.sign_attest(&Attest::seal(self.id()?, sorted.clone()))?;
507
        let r = self.store.put_attest(&sa.attest)?;
508
        for sig in &sa.sigs {
509
            self.store.put_signature(&r, sig)?;
510
        }
511
        for h in &sorted {
512
            self.store.index_seal(h, &r)?;
513
        }
514
        Ok(r)
515
    }
516
517
    /// The changes of the current bookmark this identity can authenticate
518
    /// but has not: its own unsigned ones and anonymous ones.
519
    pub fn sealable(&self) -> Result<Vec<ChangeHash>> {
520
        let Some((key, _)) = self.secret_key()? else {
521
            return fail("no identity: run oxy identity create");
522
        };
523
        let heads = self.bookmark_heads()?;
524
        let state = self.load_state(&heads)?;
525
        let mut out = Vec::new();
526
        for (h, meta) in state.changes() {
527
            if (meta.author == key || meta.author.is_zero())
528
                && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. })
529
            {
530
                out.push(*h);
531
            }
532
        }
533
        out.sort();
534
        Ok(out)
535
    }
536
537
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
538
        let Some((key, sk)) = self.secret_key()? else {
539
            return fail("no secret key: run `oxy identity create` first");
540
        };
541
        let r = self.store.put_attest(attest)?;
542
        let sig = sk.sign(Domain::Attest, &key, &r);
543
        self.store.put_signature(&r, &sig)?;
544
        if let AttestBody::Seal { hashes } = &attest.body {
545
            for h in hashes {
546
                self.store.index_seal(h, &r)?;
547
            }
548
        }
549
        Ok(SignedAttest {
550
            attest: attest.clone(),
551
            sigs: vec![sig],
552
        })
553
    }
554
}