OXYGENHUB

Coverage / crates/oxy-core/src/repo/identity.rs 88.4%

481 of 544 lines run, 60 of 90 functions · run #369 on main · run missed

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`.
3772×
    pub fn create_identity(
3872×
        &mut self,
3972×
        name: &str,
4072×
        email: &str,
4172×
        scope: IdentityScope,
4272×
    ) -> Result<(KeyId, Identity)> {
4372×
        self.create_identity_with(name, email, scope, None)
4472×
    }
45
46
    /// `create_identity` with an expiry (milliseconds since the epoch).
4775×
    pub fn create_identity_with(
4875×
        &mut self,
4975×
        name: &str,
5075×
        email: &str,
5175×
        scope: IdentityScope,
5275×
        expires: Option<u64>,
5375×
    ) -> Result<(KeyId, Identity)> {
5475×
        let sk = SecretKey::generate();
5575×
        let mut id = sk.identity(name, email, Hlc::now().wall_ms);
5675×
        id.expires = expires;
5775×
        let key = self.install_key(&id, &sk)?;
5875×
        self.set_author(&id, &key, scope)?;
5975×
        Ok((key, id))
6075×
    }
61
62
    /// Store an identity and keep its secret (and a copy of the identity)
63
    /// in the key directory.
6493×
    fn install_key(&self, id: &Identity, sk: &SecretKey) -> Result<KeyId> {
6593×
        let key = self.store.put_identity(id)?;
6693×
        fs::create_dir_all(&self.keys_dir)?;
6793×
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
6893×
        crate::durable::write_atomic_private(&path, &sk.encode(&key))?;
6993×
        write_atomic(
7093×
            &self.keys_dir.join(format!("{}.identity", key.to_hex())),
7193×
            &id.encode(),
720×
        )?;
7393×
        Ok(key)
7493×
    }
75
7694×
    fn set_author(&mut self, id: &Identity, key: &KeyId, scope: IdentityScope) -> Result<()> {
7794×
        match scope {
78
            IdentityScope::Repo => {
7937×
                self.config.set("author.name", &id.name)?;
8037×
                self.config.set("author.email", &id.email)?;
8137×
                self.config.set("author.key", &key.to_hex())?;
8237×
                self.save_config()?;
83
            }
84
            IdentityScope::User => {
8557×
                Config::set_user(&self.user_config, "author.name", &id.name)?;
8657×
                Config::set_user(&self.user_config, "author.email", &id.email)?;
8757×
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
8857×
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
89
            }
90
        }
9194×
        Ok(())
9294×
    }
93
94
    /// The scope that holds the configured author key.
950×
    fn author_scope(&self) -> IdentityScope {
960×
        if self.config.repo_layer().contains_key("author.key") {
970×
            IdentityScope::Repo
98
        } else {
990×
            IdentityScope::User
100
        }
1010×
    }
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.
10818×
    fn record_successor(&mut self, id: &Identity, old: &KeyId, new_key: &KeyId) -> Result<()> {
10936×
        let names = |v: Option<&String>| v.and_then(|k| KeyId::from_hex(k)) == Some(*old);
11018×
        let repo_pins = names(self.config.repo_layer().get("author.key"));
11118×
        let user_entries = super::config::read_config_file(&self.user_config).unwrap_or_default();
11218×
        let user_names = names(user_entries.get("author.key"));
11318×
        if !repo_pins && !user_names {
1140×
            return self.set_author(id, new_key, self.author_scope());
11518×
        }
11618×
        if user_names {
1177×
            self.set_author(id, new_key, IdentityScope::User)?;
11811×
        }
11918×
        if repo_pins {
12012×
            self.set_author(id, new_key, IdentityScope::Repo)?;
1216×
        }
12218×
        Ok(())
12318×
    }
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.
12818×
    fn replace_identity(
12918×
        &mut self,
13018×
        new: Identity,
13118×
        sk: SecretKey,
13218×
        command: &str,
13318×
    ) -> Result<Rotation> {
13418×
        let Some((old, old_sk)) = self.secret_key()? else {
1350×
            return fail("no secret key: run `oxy identity create` first");
136
        };
13718×
        if new.previous != Some(old) {
1380×
            return fail("the successor identity must name the current key as previous");
13918×
        }
14018×
        let new_key = self.install_key(&new, &sk)?;
14118×
        let attest = Attest {
14218×
            repo: self.id()?,
14318×
            hlc: Hlc::now(),
14418×
            body: AttestBody::Rotate { new: new_key },
145
        };
14618×
        let r = self.store.put_attest(&attest)?;
14718×
        self.store
14818×
            .put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?;
14918×
        let policy = match self.current_policy()? {
1509×
            Some((_, sp)) if sp.policy.mentions(&old) => {
1519×
                let mut p = self.next_policy()?;
1529×
                p.substitute_key(&old, new.clone());
1539×
                Some(self.sign_policy_with(p, &[(old, &old_sk), (new_key, &sk)], command)?)
154
            }
1559×
            _ => None,
156
        };
15718×
        self.record_successor(&new, &old, &new_key)?;
15818×
        Ok(Rotation {
15918×
            old,
16018×
            new: new_key,
16118×
            identity: new,
16218×
            policy,
16318×
        })
16418×
    }
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.
1683×
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
1693×
        let Some((old, _)) = self.secret_key()? else {
1700×
            return fail("no secret key: run `oxy identity create` first");
171
        };
1723×
        let cur = self.identity(&old)?;
1733×
        let sk = SecretKey::generate();
1743×
        let mut id = sk.identity(&cur.name, &cur.email, Hlc::now().wall_ms);
1753×
        id.previous = Some(old);
1763×
        id.delegates = cur.delegates.clone();
1773×
        id.expires = cur.expires;
1783×
        self.replace_identity(id, sk, "oxy identity rotate")
1793×
    }
180
181
    /// Re-issue the identity with the same keys and a new expiry.
1822×
    pub fn renew_identity(&mut self, expires: Option<u64>) -> Result<Rotation> {
1832×
        let Some((old, sk)) = self.secret_key()? else {
1840×
            return fail("no secret key: run `oxy identity create` first");
185
        };
1862×
        let cur = self.identity(&old)?;
1872×
        let id = cur.successor(cur.delegates.clone(), expires, Hlc::now().wall_ms);
1882×
        self.replace_identity(id, sk, "oxy identity renew")
1892×
    }
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.
19412×
    pub fn delegate_add(
19512×
        &mut self,
19612×
        delegate: Identity,
19712×
        scope: Scope,
19812×
        expires: Option<u64>,
19912×
    ) -> Result<Rotation> {
20012×
        self.delegate_add_many(vec![delegate], scope, expires)
20112×
    }
202
203
    /// As [`Repo::delegate_add`], for several delegates in one rotation.
20413×
    pub fn delegate_add_many(
20513×
        &mut self,
20613×
        new_delegates: Vec<Identity>,
20713×
        scope: Scope,
20813×
        expires: Option<u64>,
20913×
    ) -> Result<Rotation> {
21013×
        let Some((old, sk)) = self.secret_key()? else {
2110×
            return fail("no secret key: run `oxy identity create` first");
212
        };
21313×
        if scope.is_empty() {
2140×
            return fail("a delegate needs at least one scope: changes, refs or seal");
21513×
        }
21613×
        if new_delegates.is_empty() {
2170×
            return fail("name at least one identity file");
21813×
        }
21913×
        let cur = self.identity(&old)?;
22013×
        let mut delegates = cur.delegates.clone();
22113×
        let mut added = Vec::new();
22214×
        for delegate in new_delegates {
22314×
            let dk = self.store.put_identity(&delegate)?;
22414×
            added.push(dk);
22514×
            if dk == old {
2260×
                return fail("an identity cannot delegate to itself");
22714×
            }
22814×
            if cur.delegate(&dk).is_some()
22912×
                || delegates
23012×
                    .iter()
23112×
                    .skip(cur.delegates.len())
23212×
                    .any(|d| d.key == dk)
233
            {
2342×
                return fail(format!(
235
                    "{} is already a delegate; remove it first to change its scope or expiry",
2362×
                    dk.short(12)
237
                ));
23812×
            }
23912×
            delegates.push(Delegate {
24012×
                key: dk,
24112×
                scope,
24212×
                expires,
24312×
            });
244
        }
24511×
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
24611×
        self.replace_identity(
24711×
            id,
24811×
            sk,
24911×
            &format!(
250
                "oxy identity delegate add {} --scope {scope}",
25111×
                added
25211×
                    .iter()
25312×
                    .map(|k| k.short(12))
25411×
                    .collect::<Vec<_>>()
25511×
                    .join(" ")
256
            ),
257
        )
25813×
    }
259
260
    /// Withdraw a delegation; re-issues the identity with the same keys.
2613×
    pub fn delegate_remove(&mut self, delegate: &KeyId) -> Result<Rotation> {
2623×
        let Some((old, sk)) = self.secret_key()? else {
2630×
            return fail("no secret key: run `oxy identity create` first");
264
        };
2653×
        let cur = self.identity(&old)?;
2663×
        if cur.delegate(delegate).is_none() {
2671×
            return fail(format!("{} is not a delegate", delegate.short(12)));
2682×
        }
2692×
        let delegates = cur
2702×
            .delegates
2712×
            .iter()
2722×
            .copied()
2732×
            .filter(|d| d.key != *delegate)
2742×
            .collect();
2752×
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
2762×
        self.replace_identity(
2772×
            id,
2782×
            sk,
2792×
            &format!("oxy identity delegate remove {}", delegate.short(12)),
280
        )
2813×
    }
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.
2885×
    pub fn revoke_identity(&self, key: &KeyId, reason: &str) -> Result<Option<PolicyUpdate>> {
2895×
        let Some((own, sk)) = self.secret_key()? else {
2900×
            return fail("no secret key: run `oxy identity create` first");
291
        };
2925×
        let policy = self.current_policy()?.map(|(_, sp)| sp.policy);
2935×
        let is_root = policy.as_ref().is_some_and(|p| p.roots.contains(&own));
2945×
        let is_delegate = self.identity(&own)?.delegate(key).is_some();
2955×
        if *key != own && !is_root && !is_delegate {
2961×
            return fail("only the key's holder, the identity that delegated to it, or a policy root can revoke it");
2974×
        }
2984×
        let attest = Attest {
2994×
            repo: self.id()?,
3004×
            hlc: Hlc::now(),
3014×
            body: AttestBody::Revoke {
3024×
                key: *key,
3034×
                reason: reason.into(),
3044×
            },
305
        };
3064×
        let r = self.store.put_attest(&attest)?;
3074×
        self.store
3084×
            .put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?;
3094×
        let dir = self.oxy.join("revoked");
3104×
        fs::create_dir_all(&dir)?;
3114×
        write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?;
3121×
        match policy {
3131×
            Some(p) if p.mentions(key) => Ok(Some(self.policy_revoke(key)?)),
3143×
            _ => Ok(None),
315
        }
3165×
    }
317
318
    /// Whether `key` is revoked by the policy or by a recorded revocation.
319270×
    pub fn is_revoked(&self, key: &KeyId) -> Result<bool> {
320270×
        if self.oxy.join("revoked").join(key.to_hex()).exists() {
3215×
            return Ok(true);
322265×
        }
323265×
        Ok(self
324265×
            .current_policy()?
325265×
            .is_some_and(|(_, sp)| sp.policy.is_revoked(key)))
326270×
    }
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`.
3301018×
    pub fn signing_key(&self) -> Option<KeyId> {
3311018×
        if self.config.signer_keyless {
332
            // The key certified last, once this command has signed.
3331×
            return self.keyless_key.borrow().as_ref().map(|(k, _, _)| *k);
3341017×
        }
3351017×
        self.config.signer_key.or(self.config.author_key)
3361018×
    }
