OXYGENHUB

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

543 of 613 lines run, 66 of 98 functions · run #427 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
/// Make a user identity with no repository (oxygen#144), so a first
22
/// identity comes before any repository: its secret and a copy of it go to
23
/// this home's keys, and the user file names it as the author, as `oxy
24
/// identity create` does inside one. A repository reads it from the keys
25
/// when it first signs with it. `name` and `email` default to the user
26
/// file's author, then to `$USER` and `OXY_AUTHOR_*`.
272×
pub fn create_user_identity(
282×
    name: Option<&str>,
292×
    email: Option<&str>,
302×
    expires: Option<u64>,
312×
) -> Result<(KeyId, Identity)> {
322×
    let user = default_user_config();
332×
    let defaults = Config::load(Some(&user), Path::new(""))?;
342×
    let name = name.map(str::to_string).unwrap_or(defaults.author_name);
352×
    let email = email.map(str::to_string).unwrap_or(defaults.author_email);
362×
    let sk = SecretKey::generate();
372×
    let mut id = sk.identity(&name, &email, Hlc::now().wall_ms);
382×
    id.expires = expires;
392×
    let key = id.hash();
402×
    let keys = default_keys_dir();
412×
    fs::create_dir_all(&keys)?;
422×
    crate::durable::write_atomic_private(
432×
        &keys.join(format!("{}.secret", key.to_hex())),
442×
        &sk.encode(&key),
450×
    )?;
462×
    write_atomic(
472×
        &keys.join(format!("{}.identity", key.to_hex())),
482×
        &id.encode(),
490×
    )?;
502×
    Config::set_user(&user, "author.name", &id.name)?;
512×
    Config::set_user(&user, "author.email", &id.email)?;
522×
    Config::set_user(&user, "author.key", &key.to_hex())?;
532×
    Ok((key, id))
542×
}
55
56
/// The outcome of replacing the configured identity (spec §14.2).
57
#[derive(Clone, Debug)]
58
pub struct Rotation {
59
    pub old: KeyId,
60
    pub new: KeyId,
61
    pub identity: Identity,
62
    /// The policy update that moved the old key's grants to the new key,
63
    /// when the repository has a policy naming the old key.
64
    pub policy: Option<PolicyUpdate>,
65
}
66
67
impl Repo {
68
    // ------------------------------------------------------------ identity
69
70
    /// Generate a composite key pair, store its identity object, keep the
71
    /// secret in the key directory, and record the author at `scope`.
7274×
    pub fn create_identity(
7374×
        &mut self,
7474×
        name: &str,
7574×
        email: &str,
7674×
        scope: IdentityScope,
7774×
    ) -> Result<(KeyId, Identity)> {
7874×
        self.create_identity_with(name, email, scope, None)
7974×
    }
80
81
    /// `create_identity` with an expiry (milliseconds since the epoch).
8279×
    pub fn create_identity_with(
8379×
        &mut self,
8479×
        name: &str,
8579×
        email: &str,
8679×
        scope: IdentityScope,
8779×
        expires: Option<u64>,
8879×
    ) -> Result<(KeyId, Identity)> {
8979×
        let sk = SecretKey::generate();
9079×
        let mut id = sk.identity(name, email, Hlc::now().wall_ms);
9179×
        id.expires = expires;
9279×
        let key = self.install_key(&id, &sk)?;
9379×
        self.set_author(&id, &key, scope)?;
9479×
        Ok((key, id))
9579×
    }
96
97
    /// Store an identity and keep its secret (and a copy of the identity)
98
    /// in the key directory.
9999×
    fn install_key(&self, id: &Identity, sk: &SecretKey) -> Result<KeyId> {
10099×
        let key = self.store.put_identity(id)?;
10199×
        fs::create_dir_all(&self.keys_dir)?;
10299×
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
10399×
        crate::durable::write_atomic_private(&path, &sk.encode(&key))?;
10499×
        write_atomic(
10599×
            &self.keys_dir.join(format!("{}.identity", key.to_hex())),
10699×
            &id.encode(),
1070×
        )?;
10899×
        Ok(key)
10999×
    }
110
111101×
    fn set_author(&mut self, id: &Identity, key: &KeyId, scope: IdentityScope) -> Result<()> {
112101×
        match scope {
113
            IdentityScope::Repo => {
11438×
                self.config.set("author.name", &id.name)?;
11538×
                self.config.set("author.email", &id.email)?;
11638×
                self.config.set("author.key", &key.to_hex())?;
11738×
                self.save_config()?;
118
            }
119
            IdentityScope::User => {
12063×
                Config::set_user(&self.user_config, "author.name", &id.name)?;
12163×
                Config::set_user(&self.user_config, "author.email", &id.email)?;
12263×
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
12363×
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
124
            }
125
        }
126101×
        Ok(())
127101×
    }
128
129
    /// The scope that holds the configured author key.
1300×
    fn author_scope(&self) -> IdentityScope {
1310×
        if self.config.repo_layer().contains_key("author.key") {
1320×
            IdentityScope::Repo
133
        } else {
1340×
            IdentityScope::User
135
        }
1360×
    }
137
138
    /// Record a successor key everywhere the old one was named: the
139
    /// repository layer when it pins the old key, and the user file when
140
    /// it names the old key, so every checkout that followed the old key
141
    /// follows the new one (oxygen#69). A key named nowhere falls back to
142
    /// the scope that resolved it.
14320×
    fn record_successor(&mut self, id: &Identity, old: &KeyId, new_key: &KeyId) -> Result<()> {
14440×
        let names = |v: Option<&String>| v.and_then(|k| KeyId::from_hex(k)) == Some(*old);
14520×
        let repo_pins = names(self.config.repo_layer().get("author.key"));
14620×
        let user_entries = super::config::read_config_file(&self.user_config).unwrap_or_default();
14720×
        let user_names = names(user_entries.get("author.key"));
14820×
        if !repo_pins && !user_names {
1490×
            return self.set_author(id, new_key, self.author_scope());
15020×
        }
15120×
        if user_names {
1529×
            self.set_author(id, new_key, IdentityScope::User)?;
15311×
        }
15420×
        if repo_pins {
15513×
            self.set_author(id, new_key, IdentityScope::Repo)?;
1567×
        }
15720×
        Ok(())
15820×
    }
159
160
    /// Replace the configured identity by `new` signed with `sk`: store it,
161
    /// record a `rotate` attestation signed by the old key, move the old
162
    /// key's policy grants to the new key, and switch the author config.
16322×
    fn replace_identity(
16422×
        &mut self,
16522×
        new: Identity,
16622×
        sk: SecretKey,
16722×
        command: &str,
16822×
    ) -> Result<Rotation> {
16922×
        let Some((old, old_sk)) = self.secret_key()? else {
1700×
            return fail("no secret key: run `oxy identity create` first");
171
        };
17222×
        if new.previous != Some(old) {
1730×
            return fail("the successor identity must name the current key as previous");
17422×
        }
175
        // A key that already has a successor is not the identity's newest:
176
        // building another from it forks the identity into two branches
177
        // (oxygen#163), as a checkout whose stale `author.key` pin outlived
178
        // a rotation made elsewhere once did.
17922×
        if let Some(newer) = self.successors(&old)?.into_iter().next() {
1802×
            let newest = self.newest(&newer)?;
1812×
            let fix = if self.config.is_repo_set("author.key") {
1822×
                format!(
183
                    "This repository's config pins author.key to the older key: `oxy config author.key {}` here \
184
                     (or `oxy config --unset author.key` to follow the user config), then run it again.",
1852×
                    newest.to_hex()
186
                )
187
            } else {
1880×
                format!(
189
                    "The user config names the older key: `oxy config --user author.key {}`, then run it again.",
1900×
                    newest.to_hex()
191
                )
192
            };
1932×
            return fail(format!(
194
                "`{command}` would start from key {}, which is not your identity's newest ({} succeeds it, and {} is the newest). \
195
                 Another successor would fork the identity into two branches.\n{fix}",
1962×
                old.short(12),
1972×
                newer.short(12),
1982×
                newest.short(12),
199
            ));
20020×
        }
20120×
        let new_key = self.install_key(&new, &sk)?;
20220×
        let attest = Attest {
20320×
            repo: self.id()?,
20420×
            hlc: Hlc::now(),
20520×
            body: AttestBody::Rotate { new: new_key },
206
        };
20720×
        let r = self.store.put_attest(&attest)?;
20820×
        self.store
20920×
            .put_signature(&r, &old_sk.sign(Domain::Attest, &old, &r))?;
21020×
        let policy = match self.current_policy()? {
2119×
            Some((_, sp)) if sp.policy.mentions(&old) => {
2129×
                let mut p = self.next_policy()?;
2139×
                p.substitute_key(&old, new.clone());
2149×
                Some(self.sign_policy_with(p, &[(old, &old_sk), (new_key, &sk)], command)?)
215
            }
21611×
            _ => None,
217
        };
21820×
        self.record_successor(&new, &old, &new_key)?;
21920×
        Ok(Rotation {
22020×
            old,
22120×
            new: new_key,
22220×
            identity: new,
22320×
            policy,
22420×
        })
22522×
    }
226
227
    /// Keys in this home whose identity names `key` as `previous`: the
228
    /// successors already made from it. More than one means a fork.
22933×
    pub fn successors(&self, key: &KeyId) -> Result<Vec<KeyId>> {
23033×
        let mut found = Vec::new();
23133×
        let entries = match fs::read_dir(&self.keys_dir) {
23233×
            Ok(e) => e,
2330×
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(found),
2340×
            Err(e) => return Err(e.into()),
235
        };
236106×
        for entry in entries {
237106×
            let path = entry?.path();
238106×
            if path.extension().and_then(|e| e.to_str()) != Some("identity") {
23953×
                continue;
24053×
            }
24153×
            let Ok(bytes) = fs::read(&path) else { continue };
24253×
            let Ok(id) = Identity::decode(&bytes) else {
2430×
                continue;
244
            };
24553×
            if id.previous == Some(*key) {
2467×
                found.push(id.hash());
24746×
            }
248
        }
24933×
        found.sort();
25033×
        Ok(found)
25133×
    }
252
253
    /// The newest key of the line through `key` in this home: its
254
    /// successors followed to the end (the first, where there is a fork).
2554×
    pub fn newest(&self, key: &KeyId) -> Result<KeyId> {
2564×
        let mut cur = *key;
2574×
        for _ in 0..256 {
2587×
            match self.successors(&cur)?.into_iter().next() {
2593×
                Some(next) => cur = next,
2604×
                None => break,
261
            }
262
        }
2634×
        Ok(cur)
2644×
    }
265
266
    /// Rotate to a fresh key pair; the new identity names the old one as
267
    /// `previous` and keeps its name, email, delegates, and expiry.
2686×
    pub fn rotate_identity(&mut self) -> Result<Rotation> {
2696×
        let Some((old, _)) = self.secret_key()? else {
2700×
            return fail("no secret key: run `oxy identity create` first");
271
        };
2726×
        let cur = self.identity(&old)?;
2736×
        let sk = SecretKey::generate();
2746×
        let mut id = sk.identity(&cur.name, &cur.email, Hlc::now().wall_ms);
2756×
        id.previous = Some(old);
2766×
        id.delegates = cur.delegates.clone();
2776×
        id.expires = cur.expires;
2786×
        self.replace_identity(id, sk, "oxy identity rotate")
2796×
    }
280
281
    /// Re-issue the identity with the same keys and a new expiry.
2823×
    pub fn renew_identity(&mut self, expires: Option<u64>) -> Result<Rotation> {
2833×
        let Some((old, sk)) = self.secret_key()? else {
2840×
            return fail("no secret key: run `oxy identity create` first");
285
        };
2863×
        let cur = self.identity(&old)?;
2873×
        let id = cur.successor(cur.delegates.clone(), expires, Hlc::now().wall_ms);
2883×
        self.replace_identity(id, sk, "oxy identity renew")
2893×
    }
290
291
    /// Let `delegate` act on this identity's behalf within `scope` until
292
    /// `expires` (spec §14.2). Delegates are part of the identity object,
293
    /// so this re-issues it with the same keys.
29412×
    pub fn delegate_add(
29512×
        &mut self,
29612×
        delegate: Identity,
29712×
        scope: Scope,
29812×
        expires: Option<u64>,
29912×
    ) -> Result<Rotation> {
30012×
        self.delegate_add_many(vec![delegate], scope, expires)
30112×
    }
302
303
    /// As [`Repo::delegate_add`], for several delegates in one rotation.
30413×
    pub fn delegate_add_many(
30513×
        &mut self,
30613×
        new_delegates: Vec<Identity>,
30713×
        scope: Scope,
30813×
        expires: Option<u64>,
30913×
    ) -> Result<Rotation> {
31013×
        let Some((old, sk)) = self.secret_key()? else {
3110×
            return fail("no secret key: run `oxy identity create` first");
312
        };
31313×
        if scope.is_empty() {
3140×
            return fail("a delegate needs at least one scope: changes, refs or seal");
31513×
        }
31613×
        if new_delegates.is_empty() {
3170×
            return fail("name at least one identity file");
31813×
        }
31913×
        let cur = self.identity(&old)?;
32013×
        let mut delegates = cur.delegates.clone();
32113×
        let mut added = Vec::new();
32214×
        for delegate in new_delegates {
32314×
            let dk = self.store.put_identity(&delegate)?;
32414×
            added.push(dk);
32514×
            if dk == old {
3260×
                return fail("an identity cannot delegate to itself");
32714×
            }
32814×
            if cur.delegate(&dk).is_some()
32912×
                || delegates
33012×
                    .iter()
33112×
                    .skip(cur.delegates.len())
33212×
                    .any(|d| d.key == dk)
333
            {
3342×
                return fail(format!(
335
                    "{} is already a delegate; remove it first to change its scope or expiry",
3362×
                    dk.short(12)
337
                ));
33812×
            }
33912×
            delegates.push(Delegate {
34012×
                key: dk,
34112×
                scope,
34212×
                expires,
34312×
            });
344
        }
34511×
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
34611×
        self.replace_identity(
34711×
            id,
34811×
            sk,
34911×
            &format!(
350
                "oxy identity delegate add {} --scope {scope}",
35111×
                added
35211×
                    .iter()
35312×
                    .map(|k| k.short(12))
35411×
                    .collect::<Vec<_>>()
35511×
                    .join(" ")
356
            ),
357
        )
35813×
    }
359
360
    /// Withdraw a delegation; re-issues the identity with the same keys.
3613×
    pub fn delegate_remove(&mut self, delegate: &KeyId) -> Result<Rotation> {
3623×
        let Some((old, sk)) = self.secret_key()? else {
3630×
            return fail("no secret key: run `oxy identity create` first");
364
        };
3653×
        let cur = self.identity(&old)?;
3663×
        if cur.delegate(delegate).is_none() {
3671×
            return fail(format!("{} is not a delegate", delegate.short(12)));
3682×
        }
3692×
        let delegates = cur
3702×
            .delegates
3712×
            .iter()
3722×
            .copied()
3732×
            .filter(|d| d.key != *delegate)
3742×
            .collect();
3752×
        let id = cur.successor(delegates, cur.expires, Hlc::now().wall_ms);
3762×
        self.replace_identity(
3772×
            id,
3782×
            sk,
3792×
            &format!("oxy identity delegate remove {}", delegate.short(12)),
380
        )
3813×
    }
382
383
    /// Revoke `key` (your own, one you delegated to, or any key when you
384
    /// hold a policy root): record a signed `revoke` attestation and a
385
    /// local marker, and drop the key from the policy where it has one.
386
    /// Revoking a delegate does not re-issue the identity; `delegate_remove`
387
    /// does that, and the CLI does both.
3885×
    pub fn revoke_identity(&self, key: &KeyId, reason: &str) -> Result<Option<PolicyUpdate>> {
3895×
        let Some((own, sk)) = self.secret_key()? else {
3900×
            return fail("no secret key: run `oxy identity create` first");
391
        };
3925×
        let policy = self.current_policy()?.map(|(_, sp)| sp.policy);
3935×
        let is_root = policy.as_ref().is_some_and(|p| p.roots.contains(&own));
3945×
        let is_delegate = self.identity(&own)?.delegate(key).is_some();
3955×
        if *key != own && !is_root && !is_delegate {
3961×
            return fail("only the key's holder, the identity that delegated to it, or a policy root can revoke it");
3974×
        }
3984×
        let attest = Attest {
3994×
            repo: self.id()?,
4004×
            hlc: Hlc::now(),
4014×
            body: AttestBody::Revoke {
4024×
                key: *key,
4034×
                reason: reason.into(),
4044×
            },
405
        };
4064×
        let r = self.store.put_attest(&attest)?;
4074×
        self.store
4084×
            .put_signature(&r, &sk.sign(Domain::Attest, &own, &r))?;
4094×
        let dir = self.oxy.join("revoked");
4104×
        fs::create_dir_all(&dir)?;
4114×
        write_atomic(&dir.join(key.to_hex()), r.to_hex().as_bytes())?;
4121×
        match policy {
4131×
            Some(p) if p.mentions(key) => Ok(Some(self.policy_revoke(key)?)),
4143×
            _ => Ok(None),
415
        }
4165×
    }
417
418
    /// Whether `key` is revoked by the policy or by a recorded revocation.
419265×
    pub fn is_revoked(&self, key: &KeyId) -> Result<bool> {
420265×
        if self.oxy.join("revoked").join(key.to_hex()).exists() {
4215×
            return Ok(true);
422260×
        }
423260×
        Ok(self
424260×
            .current_policy()?
425260×
            .is_some_and(|(_, sp)| sp.policy.is_revoked(key)))
426265×
    }
427
428
    /// The key this repository signs with: `author.signer` when set (an
429
    /// agent's own key, acting for the author), else `author.key`.
4301049×
    pub fn signing_key(&self) -> Option<KeyId> {
4311049×
        if self.config.signer_keyless {
432
            // The key certified last, once this command has signed.
4331×
            return self.keyless_key.borrow().as_ref().map(|(k, _, _)| *k);
4341048×
        }
4351048×
        self.config.signer_key.or(self.config.author_key)
4361049×
    }
437
438
    /// The secret key for `author.signer`, else `author.key`, if configured
439
    /// and present on disk. The identity object is kept beside the secret
440
    /// so a repository that has never seen it (a fresh clone) can store it
441
    /// when it first signs.
442826×
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
443826×
        if self.config.signer_keyless {
4441×
            return self.keyless_secret().map(Some);
445825×
        }
446825×
        let Some(key) = self.signing_key() else {
447237×
            return Ok(None);
448
        };
449588×
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
450588×
        match fs::read(&path) {
451587×
            Ok(b) => {
452587×
                let (k, sk) = SecretKey::decode(&b)?;
453587×
                if k != key {
4540×
                    return fail(format!(
455
                        "{} holds a different key than its name",
4560×
                        path.display()
457
                    ));
458587×
                }
459587×
                if !self.store.has(&key) {
4604×
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
4614×
                    if let Ok(bytes) = fs::read(&id_path) {
4624×
                        let id = Identity::decode(&bytes)?;
4634×
                        if id.hash() == key {
4644×
                            self.store.put_identity(&id)?;
4650×
                        }
4660×
                    }
467583×
                }
468587×
                Ok(Some((k, sk)))
469
            }
4701×
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
4710×
            Err(e) => Err(e.into()),
472
        }
473826×
    }
