OXYGENHUB
# 2b95bc24d7d6 oxygen / crates / oxy-core / src / repo / identity.rs
28.8 KiB · Rust · last changed Release 0.4.6: agents push as themselves 18 days 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
        crate::durable::write_atomic_private(&path, &sk.encode(&key))?;
69
        write_atomic(
70
            &self.keys_dir.join(format!("{}.identity", key.to_hex())),
71
            &id.encode(),
72
        )?;
73
        Ok(key)
74
    }
75
76
    fn set_author(&mut self, id: &Identity, key: &KeyId, scope: IdentityScope) -> Result<()> {
77
        match scope {
78
            IdentityScope::Repo => {
79
                self.config.set("author.name", &id.name)?;
80
                self.config.set("author.email", &id.email)?;
81
                self.config.set("author.key", &key.to_hex())?;
82
                self.save_config()?;
83
            }
84
            IdentityScope::User => {
85
                Config::set_user(&self.user_config, "author.name", &id.name)?;
86
                Config::set_user(&self.user_config, "author.email", &id.email)?;
87
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
88
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
89
            }
90
        }
91
        Ok(())
92
    }
93
94
    /// The scope that holds the configured author key.
95
    fn author_scope(&self) -> IdentityScope {
96
        if self.config.repo_layer().contains_key("author.key") {
97
            IdentityScope::Repo
98
        } else {
99
            IdentityScope::User
100
        }
101
    }
102
103
    /// Record a successor key everywhere the old one was named: the
104
    /// repository layer when it pins the old key, and the user file when
105
    /// it names the old key, so every checkout that followed the old key
106
    /// follows the new one (oxygen#69). A key named nowhere falls back to
107
    /// the scope that resolved it.
108
    fn record_successor(&mut self, id: &Identity, old: &KeyId, new_key: &KeyId) -> Result<()> {
109
        let names = |v: Option<&String>| v.and_then(|k| KeyId::from_hex(k)) == Some(*old);
110
        let repo_pins = names(self.config.repo_layer().get("author.key"));
111
        let user_entries = super::config::read_config_file(&self.user_config).unwrap_or_default();
112
        let user_names = names(user_entries.get("author.key"));
113
        if !repo_pins && !user_names {
114
            return self.set_author(id, new_key, self.author_scope());
115
        }
116
        if user_names {
117
            self.set_author(id, new_key, IdentityScope::User)?;
118
        }
119
        if repo_pins {
120
            self.set_author(id, new_key, IdentityScope::Repo)?;
121
        }
122
        Ok(())
123
    }
124
125
    /// Replace the configured identity by `new` signed with `sk`: store it,
126
    /// record a `rotate` attestation signed by the old key, move the old
127
    /// key's policy grants to the new key, and switch the author config.
128
    fn replace_identity(
129
        &mut self,
130
        new: Identity,
131
        sk: SecretKey,
132
        command: &str,
133
    ) -> Result<Rotation> {
134
        let Some((old, old_sk)) = self.secret_key()? else {
135
            return fail("no secret key: run `oxy identity create` first");
136
        };
137
        if new.previous != Some(old) {
138
            return fail("the successor identity must name the current key as previous");
139
        }
140
        let new_key = self.install_key(&new, &sk)?;
141
        let attest = Attest {
142
            repo: self.id()?,
143
            hlc: Hlc::now(),
144
            body: AttestBody::Rotate { new: new_key },
145
        };
146
        let r = self.store.put_attest(&attest)?;
147
        self.store
148
            .put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?;
149
        let policy = match self.current_policy()? {
150
            Some((_, sp)) if sp.policy.mentions(&old) => {
151
                let mut p = self.next_policy()?;
152
                p.substitute_key(&old, new.clone());
153
                Some(self.sign_policy_with(p, &[(old, &old_sk), (new_key, &sk)], command)?)
154
            }
155
            _ => None,
156
        };
157
        self.record_successor(&new, &old, &new_key)?;
158
        Ok(Rotation {
159
            old,
160
            new: new_key,
161
            identity: new,
162
            policy,
163
        })
164
    }
165
166
    /// Rotate to a fresh key pair; the new identity names the old one as
167
    /// `previous` and keeps its name, email, delegates, and expiry.
168
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
169
        let Some((old, _)) = self.secret_key()? else {
170
            return fail("no secret key: run `oxy identity create` first");
171
        };