337
338
    /// The secret key for `author.signer`, else `author.key`, if configured
339
    /// and present on disk. The identity object is kept beside the secret
340
    /// so a repository that has never seen it (a fresh clone) can store it
341
    /// when it first signs.
342795×
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
343795×
        if self.config.signer_keyless {
3441×
            return self.keyless_secret().map(Some);
345794×
        }
346794×
        let Some(key) = self.signing_key() else {
347221×
            return Ok(None);
348
        };
349573×
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
350573×
        match fs::read(&path) {
351572×
            Ok(b) => {
352572×
                let (k, sk) = SecretKey::decode(&b)?;
353572×
                if k != key {
3540×
                    return fail(format!(
355
                        "{} holds a different key than its name",
3560×
                        path.display()
357
                    ));
358572×
                }
359572×
                if !self.store.has(&key) {
3601×
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
3611×
                    if let Ok(bytes) = fs::read(&id_path) {
3621×
                        let id = Identity::decode(&bytes)?;
3631×
                        if id.hash() == key {
3641×
                            self.store.put_identity(&id)?;
3650×
                        }
3660×
                    }
367571×
                }
368572×
                Ok(Some((k, sk)))
369
            }
3701×
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
3710×
            Err(e) => Err(e.into()),
372
        }
373795×
    }
