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