474
475
    /// An identity: from this repository's store, or else from the key
476
    /// directory, where `oxy identity create` keeps the user's own (a new
477
    /// repository has not stored it yet). The file must hash to `key`.
478340×
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
479340×
        match self.store.get_identity(key) {
480336×
            Ok(id) => Ok(id),
4814×
            Err(e) => {
4824×
                let path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
4834×
                match fs::read(&path) {
4842×
                    Ok(bytes) => {
4852×
                        let id = Identity::decode(&bytes)?;
4862×
                        if id.hash() != *key {
4871×
                            return fail(format!(
488
                                "{} does not hold identity {}",
4891×
                                path.display(),
4901×
                                key.short(12)
491
                            ));
4921×
                        }
4931×
                        Ok(id)
494
                    }
495
                    // The configured author's identity is neither in the
496
                    // repository nor in this home's keys: say why and what
497
                    // to do (oxygen#82), rather than "object … not found".
4982×
                    Err(_) if self.config.author_key == Some(*key) => fail(format!(
499
                        "this repository has no identity {} (author.key, set in {}): it is not in the repository, nor in {}.\n\
500
                         An agent home names its human's identity, and each delegation rotates that identity, so the key goes stale. \
501
                         Pin the identity this repository uses, as its owner sees it: `oxy identity show` in the owner's shell gives the key, \
502
                         then `oxy config author.key <KEY>` here; or ask the owner.",
5032×
                        key.short(12),
5042×
                        if self.config.is_repo_set("author.key") {
5051×
                            "this repository's config"
506
                        } else {
5071×
                            "the user config"
508
                        },
5092×
                        self.keys_dir.display()
510
                    )),
5110×
                    Err(_) => Err(e.into()),
512
                }
513
            }
514
        }