374
375
    /// An identity: from this repository's store, or else from the key
376
    /// directory, where `oxy identity create` keeps the user's own (a new
377
    /// repository has not stored it yet). The file must hash to `key`.
378330×
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
379330×
        match self.store.get_identity(key) {
380326×
            Ok(id) => Ok(id),
3814×
            Err(e) => {
3824×
                let path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
3834×
                match fs::read(&path) {
3842×
                    Ok(bytes) => {
3852×
                        let id = Identity::decode(&bytes)?;
3862×
                        if id.hash() != *key {
3871×
                            return fail(format!(
388
                                "{} does not hold identity {}",
3891×
                                path.display(),
3901×
                                key.short(12)
391
                            ));
3921×
                        }
3931×
                        Ok(id)
394
                    }
395
                    // The configured author's identity is neither in the
396
                    // repository nor in this home's keys: say why and what
397
                    // to do (oxygen#82), rather than "object … not found".
3982×
                    Err(_) if self.config.author_key == Some(*key) => fail(format!(
399
                        "this repository has no identity {} (author.key, set in {}): it is not in the repository, nor in {}.\n\
400
                         An agent home names its human's identity, and each delegation rotates that identity, so the key goes stale. \
401
                         Pin the identity this repository uses, as its owner sees it: `oxy identity show` in the owner's shell gives the key, \
402
                         then `oxy config author.key <KEY>` here; or ask the owner.",
4032×
                        key.short(12),
4042×
                        if self.config.is_repo_set("author.key") {
4051×
                            "this repository's config"
406
                        } else {
4071×
                            "the user config"
408
                        },
4092×
                        self.keys_dir.display()
410
                    )),
4110×
                    Err(_) => Err(e.into()),
412
                }
413
            }
414
        }