172
        let cur = self.identity(&old)?;
173
        let sk = SecretKey::generate();
174
        let mut id = sk.identity(&cur.name, &cur.email, Hlc::now().wall_ms);
175
        id.previous = Some(old);
176
        id.delegates = cur.delegates.clone();
177
        id.expires = cur.expires;
178
        self.replace_identity(id, sk, "oxy identity rotate")
179
    }
180
181
    /// Re-issue the identity with the same keys and a new expiry.
182
    pub fn renew_identity(&mut self, expires: Option<u64>) -> Result<Rotation> {
183
        let Some((old, sk)) = self.secret_key()? else {
184
            return fail("no secret key: run `oxy identity create` first");
185
        };
186
        let cur = self.identity(&old)?;
187
        let id = cur.successor(cur.delegates.clone(), expires, Hlc::now().wall_ms);
188
        self.replace_identity(id, sk, "oxy identity renew")
189
    }
190
191
    /// Let `delegate` act on this identity's behalf within `scope` until
192
    /// `expires` (spec §14.2). Delegates are part of the identity object,
193
    /// so this re-issues it with the same keys.
194
    pub fn delegate_add(
195
        &mut self,
196
        delegate: Identity,
197
        scope: Scope,
198
        expires: Option<u64>,
199
    ) -> Result<Rotation> {
200
        self.delegate_add_many(vec![delegate], scope, expires)
201
    }
202
203
    /// As [`Repo::delegate_add`], for several delegates in one rotation.
204
    pub fn delegate_add_many(
205
        &mut self,
206
        new_delegates: Vec<Identity>,
207
        scope: Scope,
208
        expires: Option<u64>,
209
    ) -> Result<Rotation> {
210
        let Some((old, sk)) = self.secret_key()? else {
211
            return fail("no secret key: run `oxy identity create` first");
212
        };
213
        if scope.is_empty() {
214
            return fail("a delegate needs at least one scope: changes, refs or seal");
215
        }
216
        if new_delegates.is_empty() {
217
            return fail("name at least one identity file");
218
        }
219
        let cur = self.identity(&old)?;
220
        let mut delegates = cur.delegates.clone();
221
        let mut added = Vec::new();
222
        for delegate in new_delegates {
223
            let dk = self.store.put_identity(&delegate)?;
224
            added.push(dk);
225
            if dk == old {
226
                return fail("an identity cannot delegate to itself");
227
            }
228
            if cur.delegate(&dk).is_some()
229
                || delegates
230
                    .iter()
231
                    .skip(cur.delegates.len())
232
                    .any(|d| d.key == dk)
233
            {
234
                return fail(format!(
235
                    "{} is already a delegate; remove it first to change its scope or expiry",
236
                    dk.short(12)
237
                ));
238
            }
239
            delegates.push(Delegate {
240
                key: dk,
241
                scope,
242
                expires,
243
            });
244
        }
245
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
246
        self.replace_identity(
247
            id,
248
            sk,
249
            &format!(
250
                "oxy identity delegate add {} --scope {scope}",
251
                added
252
                    .iter()
253
                    .map(|k| k.short(12))
254
                    .collect::<Vec<_>>()
255
                    .join(" ")
256
            ),
257
        )
258
    }
259
260
    /// Withdraw a delegation; re-issues the identity with the same keys.
261
    pub fn delegate_remove(&mut self, delegate: &KeyId) -> Result<Rotation> {
262
        let Some((old, sk)) = self.secret_key()? else {
263
            return fail("no secret key: run `oxy identity create` first");
264
        };
265
        let cur = self.identity(&old)?;
266
        if cur.delegate(delegate).is_none() {
267
            return fail(format!("{} is not a delegate", delegate.short(12)));
268
        }
269
        let delegates = cur
270
            .delegates
271
            .iter()
272
            .copied()
273
            .filter(|d| d.key != *delegate)
274
            .collect();
275
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
276
        self.replace_identity(
277
            id,
278
            sk,
279
            &format!("oxy identity delegate remove {}", delegate.short(12)),
280
        )
281
    }
282
283
    /// Revoke `key` (your own, one you delegated to, or any key when you
284
    /// hold a policy root): record a signed `revoke` attestation and a
285
    /// local marker, and drop the key from the policy where it has one.
286
    /// Revoking a delegate does not re-issue the identity; `delegate_remove`
287
    /// does that, and the CLI does both.