515340×
    }
516
517
    /// Sign a stored change with the configured secret key.
51847×
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
51947×
        let Some((key, sk)) = self.secret_key()? else {
5200×
            return fail("no secret key: run `oxy identity create` first");
521
        };
52247×
        let sig = sk.sign(Domain::Change, &key, hash);
52347×
        self.store.put_signature(hash, &sig)?;
52447×
        Ok(sig)
52547×
    }
526
527
    /// Check the signatures on a change against the author-or-delegate rule.
52824×
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
52924×
        self.verify_change_known(hash, None)
53024×
    }
531
532
    /// `verify_change`, with the change's metadata when the caller has it
533
    /// (a change object can be as large as the tree it adds). Signatures
534
    /// are looked up first: an unsigned change needs no metadata.
535448×
    fn verify_change_known(
536448×
        &self,
537448×
        hash: &ChangeHash,
538448×
        known: Option<&crate::state::ChangeMeta>,
539448×
    ) -> Result<SignatureStatus> {
540448×
        let sigs = self.store.signatures(hash)?;
541448×
        if sigs.is_empty() {
542351×
            return Ok(SignatureStatus::Unsigned);
54397×
        }
544
        let read;
54597×
        let change = match known {
54618×
            Some(m) => m,
547
            None => {
54879×
                read = self.store.get_change_meta(hash)?;
54979×
                &read
550
            }
551
        };
55297×
        let mut last_err = String::new();
55397×
        for sig in &sigs {
55497×
            match self.check_signer(change, sig, Domain::Change, hash) {
55591×
                Ok(id) => {
556
                    return Ok(SignatureStatus::Valid {
55791×
                        key: sig.key,
55891×
                        name: self.certified_subject(&sig.key).unwrap_or(id.name),
55991×
                        email: id.email,
560
                        sealed: false,
56191×
                        revoked: self.is_revoked(&sig.key)?,
562
                    })
563
                }
5646×
                Err(e) => last_err = e,
565
            }
566
        }
5676×
        Ok(SignatureStatus::Invalid(last_err))
568448×
    }
