//! Identities, signing, and signature verification (spec §14). use super::*; /// Outcome of verifying the signatures on a change (spec §14.3, §14.4). #[derive(Clone, Debug, PartialEq, Eq)] pub enum SignatureStatus { Unsigned, Valid { key: KeyId, name: String, email: String, sealed: bool }, Invalid(String), } impl Repo { // ------------------------------------------------------------ identity /// Generate a composite key pair, store its identity object, keep the /// secret in the key directory, and record the author at `scope`. pub fn create_identity(&mut self, name: &str, email: &str, scope: IdentityScope) -> Result<(KeyId, Identity)> { let sk = SecretKey::generate(); let id = sk.identity(name, email, Hlc::now().wall_ms); let key = self.store.put_identity(&id)?; fs::create_dir_all(&self.keys_dir)?; let path = self.keys_dir.join(format!("{}.secret", key.to_hex())); write_atomic(&path, &sk.encode(&key))?; write_atomic(&self.keys_dir.join(format!("{}.identity", key.to_hex())), &id.encode())?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; } match scope { IdentityScope::Repo => { self.config.set("author.name", name)?; self.config.set("author.email", email)?; self.config.set("author.key", &key.to_hex())?; self.save_config()?; } IdentityScope::User => { Config::set_user(&self.user_config, "author.name", name)?; Config::set_user(&self.user_config, "author.email", email)?; Config::set_user(&self.user_config, "author.key", &key.to_hex())?; self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?; } } Ok((key, id)) } /// The secret key for `author.key`, if configured and present on disk. /// The identity object is kept beside the secret so a repository that has /// never seen it (a fresh clone) can store it when it first signs. pub fn secret_key(&self) -> Result> { let Some(key) = self.config.author_key else { return Ok(None) }; let path = self.keys_dir.join(format!("{}.secret", key.to_hex())); match fs::read(&path) { Ok(b) => { let (k, sk) = SecretKey::decode(&b)?; if k != key { return fail(format!("{} holds a different key than its name", path.display())); } if !self.store.has(&key) { let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex())); if let Ok(bytes) = fs::read(&id_path) { let id = Identity::decode(&bytes)?; if id.hash() == key { self.store.put_identity(&id)?; } } } Ok(Some((k, sk))) } Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } pub fn identity(&self, key: &KeyId) -> Result { Ok(self.store.get_identity(key)?) } /// Sign a stored change with the configured secret key. pub fn sign_change(&self, hash: &ChangeHash) -> Result { let Some((key, sk)) = self.secret_key()? else { return fail("no secret key: run `oxy identity create` first"); }; let sig = sk.sign(Domain::Change, &key, hash); self.store.put_signature(hash, &sig)?; Ok(sig) } /// Check the signatures on a change against the author-or-delegate rule. pub fn verify_change(&self, hash: &ChangeHash) -> Result { let change = self.store.get_change(hash)?; let sigs = self.store.signatures(hash)?; if sigs.is_empty() { return Ok(SignatureStatus::Unsigned); } let mut last_err = String::new(); for sig in &sigs { match self.check_signer(&change, sig, Domain::Change, hash) { Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: false }), Err(e) => last_err = e, } } Ok(SignatureStatus::Invalid(last_err)) } /// Verify `sig` over `target` and check the author-or-delegate rule for `change`. fn check_signer(&self, change: &Change, sig: &Signature, domain: Domain, target: &ObjectRef) -> std::result::Result { let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?; let authorised = sig.key == change.author || self.store.get_identity(&change.author).map(|a| a.delegates.contains(&sig.key)).unwrap_or(false); if !authorised { return Err(format!("key {} is neither the author nor a delegate", sig.key.short(12))); } verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?; Ok(id) } /// Like `verify_change`, but a change with no valid individual signature /// is also authenticated by a valid seal covering it (spec §14.4). pub fn authenticate_change(&self, hash: &ChangeHash) -> Result { let direct = self.verify_change(hash)?; if matches!(direct, SignatureStatus::Valid { .. }) { return Ok(direct); } let change = self.store.get_change(hash)?; let mut last_err = match &direct { SignatureStatus::Invalid(e) => e.clone(), _ => String::new(), }; for seal_ref in self.store.seals_for(hash)? { let attest = self.store.get_attest(&seal_ref)?; let AttestBody::Seal { hashes } = &attest.body else { continue }; if !hashes.contains(hash) { continue; } for sig in self.store.signatures(&seal_ref)? { match self.check_signer(&change, &sig, Domain::Attest, &seal_ref) { Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: true }), Err(e) => last_err = e, } } } if last_err.is_empty() { Ok(SignatureStatus::Unsigned) } else { Ok(SignatureStatus::Invalid(last_err)) } } /// Store and sign an attestation with the configured key. pub(crate) fn sign_attest(&self, attest: &Attest) -> Result { let Some((key, sk)) = self.secret_key()? else { return fail("no secret key: run `oxy identity create` first"); }; let r = self.store.put_attest(attest)?; let sig = sk.sign(Domain::Attest, &key, &r); self.store.put_signature(&r, &sig)?; if let AttestBody::Seal { hashes } = &attest.body { for h in hashes { self.store.index_seal(h, &r)?; } } Ok(SignedAttest { attest: attest.clone(), sigs: vec![sig] }) } }