288
    pub fn revoke_identity(&self, key: &KeyId, reason: &str) -> Result<Option<PolicyUpdate>> {
289
        let Some((own, sk)) = self.secret_key()? else {
290
            return fail("no secret key: run `oxy identity create` first");
291
        };
292
        let policy = self.current_policy()?.map(|(_, sp)| sp.policy);
293
        let is_root = policy.as_ref().is_some_and(|p| p.roots.contains(&own));
294
        let is_delegate = self.identity(&own)?.delegate(key).is_some();
295
        if *key != own && !is_root && !is_delegate {
296
            return fail("only the key's holder, the identity that delegated to it, or a policy root can revoke it");
297
        }
298
        let attest = Attest {
299
            repo: self.id()?,
300
            hlc: Hlc::now(),
301
            body: AttestBody::Revoke {
302
                key: *key,
303
                reason: reason.into(),
304
            },
305
        };
306
        let r = self.store.put_attest(&attest)?;
307
        self.store
308
            .put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?;
309
        let dir = self.oxy.join("revoked");
310
        fs::create_dir_all(&dir)?;
311
        write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?;
312
        match policy {
313
            Some(p) if p.mentions(key) => Ok(Some(self.policy_revoke(key)?)),
314
            _ => Ok(None),
315
        }
316
    }
317
318
    /// Whether `key` is revoked by the policy or by a recorded revocation.
319
    pub fn is_revoked(&self, key: &KeyId) -> Result<bool> {
320
        if self.oxy.join("revoked").join(key.to_hex()).exists() {
321
            return Ok(true);
322
        }
323
        Ok(self
324
            .current_policy()?
325
            .is_some_and(|(_, sp)| sp.policy.is_revoked(key)))
326
    }
327
328
    /// The key this repository signs with: `author.signer` when set (an
329
    /// agent's own key, acting for the author), else `author.key`.
330
    pub fn signing_key(&self) -> Option<KeyId> {
331
        self.config.signer_key.or(self.config.author_key)
332
    }
333
334
    /// The secret key for `author.signer`, else `author.key`, if configured
335
    /// and present on disk. The identity object is kept beside the secret
336
    /// so a repository that has never seen it (a fresh clone) can store it
337
    /// when it first signs.
338
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
339
        let Some(key) = self.signing_key() else {
340
            return Ok(None);
341
        };
342
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
343
        match fs::read(&path) {
344
            Ok(b) => {
345
                let (k, sk) = SecretKey::decode(&b)?;
346
                if k != key {
347
                    return fail(format!(
348
                        "{} holds a different key than its name",
349
                        path.display()
350
                    ));
351
                }
352
                if !self.store.has(&key) {
353
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
354
                    if let Ok(bytes) = fs::read(&id_path) {
355
                        let id = Identity::decode(&bytes)?;
356
                        if id.hash() == key {
357
                            self.store.put_identity(&id)?;
358
                        }
359
                    }
360
                }
361
                Ok(Some((k, sk)))
362
            }
363
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
364
            Err(e) => Err(e.into()),
365
        }
366
    }
367
368
    /// An identity: from this repository's store, or else from the key
369
    /// directory, where `oxy identity create` keeps the user's own (a new
370
    /// repository has not stored it yet). The file must hash to `key`.
371
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
372
        match self.store.get_identity(key) {
373
            Ok(id) => Ok(id),
374
            Err(e) => {
375
                let path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
376
                match fs::read(&path) {
377
                    Ok(bytes) => {
378
                        let id = Identity::decode(&bytes)?;
379
                        if id.hash() != *key {
380
                            return fail(format!(
381
                                "{} does not hold identity {}",
382
                                path.display(),
383
                                key.short(12)
384
                            ));
385
                        }
386
                        Ok(id)
387
                    }
388
                    // The configured author's identity is neither in the
389
                    // repository nor in this home's keys: say why and what
390
                    // to do (oxygen#82), rather than "object … not found".
391
                    Err(_) if self.config.author_key == Some(*key) => fail(format!(
392
                        "this repository has no identity {} (author.key, set in {}): it is not in the repository, nor in {}.\n\
393
                         An agent home names its human's identity, and each delegation rotates that identity, so the key goes stale. \
394
                         Pin the identity this repository uses, as its owner sees it: `oxy identity show` in the owner's shell gives the key, \
395
                         then `oxy config author.key <KEY>` here; or ask the owner.",
396
                        key.short(12),
397
                        if self.config.is_repo_set("author.key") {
398
                            "this repository's config"
399
                        } else {
400
                            "the user config"
401
                        },
402
                        self.keys_dir.display()
403
                    )),
404
                    Err(_) => Err(e.into()),
405
                }
406
            }
407
        }