569
570
    /// Whether `signer` is a short-lived key acting for `author` under a
571
    /// grant (spec §14.7): its certificate verifies with its issuer and
572
    /// carries a grant `author` signed, for the certificate's subject and
573
    /// issuer, giving `needed` at `at_ms` (the change's time, as for a
574
    /// delegation). Whether the certificate covered the time it signed is
575
    /// the policy's check, at a time the signer did not choose. `None`
576
    /// when it does; otherwise why not.
57714×
    fn granted(
57814×
        &self,
57914×
        author: &KeyId,
58014×
        signer: &KeyId,
58114×
        needed: Scope,
58214×
        at_ms: u64,
58314×
    ) -> std::result::Result<Option<String>, String> {
58414×
        let neither = || {
5857×
            Some(format!(
5867×
                "key {} is neither the author nor a delegate",
5877×
                signer.short(12)
5887×
            ))
5897×
        };
59014×
        let Ok(Some(c)) = self.store.certificate_of(signer) else {
5914×
            return Ok(neither());
592
        };
59310×
        let Ok(sc) = self.store.get_certificate(&c) else {
5940×
            return Ok(neither());
595
        };
59610×
        let Some(g) = &sc.cert.grant else {
5971×
            return Ok(neither());
598
        };
5999×
        let who = signer.short(12);
6009×
        let issuer = self
6019×
            .store
6029×
            .get_identity(&sc.cert.issuer)
6039×
            .map_err(|_| format!("key {who}'s certificate names an unknown issuer"))?;
6049×
        sc.verify(&issuer)
6059×
            .map_err(|e| format!("key {who}'s certificate: {e}"))?;
606
        // A grant by an earlier key of the author's identity counts while
607
        // it was made before that key was rotated away (oxygen#135).
6089×
        if g.grant.grantor != *author && !self.granted_before_rotation(author, &g.grant) {
6092×
            return Ok(neither());
6107×
        }
6117×
        let grantor = self
6127×
            .identity(&g.grant.grantor)
6137×
            .map_err(|_| format!("key {who}'s grant is by an unknown author"))?;
6147×
        g.verify(&grantor)
6157×
            .map_err(|e| format!("key {who}'s grant: {e}"))?;
6167×
        if g.grant.subject != sc.cert.subject || g.grant.issuer != sc.cert.issuer {
6170×
            return Err(format!(
6180×
                "key {who}'s grant is for {} from issuer {}, not its certificate's",
6190×
                g.grant.subject,
6200×
                g.grant.issuer.short(12)
6210×
            ));
6227×
        }
6237×
        if !sc.cert.scope.has(needed) || !g.grant.scope.has(needed) {
6240×
            return Err(format!("key {who} is not granted {needed}"));
6257×
        }
6267×
        if !g.grant.covers(needed, at_ms) {
6272×
            return Err(format!(
6282×
                "key {who}'s grant did not cover the time the change was made"
6292×
            ));
6305×
        }
6315×
        Ok(None)
63214×
    }