415330×
    }
416
417
    /// Sign a stored change with the configured secret key.
41845×
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
41945×
        let Some((key, sk)) = self.secret_key()? else {
4200×
            return fail("no secret key: run `oxy identity create` first");
421
        };
42245×
        let sig = sk.sign(Domain::Change, &key, hash);
42345×
        self.store.put_signature(hash, &sig)?;
42445×
        Ok(sig)
42545×
    }
426
427
    /// Check the signatures on a change against the author-or-delegate rule.
42824×
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
42924×
        self.verify_change_known(hash, None)
43024×
    }
431
432
    /// `verify_change`, with the change's metadata when the caller has it
433
    /// (a change object can be as large as the tree it adds). Signatures
434
    /// are looked up first: an unsigned change needs no metadata.
435448×
    fn verify_change_known(
436448×
        &self,
437448×
        hash: &ChangeHash,
438448×
        known: Option<&crate::state::ChangeMeta>,
439448×
    ) -> Result<SignatureStatus> {
440448×
        let sigs = self.store.signatures(hash)?;
441448×
        if sigs.is_empty() {
442350×
            return Ok(SignatureStatus::Unsigned);
44398×
        }
444
        let read;
44598×
        let change = match known {
44616×
            Some(m) => m,
447
            None => {
44882×
                read = self.store.get_change_meta(hash)?;
44982×
                &read
450
            }
451
        };
45298×
        let mut last_err = String::new();
45398×
        for sig in &sigs {
45498×
            match self.check_signer(change, sig, Domain::Change, hash) {
45592×
                Ok(id) => {
456
                    return Ok(SignatureStatus::Valid {
45792×
                        key: sig.key,
45892×
                        name: self.certified_subject(&sig.key).unwrap_or(id.name),
45992×
                        email: id.email,
460
                        sealed: false,
46192×
                        revoked: self.is_revoked(&sig.key)?,
462
                    })
463
                }
4646×
                Err(e) => last_err = e,
465
            }
466
        }
4676×
        Ok(SignatureStatus::Invalid(last_err))
468448×
    }
469
470
    /// Whether `signer` is a short-lived key acting for `author` under a
471
    /// grant (spec §14.7): its certificate verifies with its issuer and