408
    }
409
410
    /// Sign a stored change with the configured secret key.
411
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
412
        let Some((key, sk)) = self.secret_key()? else {
413
            return fail("no secret key: run `oxy identity create` first");
414
        };
415
        let sig = sk.sign(Domain::Change, &key, hash);
416
        self.store.put_signature(hash, &sig)?;
417
        Ok(sig)
418
    }
419
420
    /// Check the signatures on a change against the author-or-delegate rule.
421
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
422
        self.verify_change_known(hash, None)
423
    }
424
425
    /// `verify_change`, with the change's metadata when the caller has it
426
    /// (a change object can be as large as the tree it adds). Signatures
427
    /// are looked up first: an unsigned change needs no metadata.
428
    fn verify_change_known(
429
        &self,
430
        hash: &ChangeHash,
431
        known: Option<&crate::state::ChangeMeta>,
432
    ) -> Result<SignatureStatus> {
433
        let sigs = self.store.signatures(hash)?;
434
        if sigs.is_empty() {
435
            return Ok(SignatureStatus::Unsigned);
436
        }
437
        let read;
438
        let change = match known {
439
            Some(m) => m,
440
            None => {
441
                read = self.store.get_change_meta(hash)?;
442
                &read
443
            }
444
        };
445
        let mut last_err = String::new();
446
        for sig in &sigs {
447
            match self.check_signer(change, sig, Domain::Change, hash) {
448
                Ok(id) => {
449
                    return Ok(SignatureStatus::Valid {
450
                        key: sig.key,
451
                        name: self.certified_subject(&sig.key).unwrap_or(id.name),
452
                        email: id.email,
453
                        sealed: false,
454
                        revoked: self.is_revoked(&sig.key)?,
455
                    })
456
                }
457
                Err(e) => last_err = e,
458
            }
459
        }
460
        Ok(SignatureStatus::Invalid(last_err))
461
    }
462
463
    /// Whether `signer` is a short-lived key acting for `author` under a
464
    /// grant (spec §14.7): its certificate verifies with its issuer and
465
    /// carries a grant `author` signed, for the certificate's subject and
466
    /// issuer, giving `needed` at `at_ms` (the change's time, as for a
467
    /// delegation). Whether the certificate covered the time it signed is
468
    /// the policy's check, at a time the signer did not choose. `None`
469
    /// when it does; otherwise why not.
470
    fn granted(
471
        &self,
472
        author: &KeyId,
473
        signer: &KeyId,
474
        needed: Scope,
475
        at_ms: u64,
476
    ) -> std::result::Result<Option<String>, String> {
477
        let neither = || {
478
            Some(format!(
479
                "key {} is neither the author nor a delegate",
480
                signer.short(12)
481
            ))
482
        };
483
        let Ok(Some(c)) = self.store.certificate_of(signer) else {
484
            return Ok(neither());
485
        };
486
        let Ok(sc) = self.store.get_certificate(&c) else {
487
            return Ok(neither());
488
        };
489
        let Some(g) = &sc.cert.grant else {
490
            return Ok(neither());
491
        };
492
        let who = signer.short(12);
493
        let issuer = self
494
            .store
495
            .get_identity(&sc.cert.issuer)
496
            .map_err(|_| format!("key {who}'s certificate names an unknown issuer"))?;
497
        sc.verify(&issuer)
498
            .map_err(|e| format!("key {who}'s certificate: {e}"))?;
499
        if g.grant.grantor != *author {
500
            return Ok(neither());
501
        }
502
        let grantor = self
503
            .store
504
            .get_identity(author)
505
            .map_err(|_| format!("key {who}'s grant is by an unknown author"))?;
506
        g.verify(&grantor)
507
            .map_err(|e| format!("key {who}'s grant: {e}"))?;
508
        if g.grant.subject != sc.cert.subject || g.grant.issuer != sc.cert.issuer {
509
            return Err(format!(
510
                "key {who}'s grant is for {} from issuer {}, not its certificate's",
511
                g.grant.subject,
512
                g.grant.issuer.short(12)
513
            ));
514
        }
515
        if !sc.cert.scope.has(needed) || !g.grant.scope.has(needed) {
516
            return Err(format!("key {who} is not granted {needed}"));
517
        }
518
        if !g.grant.covers(needed, at_ms) {
519
            return Err(format!(
520
                "key {who}'s grant did not cover the time the change was made"
521
            ));
522
        }
523
        Ok(None)
524
    }