633
634
    /// Whether `grant` is by a key `author`'s identity was rotated from,
635
    /// made before that rotation (oxygen#135). An owner rotates with every
636
    /// delegation, and a repository may name the newest key as the author
637
    /// while the agent's grant is by an older one; the grant stays good, as
638
    /// a policy keeps a predecessor's signatures from before the rotation
639
    /// (spec §14.5), but a key rotated away grants nothing new.
6403×
    fn granted_before_rotation(&self, author: &KeyId, grant: &crate::cert::Grant) -> bool {
6413×
        let mut cur = self.identity(author).ok();
6423×
        for _ in 0..32 {
6433×
            let Some(id) = cur else {
6440×
                return false;
645
            };
6462×
            match id.previous {
6472×
                Some(prev) if prev == grant.grantor => return grant.not_before < id.created,
6480×
                Some(prev) => cur = self.identity(&prev).ok(),
6491×
                None => return false,
650
            }
651
        }
6520×
        false
6533×
    }
654
655
    /// The subject a short-lived key was certified for (spec §14.7), such
656
    /// as `agent:claude@andy`: what names it, since its identity is only
657
    /// a fresh key's.
658249×
    pub fn certified_subject(&self, key: &KeyId) -> Option<String> {
659249×
        let c = self.store.certificate_of(key).ok()??;
6609×
        Some(self.store.get_certificate(&c).ok()?.cert.subject)
661249×
    }