472
    /// carries a grant `author` signed, for the certificate's subject and
473
    /// issuer, giving `needed` at `at_ms` (the change's time, as for a
474
    /// delegation). Whether the certificate covered the time it signed is
475
    /// the policy's check, at a time the signer did not choose. `None`
476
    /// when it does; otherwise why not.
47714×
    fn granted(
47814×
        &self,
47914×
        author: &KeyId,
48014×
        signer: &KeyId,
48114×
        needed: Scope,
48214×
        at_ms: u64,
48314×
    ) -> std::result::Result<Option<String>, String> {
48414×
        let neither = || {
4857×
            Some(format!(
4867×
                "key {} is neither the author nor a delegate",
4877×
                signer.short(12)
4887×
            ))
4897×
        };
49014×
        let Ok(Some(c)) = self.store.certificate_of(signer) else {
4914×
            return Ok(neither());
492
        };
49310×
        let Ok(sc) = self.store.get_certificate(&c) else {
4940×
            return Ok(neither());
495
        };
49610×
        let Some(g) = &sc.cert.grant else {
4971×
            return Ok(neither());
498
        };
4999×
        let who = signer.short(12);
5009×
        let issuer = self
5019×
            .store
5029×
            .get_identity(&sc.cert.issuer)
5039×
            .map_err(|_| format!("key {who}'s certificate names an unknown issuer"))?;
5049×
        sc.verify(&issuer)
5059×
            .map_err(|e| format!("key {who}'s certificate: {e}"))?;
506
        // A grant by an earlier key of the author's identity counts while
507
        // it was made before that key was rotated away (oxygen#135).
5089×
        if g.grant.grantor != *author && !self.granted_before_rotation(author, &g.grant) {
5092×
            return Ok(neither());
5107×
        }
5117×
        let grantor = self
5127×
            .identity(&g.grant.grantor)
5137×
            .map_err(|_| format!("key {who}'s grant is by an unknown author"))?;
5147×
        g.verify(&grantor)
5157×
            .map_err(|e| format!("key {who}'s grant: {e}"))?;
5167×
        if g.grant.subject != sc.cert.subject || g.grant.issuer != sc.cert.issuer {
5170×
            return Err(format!(
5180×
                "key {who}'s grant is for {} from issuer {}, not its certificate's",
5190×
                g.grant.subject,
5200×
                g.grant.issuer.short(12)
5210×
            ));
5227×
        }
5237×
        if !sc.cert.scope.has(needed) || !g.grant.scope.has(needed) {
5240×
            return Err(format!("key {who} is not granted {needed}"));
5257×
        }
5267×
        if !g.grant.covers(needed, at_ms) {
5272×
            return Err(format!(
5282×
                "key {who}'s grant did not cover the time the change was made"
5292×
            ));
5305×
        }
5315×
        Ok(None)
53214×
    }
533
534
    /// Whether `grant` is by a key `author`'s identity was rotated from,
535
    /// made before that rotation (oxygen#135). An owner rotates with every
536
    /// delegation, and a repository may name the newest key as the author
537
    /// while the agent's grant is by an older one; the grant stays good, as
538
    /// a policy keeps a predecessor's signatures from before the rotation
539
    /// (spec §14.5), but a key rotated away grants nothing new.
5403×
    fn granted_before_rotation(&self, author: &KeyId, grant: &crate::cert::Grant) -> bool {
5413×
        let mut cur = self.identity(author).ok();
5423×
        for _ in 0..32 {
5433×
            let Some(id) = cur else {
5440×
                return false;
545
            };
5462×
            match id.previous {
5472×
                Some(prev) if prev == grant.grantor => return grant.not_before < id.created,
5480×
                Some(prev) => cur = self.identity(&prev).ok(),
5491×
                None => return false,
550
            }
551
        }
5520×
        false
5533×
    }
554
555
    /// The subject a short-lived key was certified for (spec §14.7), such
556
    /// as `agent:claude@andy`: what names it, since its identity is only
557
    /// a fresh key's.
558255×
    pub fn certified_subject(&self, key: &KeyId) -> Option<String> {
559255×
        let c = self.store.certificate_of(key).ok()??;
5609×
        Some(self.store.get_certificate(&c).ok()?.cert.subject)
561255×
    }