525
526
    /// The subject a short-lived key was certified for (spec §14.7), such
527
    /// as `agent:claude@andy`: what names it, since its identity is only
528
    /// a fresh key's.
529
    pub fn certified_subject(&self, key: &KeyId) -> Option<String> {
530
        let c = self.store.certificate_of(key).ok()??;
531
        Some(self.store.get_certificate(&c).ok()?.cert.subject)
532
    }
533
534
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
535
    fn check_signer(
536
        &self,
537
        change: &crate::state::ChangeMeta,
538
        sig: &Signature,
539
        domain: Domain,
540
        target: &ObjectRef,
541
    ) -> std::result::Result<Identity, String> {
542
        let id = self
543
            .store
544
            .get_identity(&sig.key)
545
            .map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
546
        // The author-or-delegate rule (spec §14.4). A change whose author is
547
        // the zero key names nobody (Git imports, history from before an
548
        // identity existed), so any key may adopt it with a seal; policy
549
        // then authorises that key for the reference as usual. A delegate
550
        // acts within its scope, signing changes or sealing them, and only
551
        // until its delegation expires (§14.2).
552
        if !change.author.is_zero() && sig.key != change.author {
553
            let needed = match domain {
554
                Domain::Change => Scope::CHANGES,
555
                _ => Scope::SEAL,
556
            };
557
            let delegation = self
558
                .store
559
                .get_identity(&change.author)
560
                .ok()
561
                .and_then(|a| a.delegate(&sig.key).copied());
562
            match delegation {
563
                // A certified key acting under the author's grant (§14.7).
564
                None => {
565
                    if let Some(why) =
566
                        self.granted(&change.author, &sig.key, needed, change.hlc.wall_ms)?
567
                    {
568
                        return Err(why);
569
                    }
570
                }
571
                Some(d) if !d.scope.has(needed) => {
572
                    return Err(format!(
573
                        "key {} is a delegate for {} only and may not {}",
574
                        sig.key.short(12),
575
                        d.scope,
576
                        if needed == Scope::SEAL {
577
                            "seal"
578
                        } else {
579
                            "sign changes"
580
                        }
581
                    ))
582
                }
583
                Some(d) if !d.allows(needed, change.hlc.wall_ms) => {
584
                    return Err(format!(
585
                        "key {}'s delegation had expired when the change was made",
586
                        sig.key.short(12)
587
                    ))
588
                }
589
                Some(_) => {}
590
            }
591
        }
592
        if id.is_expired_at(change.hlc.wall_ms) {
593
            return Err(format!(
594
                "key {} had expired when the change was made",
595
                sig.key.short(12)
596
            ));
597
        }
598
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
599
        Ok(id)
600
    }
601
602
    /// Like `verify_change`, but a change with no valid individual signature
603
    /// is also authenticated by a valid seal covering it (spec §14.4).
604
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
605
        self.authenticate_change_known(hash, None)
606
    }
607
608
    /// `authenticate_change`, with the change's metadata when the caller
609
    /// has it (from a log, say), so the change itself need not be read.