662
663
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
664262×
    fn check_signer(
665262×
        &self,
666262×
        change: &crate::state::ChangeMeta,
667262×
        sig: &Signature,
668262×
        domain: Domain,
669262×
        target: &ObjectRef,
670262×
    ) -> std::result::Result<Identity, String> {
671262×
        let id = self
672262×
            .store
673262×
            .get_identity(&sig.key)
674262×
            .map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
675
        // The author-or-delegate rule (spec §14.4). A change whose author is
676
        // the zero key names nobody (Git imports, history from before an
677
        // identity existed), so any key may adopt it with a seal; policy
678
        // then authorises that key for the reference as usual. A delegate
679
        // acts within its scope, signing changes or sealing them, and only
680
        // until its delegation expires (§14.2).
681262×
        if !change.author.is_zero() && sig.key != change.author {
68234×
            let needed = match domain {
68322×
                Domain::Change => Scope::CHANGES,
68412×
                _ => Scope::SEAL,
685
            };
68634×
            let delegation = self
68734×
                .store
68834×
                .get_identity(&change.author)
68934×
                .ok()
69034×
                .and_then(|a| a.delegate(&sig.key).copied());
69119×
            match delegation {
692
                // A certified key acting under the author's grant (§14.7).
693
                None => {
6947×
                    if let Some(why) =
69514×
                        self.granted(&change.author, &sig.key, needed, change.hlc.wall_ms)?
696
                    {
6977×
                        return Err(why);
6985×
                    }
699
                }
70020×
                Some(d) if !d.scope.has(needed) => {
7011×
                    return Err(format!(
702
                        "key {} is a delegate for {} only and may not {}",
7031×
                        sig.key.short(12),
704
                        d.scope,
7051×
                        if needed == Scope::SEAL {
7061×
                            "seal"
707
                        } else {
7080×
                            "sign changes"
709
                        }
710
                    ))
711
                }
71219×
                Some(d) if !d.allows(needed, change.hlc.wall_ms) => {
7131×
                    return Err(format!(
7141×
                        "key {}'s delegation had expired when the change was made",
7151×
                        sig.key.short(12)
7161×
                    ))
717
                }
71818×
                Some(_) => {}
719
            }
720228×
        }
721251×
        if id.is_expired_at(change.hlc.wall_ms) {
7221×
            return Err(format!(
7231×
                "key {} had expired when the change was made",
7241×
                sig.key.short(12)
7251×
            ));
726250×
        }
727250×
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
728249×
        Ok(id)
729262×
    }