562
563
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
564268×
    fn check_signer(
565268×
        &self,
566268×
        change: &crate::state::ChangeMeta,
567268×
        sig: &Signature,
568268×
        domain: Domain,
569268×
        target: &ObjectRef,
570268×
    ) -> std::result::Result<Identity, String> {
571268×
        let id = self
572268×
            .store
573268×
            .get_identity(&sig.key)
574268×
            .map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
575
        // The author-or-delegate rule (spec §14.4). A change whose author is
576
        // the zero key names nobody (Git imports, history from before an
577
        // identity existed), so any key may adopt it with a seal; policy
578
        // then authorises that key for the reference as usual. A delegate
579
        // acts within its scope, signing changes or sealing them, and only
580
        // until its delegation expires (§14.2).
581268×
        if !change.author.is_zero() && sig.key != change.author {
58234×
            let needed = match domain {
58322×
                Domain::Change => Scope::CHANGES,
58412×
                _ => Scope::SEAL,
585
            };
58634×
            let delegation = self
58734×
                .store
58834×
                .get_identity(&change.author)
58934×
                .ok()
59034×
                .and_then(|a| a.delegate(&sig.key).copied());
59119×
            match delegation {
592
                // A certified key acting under the author's grant (§14.7).
593
                None => {
5947×
                    if let Some(why) =
59514×
                        self.granted(&change.author, &sig.key, needed, change.hlc.wall_ms)?
596
                    {
5977×
                        return Err(why);
5985×
                    }
599
                }
60020×
                Some(d) if !d.scope.has(needed) => {
6011×
                    return Err(format!(
602
                        "key {} is a delegate for {} only and may not {}",
6031×
                        sig.key.short(12),
604
                        d.scope,
6051×
                        if needed == Scope::SEAL {
6061×
                            "seal"
607
                        } else {
6080×
                            "sign changes"
609
                        }
610
                    ))
611
                }
61219×
                Some(d) if !d.allows(needed, change.hlc.wall_ms) => {
6131×
                    return Err(format!(
6141×
                        "key {}'s delegation had expired when the change was made",
6151×
                        sig.key.short(12)
6161×
                    ))
617
                }
61818×
                Some(_) => {}
619
            }
620234×
        }
621257×
        if id.is_expired_at(change.hlc.wall_ms) {
6221×
            return Err(format!(
6231×
                "key {} had expired when the change was made",
6241×
                sig.key.short(12)
6251×
            ));
626256×
        }
627256×
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
628255×
        Ok(id)
629268×
    }
630
631
    /// Like `verify_change`, but a change with no valid individual signature
632
    /// is also authenticated by a valid seal covering it (spec §14.4).
633348×
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
634348×
        self.authenticate_change_known(hash, None)
635348×
    }
636
637
    /// `authenticate_change`, with the change's metadata when the caller
638
    /// has it (from a log, say), so the change itself need not be read.
639424×
    pub fn authenticate_change_known(
640424×
        &self,
641424×
        hash: &ChangeHash,
642424×
        known: Option<&crate::state::ChangeMeta>,
643424×
    ) -> Result<SignatureStatus> {
644424×
        let direct = self.verify_change_known(hash, known)?;
645424×
        if matches!(direct, SignatureStatus::Valid { .. }) {
64677×
            return Ok(direct);
647347×
        }
648347×
        let mut last_err = match &direct {
6492×
            SignatureStatus::Invalid(e) => e.clone(),
650345×
            _ => String::new(),
651
        };
652347×
        let seals = self.store.seals_for(hash)?;
653347×
        if seals.is_empty() {
654177×
            return Ok(match last_err.is_empty() {
655175×
                true => SignatureStatus::Unsigned,
6562×
                false => SignatureStatus::Invalid(last_err),
657
            });
658170×
        }
659
        let read;
660170×
        let change = match known {
6610×
            Some(m) => m,
662
            None => {
663170×
                read = self.store.get_change_meta(hash)?;
664170×
                &read
665
            }
666
        };
667170×
        for seal_ref in seals {
668170×
            let attest = self.store.get_attest(&seal_ref)?;
669170×
            let AttestBody::Seal { hashes } = &attest.body else {
6700×
                continue;
671
            };
672170×
            if !hashes.contains(hash) {
6730×
                continue;
674170×
            }
675170×
            for sig in self.store.signatures(&seal_ref)? {
676170×
                match self.check_signer(change, &sig, Domain::Attest, &seal_ref) {
677163×
                    Ok(id) => {
678
                        return Ok(SignatureStatus::Valid {
679163×
                            key: sig.key,
680163×
                            name: self.certified_subject(&sig.key).unwrap_or(id.name),
681163×
                            email: id.email,
682
                            sealed: true,
683163×
                            revoked: self.is_revoked(&sig.key)?,
684
                        })
685
                    }
6867×
                    Err(e) => last_err = e,
687
                }
688
            }
689
        }
6907×
        if last_err.is_empty() {
6910×
            Ok(SignatureStatus::Unsigned)
692
        } else {
6937×
            Ok(SignatureStatus::Invalid(last_err))
694
        }
695424×
    }
