OXYGENHUB
# 79d03f826ecd oxygen / crates / oxy-core / src / repo / identity.rs
7.1 KiB · Rust · last changed Split repo.rs into repo/{config,snapshot,checkout,identity,sync,tests}; hermetic OXY_HOME in tests; identity recorded in user config 1 month ago source blame history raw
1
//! Identities, signing, and signature verification (spec §14).
2
3
use super::*;
4
5
/// Outcome of verifying the signatures on a change (spec §14.3, §14.4).
6
#[derive(Clone, Debug, PartialEq, Eq)]
7
pub enum SignatureStatus {
8
    Unsigned,
9
    Valid { key: KeyId, name: String, email: String, sealed: bool },
10
    Invalid(String),
11
}
12
13
impl Repo {
14
    // ------------------------------------------------------------ identity
15
16
    /// Generate a composite key pair, store its identity object, keep the
17
    /// secret in the key directory, and record the author at `scope`.
18
    pub fn create_identity(&mut self, name: &str, email: &str, scope: IdentityScope) -> Result<(KeyId, Identity)> {
19
        let sk = SecretKey::generate();
20
        let id = sk.identity(name, email, Hlc::now().wall_ms);
21
        let key = self.store.put_identity(&id)?;
22
        fs::create_dir_all(&self.keys_dir)?;
23
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
24
        write_atomic(&path, &sk.encode(&key))?;
25
        write_atomic(&self.keys_dir.join(format!("{}.identity", key.to_hex())), &id.encode())?;
26
        #[cfg(unix)]
27
        {
28
            use std::os::unix::fs::PermissionsExt;
29
            fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
30
        }
31
        match scope {
32
            IdentityScope::Repo => {
33
                self.config.set("author.name", name)?;
34
                self.config.set("author.email", email)?;
35
                self.config.set("author.key", &key.to_hex())?;
36
                self.save_config()?;
37
            }
38
            IdentityScope::User => {
39
                Config::set_user(&self.user_config, "author.name", name)?;
40
                Config::set_user(&self.user_config, "author.email", email)?;
41
                Config::set_user(&self.user_config, "author.key", &key.to_hex())?;
42
                self.config = Config::load(Some(&self.user_config), &self.oxy.join("config"))?;
43
            }
44
        }
45
        Ok((key, id))
46
    }
47
48
    /// The secret key for `author.key`, if configured and present on disk.
49
    /// The identity object is kept beside the secret so a repository that has
50
    /// never seen it (a fresh clone) can store it when it first signs.
51
    pub fn secret_key(&self) -> Result<Option<(KeyId, SecretKey)>> {
52
        let Some(key) = self.config.author_key else { return Ok(None) };
53
        let path = self.keys_dir.join(format!("{}.secret", key.to_hex()));
54
        match fs::read(&path) {
55
            Ok(b) => {
56
                let (k, sk) = SecretKey::decode(&b)?;
57
                if k != key {
58
                    return fail(format!("{} holds a different key than its name", path.display()));
59
                }
60
                if !self.store.has(&key) {
61
                    let id_path = self.keys_dir.join(format!("{}.identity", key.to_hex()));
62
                    if let Ok(bytes) = fs::read(&id_path) {
63
                        let id = Identity::decode(&bytes)?;
64
                        if id.hash() == key {
65
                            self.store.put_identity(&id)?;
66
                        }
67
                    }
68
                }
69
                Ok(Some((k, sk)))
70
            }
71
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
72
            Err(e) => Err(e.into()),
73
        }
74
    }
75
76
    pub fn identity(&self, key: &KeyId) -> Result<Identity> {
77
        Ok(self.store.get_identity(key)?)
78
    }
79
80
    /// Sign a stored change with the configured secret key.
81
    pub fn sign_change(&self, hash: &ChangeHash) -> Result<Signature> {
82
        let Some((key, sk)) = self.secret_key()? else {
83
            return fail("no secret key: run `oxy identity create` first");
84
        };
85
        let sig = sk.sign(Domain::Change, &key, hash);
86
        self.store.put_signature(hash, &sig)?;
87
        Ok(sig)
88
    }
89
90
    /// Check the signatures on a change against the author-or-delegate rule.
91
    pub fn verify_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
92
        let change = self.store.get_change(hash)?;
93
        let sigs = self.store.signatures(hash)?;
94
        if sigs.is_empty() {
95
            return Ok(SignatureStatus::Unsigned);
96
        }
97
        let mut last_err = String::new();
98
        for sig in &sigs {
99
            match self.check_signer(&change, sig, Domain::Change, hash) {
100
                Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: false }),
101
                Err(e) => last_err = e,
102
            }
103
        }
104
        Ok(SignatureStatus::Invalid(last_err))
105
    }
106
107
    /// Verify `sig` over `target` and check the author-or-delegate rule for `change`.
108
    fn check_signer(&self, change: &Change, sig: &Signature, domain: Domain, target: &ObjectRef) -> std::result::Result<Identity, String> {
109
        let id = self.store.get_identity(&sig.key).map_err(|_| format!("unknown key {}", sig.key.short(12)))?;
110
        let authorised =
111
            sig.key == change.author || self.store.get_identity(&change.author).map(|a| a.delegates.contains(&sig.key)).unwrap_or(false);
112
        if !authorised {
113
            return Err(format!("key {} is neither the author nor a delegate", sig.key.short(12)));
114
        }
115
        verify_signature(&id, sig, domain, target).map_err(|e| e.to_string())?;
116
        Ok(id)
117
    }
118
119
    /// Like `verify_change`, but a change with no valid individual signature
120
    /// is also authenticated by a valid seal covering it (spec §14.4).
121
    pub fn authenticate_change(&self, hash: &ChangeHash) -> Result<SignatureStatus> {
122
        let direct = self.verify_change(hash)?;
123
        if matches!(direct, SignatureStatus::Valid { .. }) {
124
            return Ok(direct);
125
        }
126
        let change = self.store.get_change(hash)?;
127
        let mut last_err = match &direct {
128
            SignatureStatus::Invalid(e) => e.clone(),
129
            _ => String::new(),
130
        };
131
        for seal_ref in self.store.seals_for(hash)? {
132
            let attest = self.store.get_attest(&seal_ref)?;
133
            let AttestBody::Seal { hashes } = &attest.body else { continue };
134
            if !hashes.contains(hash) {
135
                continue;
136
            }
137
            for sig in self.store.signatures(&seal_ref)? {
138
                match self.check_signer(&change, &sig, Domain::Attest, &seal_ref) {
139
                    Ok(id) => return Ok(SignatureStatus::Valid { key: sig.key, name: id.name, email: id.email, sealed: true }),
140
                    Err(e) => last_err = e,
141
                }
142
            }
143
        }
144
        if last_err.is_empty() { Ok(SignatureStatus::Unsigned) } else { Ok(SignatureStatus::Invalid(last_err)) }
145
    }
146
147
    /// Store and sign an attestation with the configured key.
148
    pub(crate) fn sign_attest(&self, attest: &Attest) -> Result<SignedAttest> {
149
        let Some((key, sk)) = self.secret_key()? else {
150
            return fail("no secret key: run `oxy identity create` first");
151
        };
152
        let r = self.store.put_attest(attest)?;
153
        let sig = sk.sign(Domain::Attest, &key, &r);
154
        self.store.put_signature(&r, &sig)?;
155
        if let AttestBody::Seal { hashes } = &attest.body {
156
            for h in hashes {
157
                self.store.index_seal(h, &r)?;
158
            }
159
        }
160
        Ok(SignedAttest { attest: attest.clone(), sigs: vec![sig] })
161
    }
162
}