730
731
    /// Like `verify_change`, but a change with no valid individual signature
732
    /// is also authenticated by a valid seal covering it (spec §14.4).
733344×
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
734344×
        self.authenticate_change_known(hash, None)
735344×
    }
736
737
    /// `authenticate_change`, with the change's metadata when the caller
738
    /// has it (from a log, say), so the change itself need not be read.
739424×
    pub fn authenticate_change_known(
740424×
        &self,
741424×
        hash: &ChangeHash,
742424×
        known: Option<&crate::state::ChangeMeta>,
743424×
    ) -> Result<SignatureStatus> {
744424×
        let direct = self.verify_change_known(hash, known)?;
745424×
        if matches!(direct, SignatureStatus::Valid { .. }) {
74676×
            return Ok(direct);
747348×
        }
748348×
        let mut last_err = match &direct {
7492×
            SignatureStatus::Invalid(e) => e.clone(),
750346×
            _ => String::new(),
751
        };
752348×
        let seals = self.store.seals_for(hash)?;
753348×
        if seals.is_empty() {
754183×
            return Ok(match last_err.is_empty() {
755181×
                true => SignatureStatus::Unsigned,
7562×
                false => SignatureStatus::Invalid(last_err),
757
            });
758165×
        }
759
        let read;
760165×
        let change = match known {
7610×
            Some(m) => m,
762
            None => {
763165×
                read = self.store.get_change_meta(hash)?;
764165×
                &read
765
            }
766
        };
767165×
        for seal_ref in seals {
768165×
            let attest = self.store.get_attest(&seal_ref)?;
769165×
            let AttestBody::Seal { hashes } = &attest.body else {
7700×
                continue;
771
            };
772165×
            if !hashes.contains(hash) {
7730×
                continue;
774165×
            }
775165×
            for sig in self.store.signatures(&seal_ref)? {
776165×
                match self.check_signer(change, &sig, Domain::Attest, &seal_ref) {
777158×
                    Ok(id) => {
778
                        return Ok(SignatureStatus::Valid {
779158×
                            key: sig.key,
780158×
                            name: self.certified_subject(&sig.key).unwrap_or(id.name),
781158×
                            email: id.email,
782
                            sealed: true,
783158×
                            revoked: self.is_revoked(&sig.key)?,
784
                        })
785
                    }
7867×
                    Err(e) => last_err = e,
787
                }
788
            }
789
        }
7907×
        if last_err.is_empty() {
7910×
            Ok(SignatureStatus::Unsigned)
792
        } else {
7937×
            Ok(SignatureStatus::Invalid(last_err))
794
        }
795424×
    }