696
697
    /// Store and sign an attestation with the configured key.
698
    /// Seal changes with the identity key (spec §14.4): a stored, indexed
699
    /// seal attestation over `hashes`, pushed with the next push. Returns
700
    /// the attestation's reference.
7013×
    pub fn seal(&self, hashes: &[ChangeHash]) -> Result<ObjectRef> {
7023×
        if hashes.is_empty() {
7030×
            return fail("nothing to seal");
7043×
        }
7053×
        if hashes.len() > crate::attest::MAX_SEAL_HASHES {
7060×
            return fail(format!(
707
                "a seal covers at most {} changes",
708
                crate::attest::MAX_SEAL_HASHES
709
            ));
7103×
        }
7113×
        for h in hashes {
7123×
            if !self.store.has(h) {
7130×
                return fail(format!("unknown change {}", h.short(12)));
7143×
            }
715
        }
7163×
        let mut sorted = hashes.to_vec();
7173×
        sorted.sort();
7183×
        sorted.dedup();
7193×
        let sa = self.sign_attest(&Attest::seal(self.id()?, sorted.clone()))?;
7203×
        let r = self.store.put_attest(&sa.attest)?;
7213×
        for sig in &sa.sigs {
7223×
            self.store.put_signature(&r, sig)?;
723
        }
7243×
        for h in &sorted {
7253×
            self.store.index_seal(h, &r)?;
726
        }
7273×
        Ok(r)
7283×
    }
729
730
    /// Whether `signer` seals changes by `author`: its own, anonymous ones
731
    /// (§14.4), and, when it signs for the configured author (a delegate,
732
    /// or a short-lived key under the author's grant, §14.7), the
733
    /// author's. A receiver still checks it may.
73492×
    pub(crate) fn seals_for_author(&self, author: &KeyId, signer: &KeyId) -> bool {
73592×
        author == signer
73611×
            || author.is_zero()
73710×
            || ((self.config.signer_key.is_some() || self.config.signer_keyless)
7387×
                && self.config.author_key == Some(*author))
73992×
    }
740
741
    /// The changes of the current bookmark this identity can authenticate
742
    /// but has not: its own unsigned ones, anonymous ones, and the
743
    /// author's when it signs for them.
7440×
    pub fn sealable(&self) -> Result<Vec<ChangeHash>> {
7450×
        let Some((key, _)) = self.secret_key()? else {
7460×
            return fail("no identity: run oxy identity create");
747
        };
7480×
        let heads = self.bookmark_heads()?;
7490×
        let state = self.load_state_history(&heads)?;
7500×
        let mut out = Vec::new();
7510×
        for (h, meta) in state.changes() {
7520×
            if self.seals_for_author(&meta.author, &key)
7530×
                && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. })
7540×
            {
7550×
                out.push(*h);
7560×
            }
757
        }
7580×
        out.sort();
7590×
        Ok(out)
7600×
    }
761
762185×
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
763185×
        let Some((key, sk)) = self.secret_key()? else {
7640×
            return fail("no secret key: run `oxy identity create` first");
765
        };
766185×
        let r = self.store.put_attest(attest)?;
767183×
        let sig = sk.sign(Domain::Attest, &key, &r);
768183×
        self.store.put_signature(&r, &sig)?;
769183×
        if let AttestBody::Seal { hashes } = &attest.body {
77055×
            for h in hashes {
77155×
                self.store.index_seal(h, &r)?;
772
            }
773131×
        }
774183×
        Ok(SignedAttest {
775183×
            attest: attest.clone(),
776183×
            sigs: vec![sig],
777183×
        })
778185×
    }
779
}