610
    pub fn authenticate_change_known(
611
        &self,
612
        hash: &ChangeHash,
613
        known: Option<&crate::state::ChangeMeta>,
614
    ) -> Result<SignatureStatus> {
615
        let direct = self.verify_change_known(hash, known)?;
616
        if matches!(direct, SignatureStatus::Valid { .. }) {
617
            return Ok(direct);
618
        }
619
        let mut last_err = match &direct {
620
            SignatureStatus::Invalid(e) => e.clone(),
621
            _ => String::new(),
622
        };
623
        let seals = self.store.seals_for(hash)?;
624
        if seals.is_empty() {
625
            return Ok(match last_err.is_empty() {
626
                true => SignatureStatus::Unsigned,
627
                false => SignatureStatus::Invalid(last_err),
628
            });
629
        }
630
        let read;
631
        let change = match known {
632
            Some(m) => m,
633
            None => {
634
                read = self.store.get_change_meta(hash)?;
635
                &read
636
            }
637
        };
638
        for seal_ref in seals {
639
            let attest = self.store.get_attest(&seal_ref)?;
640
            let AttestBody::Seal { hashes } = &attest.body else {
641
                continue;
642
            };
643
            if !hashes.contains(hash) {
644
                continue;
645
            }
646
            for sig in self.store.signatures(&seal_ref)? {
647
                match self.check_signer(change, &sig, Domain::Attest, &seal_ref) {
648
                    Ok(id) => {
649
                        return Ok(SignatureStatus::Valid {
650
                            key: sig.key,
651
                            name: self.certified_subject(&sig.key).unwrap_or(id.name),
652
                            email: id.email,
653
                            sealed: true,
654
                            revoked: self.is_revoked(&sig.key)?,
655
                        })
656
                    }
657
                    Err(e) => last_err = e,
658
                }
659
            }
660
        }
661
        if last_err.is_empty() {
662
            Ok(SignatureStatus::Unsigned)
663
        } else {
664
            Ok(SignatureStatus::Invalid(last_err))
665
        }
666
    }
667
668
    /// Store and sign an attestation with the configured key.
669
    /// Seal changes with the identity key (spec §14.4): a stored, indexed
670
    /// seal attestation over `hashes`, pushed with the next push. Returns
671
    /// the attestation's reference.
672
    pub fn seal(&self, hashes: &[ChangeHash]) -> Result<ObjectRef> {
673
        if hashes.is_empty() {
674
            return fail("nothing to seal");
675
        }
676
        if hashes.len() > crate::attest::MAX_SEAL_HASHES {
677
            return fail(format!(
678
                "a seal covers at most {} changes",
679
                crate::attest::MAX_SEAL_HASHES
680
            ));
681
        }
682
        for h in hashes {
683
            if !self.store.has(h) {
684
                return fail(format!("unknown change {}", h.short(12)));
685
            }
686
        }
687
        let mut sorted = hashes.to_vec();
688
        sorted.sort();
689
        sorted.dedup();
690
        let sa = self.sign_attest(&Attest::seal(self.id()?, sorted.clone()))?;
691
        let r = self.store.put_attest(&sa.attest)?;
692
        for sig in &sa.sigs {
693
            self.store.put_signature(&r, sig)?;
694
        }
695
        for h in &sorted {
696
            self.store.index_seal(h, &r)?;
697
        }
698
        Ok(r)
699
    }
700
701
    /// Whether `signer` seals changes by `author`: its own, anonymous ones
702
    /// (§14.4), and, when it signs for the configured author (a delegate,
703
    /// or a short-lived key under the author's grant, §14.7), the
704
    /// author's. A receiver still checks it may.
705
    pub(crate) fn seals_for_author(&self, author: &KeyId, signer: &KeyId) -> bool {
706
        author == signer
707
            || author.is_zero()
708
            || (self.config.signer_key.is_some() && self.config.author_key == Some(*author))
709
    }
710
711
    /// The changes of the current bookmark this identity can authenticate
712
    /// but has not: its own unsigned ones, anonymous ones, and the
713
    /// author's when it signs for them.
714
    pub fn sealable(&self) -> Result<Vec<ChangeHash>> {
715
        let Some((key, _)) = self.secret_key()? else {
716
            return fail("no identity: run oxy identity create");
717
        };
718
        let heads = self.bookmark_heads()?;
719
        let state = self.load_state_history(&heads)?;
720
        let mut out = Vec::new();
721
        for (h, meta) in state.changes() {
722
            if self.seals_for_author(&meta.author, &key)
723
                && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. })
724
            {
725
                out.push(*h);
726
            }
727
        }
728
        out.sort();
729
        Ok(out)
730
    }
731
732
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
733
        let Some((key, sk)) = self.secret_key()? else {
734
            return fail("no secret key: run `oxy identity create` first");
735
        };
736
        let r = self.store.put_attest(attest)?;
737
        let sig = sk.sign(Domain::Attest, &key, &r);
738
        self.store.put_signature(&r, &sig)?;
739
        if let AttestBody::Seal { hashes } = &attest.body {
740
            for h in hashes {
741
                self.store.index_seal(h, &r)?;
742
            }
743
        }
744
        Ok(SignedAttest {
745
            attest: attest.clone(),
746
            sigs: vec![sig],
747
        })
748
    }
749
}