796
797
    /// Store and sign an attestation with the configured key.
798
    /// Seal changes with the identity key (spec §14.4): a stored, indexed
799
    /// seal attestation over `hashes`, pushed with the next push. Returns
800
    /// the attestation's reference.
8013×
    pub fn seal(&self, hashes: &[ChangeHash]) -> Result<ObjectRef> {
8023×
        if hashes.is_empty() {
8030×
            return fail("nothing to seal");
8043×
        }
8053×
        if hashes.len() > crate::attest::MAX_SEAL_HASHES {
8060×
            return fail(format!(
807
                "a seal covers at most {} changes",
808
                crate::attest::MAX_SEAL_HASHES
809
            ));
8103×
        }
8113×
        for h in hashes {
8123×
            if !self.store.has(h) {
8130×
                return fail(format!("unknown change {}", h.short(12)));
8143×
            }
815
        }
8163×
        let mut sorted = hashes.to_vec();
8173×
        sorted.sort();
8183×
        sorted.dedup();
8193×
        let sa = self.sign_attest(&Attest::seal(self.id()?, sorted.clone()))?;
8203×
        let r = self.store.put_attest(&sa.attest)?;
8213×
        for sig in &sa.sigs {
8223×
            self.store.put_signature(&r, sig)?;
823
        }
8243×
        for h in &sorted {
8253×
            self.store.index_seal(h, &r)?;
826
        }
8273×
        Ok(r)
8283×
    }
829
830
    /// Whether `signer` seals changes by `author`: its own, anonymous ones
831
    /// (§14.4), and, when it signs for the configured author (a delegate,
832
    /// or a short-lived key under the author's grant, §14.7), the
833
    /// author's. A receiver still checks it may.
83482×
    pub(crate) fn seals_for_author(&self, author: &KeyId, signer: &KeyId) -> bool {
83582×
        author == signer
8368×
            || author.is_zero()
8377×
            || ((self.config.signer_key.is_some() || self.config.signer_keyless)
8385×
                && self.config.author_key == Some(*author))
83982×
    }
840
841
    /// The changes of the current bookmark this identity can authenticate
842
    /// but has not: its own unsigned ones, anonymous ones, and the
843
    /// author's when it signs for them.
8440×
    pub fn sealable(&self) -> Result<Vec<ChangeHash>> {
8450×
        let Some((key, _)) = self.secret_key()? else {
8460×
            return fail("no identity: run oxy identity create");
847
        };
8480×
        let heads = self.bookmark_heads()?;
8490×
        let state = self.load_state_history(&heads)?;
8500×
        let mut out = Vec::new();
8510×
        for (h, meta) in state.changes() {
8520×
            if self.seals_for_author(&meta.author, &key)
8530×
                && !matches!(self.authenticate_change(h)?, SignatureStatus::Valid { .. })
8540×
            {
8550×
                out.push(*h);
8560×
            }
857
        }
8580×
        out.sort();
8590×
        Ok(out)
8600×
    }
861
862186×
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
863186×
        let Some((key, sk)) = self.secret_key()? else {
8640×
            return fail("no secret key: run `oxy identity create` first");
865
        };
866186×
        let r = self.store.put_attest(attest)?;
867184×
        let sig = sk.sign(Domain::Attest, &key, &r);
868184×
        self.store.put_signature(&r, &sig)?;
869184×
        if let AttestBody::Seal { hashes } = &attest.body {
87055×
            for h in hashes {
87155×
                self.store.index_seal(h, &r)?;
872
            }
873132×
        }
874184×
        Ok(SignedAttest {
875184×
            attest: attest.clone(),
876184×
            sigs: vec![sig],
877184×
        })
878186×
    }
879
}