OXYGENHUB
# 728da0f92161 oxygen / crates / oxy-core / src / identity.rs
13.9 KiB · Rust · last changed Identity lifecycle: rotate, renew, delegates, revoke; expiry checks; policy roots, thresholds, proposals and oxy policy sign; policy chains served on refs and carried in push bundles (spec 0.6n) 1 month ago source blame history raw
1
//! Identities and composite Ed25519 + ML-DSA-65 signatures (spec §14.2, §14.3).
2
//!
3
//! Both halves sign the same message, which binds the composite `KeyId`, and
4
//! verification requires both. A signature with either half missing, wrong,
5
//! or made for a different KeyId is invalid.
6
7
use crate::cbor::{self, Value};
8
use crate::hash::{hash_object, KeyId, ObjectRef, ObjectType};
9
use crate::objects::{array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION};
10
use ed25519_dalek::ed25519::signature::Signer as _;
11
use ed25519_dalek::{Signature as EdSignature, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey};
12
use ml_dsa::{EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature, SigningKey as MlSigningKey, VerifyingKey as MlVerifyingKey};
13
use rand::RngCore;
14
15
pub const ED25519_PK_LEN: usize = 32;
16
pub const MLDSA65_PK_LEN: usize = 1952;
17
pub const ED25519_SIG_LEN: usize = 64;
18
pub const MLDSA65_SIG_LEN: usize = 3309;
19
/// `alg` value for Ed25519 + ML-DSA-65.
20
pub const ALG_COMPOSITE: u64 = 1;
21
22
#[derive(Clone, Debug, PartialEq, Eq)]
23
pub struct Identity {
24
    pub ed25519_pk: [u8; ED25519_PK_LEN],
25
    pub mldsa65_pk: Vec<u8>,
26
    pub name: String,
27
    pub email: String,
28
    pub created: u64,
29
    pub expires: Option<u64>,
30
    pub previous: Option<KeyId>,
31
    pub delegates: Vec<KeyId>,
32
}
33
34
impl Identity {
35
    pub fn to_value(&self) -> Value {
36
        Value::map([
37
            (0, Value::Uint(FORMAT_VERSION)),
38
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
39
            (2, Value::Bytes(self.mldsa65_pk.clone())),
40
            (3, Value::Text(self.name.clone())),
41
            (4, Value::Text(self.email.clone())),
42
            (5, Value::Uint(self.created)),
43
            (6, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
44
            (7, self.previous.map(|p| Value::Bytes(p.0.to_vec())).unwrap_or(Value::Null)),
45
            (8, Value::Array(self.delegates.iter().map(|d| Value::Bytes(d.0.to_vec())).collect())),
46
        ])
47
    }
48
    pub fn encode(&self) -> Vec<u8> {
49
        cbor::encode(&self.to_value())
50
    }
51
    pub fn hash(&self) -> KeyId {
52
        hash_object(ObjectType::Identity, &self.encode())
53
    }
54
    pub fn decode(b: &[u8]) -> Result<Identity, ObjectError> {
55
        let v = cbor::decode(b)?;
56
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?;
57
        check_version(m)?;
58
        let ed = bytes(field(m, 1, "ed25519_pk")?, "ed25519_pk")?;
59
        let ml = bytes(field(m, 2, "mldsa65_pk")?, "mldsa65_pk")?;
60
        if ed.len() != ED25519_PK_LEN || ml.len() != MLDSA65_PK_LEN {
61
            return Err(ObjectError::Schema("identity: wrong public key length".into()));
62
        }
63
        let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> { if v.is_null() { Ok(None) } else { uint(v, "expires").map(Some) } };
64
        let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> { if v.is_null() { Ok(None) } else { oref(v, "previous").map(Some) } };
65
        let mut delegates = array(field(m, 8, "delegates")?, "delegates")?.iter().map(|d| oref(d, "delegate")).collect::<Result<Vec<_>, _>>()?;
66
        delegates.sort();
67
        delegates.dedup();
68
        Ok(Identity {
69
            ed25519_pk: ed.try_into().unwrap(),
70
            mldsa65_pk: ml.to_vec(),
71
            name: text(field(m, 3, "name")?, "name")?.to_owned(),
72
            email: text(field(m, 4, "email")?, "email")?.to_owned(),
73
            created: uint(field(m, 5, "created")?, "created")?,
74
            expires: opt_u(field(m, 6, "expires")?)?,
75
            previous: opt_r(field(m, 7, "previous")?)?,
76
            delegates,
77
        })
78
    }
79
80
    /// Whether the identity is past its expiry at `now_ms`.
81
    pub fn is_expired_at(&self, now_ms: u64) -> bool {
82
        self.expires.is_some_and(|e| now_ms > e)
83
    }
84
85
    /// A successor identity with the same keys, `previous` set to this
86
    /// identity's KeyId, and the given delegates and expiry (spec §14.2).
87
    pub fn successor(&self, delegates: Vec<KeyId>, expires: Option<u64>, created: u64) -> Identity {
88
        let mut delegates = delegates;
89
        delegates.sort();
90
        delegates.dedup();
91
        Identity { previous: Some(self.hash()), delegates, expires, created, ..self.clone() }
92
    }
93
94
    fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> {
95
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey)
96
    }
97
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> {
98
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| VerifyError::BadKey)?;
99
        Ok(MlVerifyingKey::decode(&enc))
100
    }
101
}
102
103
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104
pub enum Domain {
105
    Change,
106
    Attest,
107
    Policy,
108
}
109
110
impl Domain {
111
    pub fn name(self) -> &'static str {
112
        match self {
113
            Domain::Change => "change",
114
            Domain::Attest => "attest",
115
            Domain::Policy => "policy",
116
        }
117
    }
118
}
119
120
/// `"oxy1 sig " || domain || 0x00 || KeyId || target` (spec §14.3).
121
pub fn signing_message(domain: Domain, key: &KeyId, target: &ObjectRef) -> Vec<u8> {
122
    let mut m = Vec::with_capacity(80);
123
    m.extend_from_slice(b"oxy1 sig ");
124
    m.extend_from_slice(domain.name().as_bytes());
125
    m.push(0);
126
    m.extend_from_slice(&key.0);
127
    m.extend_from_slice(&target.0);
128
    m
129
}
130
131
#[derive(Clone, PartialEq, Eq)]
132
pub struct Signature {
133
    pub key: KeyId,
134
    pub ed: [u8; ED25519_SIG_LEN],
135
    pub ml: Vec<u8>,
136
}
137
138
impl std::fmt::Debug for Signature {
139
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140
        write!(f, "Signature(key {:?})", self.key)
141
    }
142
}
143
144
impl Signature {
145
    pub fn to_value(&self) -> Value {
146
        Value::map([
147
            (0, Value::Bytes(self.key.0.to_vec())),
148
            (1, Value::Uint(ALG_COMPOSITE)),
149
            (2, Value::Bytes(self.ed.to_vec())),
150
            (3, Value::Bytes(self.ml.clone())),
151
        ])
152
    }
153
    pub fn from_value(v: &Value) -> Result<Signature, ObjectError> {
154
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?;
155
        let alg = uint(field(m, 1, "alg")?, "alg")?;
156
        if alg != ALG_COMPOSITE {
157
            return Err(ObjectError::Schema(format!("unknown signature algorithm {alg}")));
158
        }
159
        let ed = bytes(field(m, 2, "ed")?, "ed")?;
160
        let ml = bytes(field(m, 3, "ml")?, "ml")?;
161
        if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN {
162
            return Err(ObjectError::Schema("signature: wrong length".into()));
163
        }
164
        Ok(Signature { key: oref(field(m, 0, "key")?, "key")?, ed: ed.try_into().unwrap(), ml: ml.to_vec() })
165
    }
166
    pub fn encode_list(sigs: &[Signature]) -> Vec<u8> {
167
        cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect()))
168
    }
169
    pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> {
170
        let v = cbor::decode(b)?;
171
        array(&v, "signatures")?.iter().map(Signature::from_value).collect()
172
    }
173
}
174
175
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
176
pub enum VerifyError {
177
    #[error("signature names a different key than the identity")]
178
    KeyMismatch,
179
    #[error("identity holds a malformed public key")]
180
    BadKey,
181
    #[error("Ed25519 signature invalid")]
182
    Ed25519,
183
    #[error("ML-DSA-65 signature invalid")]
184
    MlDsa,
185
}
186
187
/// Verify a composite signature by `identity` over `target` (spec §14.3).
188
pub fn verify(identity: &Identity, sig: &Signature, domain: Domain, target: &ObjectRef) -> Result<(), VerifyError> {
189
    let key = identity.hash();
190
    if sig.key != key {
191
        return Err(VerifyError::KeyMismatch);
192
    }
193
    let msg = signing_message(domain, &key, target);
194
    let ed_sig = EdSignature::from_bytes(&sig.ed);
195
    identity.ed_key()?.verify_strict(&msg, &ed_sig).map_err(|_| VerifyError::Ed25519)?;
196
    let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?;
197
    let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?;
198
    if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) {
199
        return Err(VerifyError::MlDsa);
200
    }
201
    Ok(())
202
}
203
204
/// The seeds of a composite key pair. Stored only in the user's key directory.
205
#[derive(Clone)]
206
pub struct SecretKey {
207
    pub ed_seed: [u8; 32],
208
    pub ml_seed: [u8; 32],
209
}
210
211
impl std::fmt::Debug for SecretKey {
212
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213
        f.write_str("SecretKey(..)")
214
    }
215
}
216
217
impl SecretKey {
218
    pub fn generate() -> SecretKey {
219
        let mut ed_seed = [0u8; 32];
220
        let mut ml_seed = [0u8; 32];
221
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
222
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
223
        SecretKey { ed_seed, ml_seed }
224
    }
225
226
    fn ed(&self) -> EdSigningKey {
227
        EdSigningKey::from_bytes(&self.ed_seed)
228
    }
229
    fn ml(&self) -> MlSigningKey<MlDsa65> {
230
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
231
    }
232
233
    /// The identity object for these keys.
234
    pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity {
235
        Identity {
236
            ed25519_pk: self.ed().verifying_key().to_bytes(),
237
            mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(),
238
            name: name.into(),
239
            email: email.into(),
240
            created,
241
            expires: None,
242
            previous: None,
243
            delegates: vec![],
244
        }
245
    }
246
247
    pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature {
248
        let msg = signing_message(domain, key, target);
249
        let ed = self.ed().sign(&msg).to_bytes();
250
        let ml = self.ml().expanded_key().sign_deterministic(&msg, b"").expect("empty context is valid").encode().as_slice().to_vec();
251
        Signature { key: *key, ed, ml }
252
    }
253
254
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
255
        cbor::encode(&Value::map([
256
            (0, Value::Uint(FORMAT_VERSION)),
257
            (1, Value::Bytes(key.0.to_vec())),
258
            (2, Value::Bytes(self.ed_seed.to_vec())),
259
            (3, Value::Bytes(self.ml_seed.to_vec())),
260
        ]))
261
    }
262
263
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> {
264
        let v = cbor::decode(b)?;
265
        let m = v.as_map().ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?;
266
        check_version(m)?;
267
        let seed = |k: u64| -> Result<[u8; 32], ObjectError> {
268
            bytes(field(m, k, "seed")?, "seed")?.try_into().map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into()))
269
        };
270
        Ok((oref(field(m, 1, "key")?, "key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? }))
271
    }
272
}
273
274
#[cfg(test)]
275
mod tests {
276
    use super::*;
277
278
    fn fixed_key() -> SecretKey {
279
        SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] }
280
    }
281
282
    #[test]
283
    fn identity_round_trip_and_sizes() {
284
        let sk = fixed_key();
285
        let id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
286
        assert_eq!(id.mldsa65_pk.len(), MLDSA65_PK_LEN);
287
        let d = Identity::decode(&id.encode()).unwrap();
288
        assert_eq!(d, id);
289
        assert_eq!(d.hash(), id.hash());
290
        // Deterministic from seeds.
291
        assert_eq!(fixed_key().identity("Ada", "[email protected]", 1_700_000_000_000).hash(), id.hash());
292
        let mut other = id.clone();
293
        other.delegates.push(ObjectRef([9; 32]));
294
        assert_ne!(other.hash(), id.hash());
295
    }
296
297
    #[test]
298
    fn sign_and_verify() {
299
        let sk = fixed_key();
300
        let id = sk.identity("Ada", "[email protected]", 1);
301
        let key = id.hash();
302
        let target = ObjectRef([7; 32]);
303
        let sig = sk.sign(Domain::Change, &key, &target);
304
        assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN);
305
        assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(()));
306
        // Different target, different domain: invalid.
307
        assert_eq!(verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])), Err(VerifyError::Ed25519));
308
        assert_eq!(verify(&id, &sig, Domain::Attest, &target), Err(VerifyError::Ed25519));
309
        // Deterministic signatures.
310
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
311
        // Signature list round trip.
312
        let l = Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
313
        assert_eq!(l, vec![sig]);
314
    }
315
316
    #[test]
317
    fn stripping_or_forging_either_half_fails() {
318
        let sk = fixed_key();
319
        let id = sk.identity("Ada", "a@b", 1);
320
        let key = id.hash();
321
        let target = ObjectRef([7; 32]);
322
        let good = sk.sign(Domain::Change, &key, &target);
323
324
        let mut no_ml = good.clone();
325
        no_ml.ml = vec![0; MLDSA65_SIG_LEN];
326
        assert_eq!(verify(&id, &no_ml, Domain::Change, &target), Err(VerifyError::MlDsa));
327
328
        let mut bad_ed = good.clone();
329
        bad_ed.ed[10] ^= 1;
330
        assert_eq!(verify(&id, &bad_ed, Domain::Change, &target), Err(VerifyError::Ed25519));
331
332
        // A signature made for another KeyId over the same target does not transfer.
333
        let other = SecretKey { ed_seed: [3; 32], ml_seed: [4; 32] };
334
        let other_id = other.identity("Bob", "b@b", 1);
335
        let theirs = other.sign(Domain::Change, &other_id.hash(), &target);
336
        assert_eq!(verify(&id, &theirs, Domain::Change, &target), Err(VerifyError::KeyMismatch));
337
        let mut relabelled = theirs.clone();
338
        relabelled.key = key;
339
        assert_eq!(verify(&id, &relabelled, Domain::Change, &target), Err(VerifyError::Ed25519));
340
341
        // Wrong length is rejected at decode time.
342
        let mut short = good.to_value();
343
        if let Value::Map(m) = &mut short {
344
            m.insert(3, Value::Bytes(vec![0; 10]));
345
        }
346
        assert!(Signature::from_value(&short).is_err());
347
    }
348
349
    #[test]
350
    fn secret_key_round_trip() {
351
        let sk = fixed_key();
352
        let key = sk.identity("x", "y", 0).hash();
353
        let (k, d) = SecretKey::decode(&sk.encode(&key)).unwrap();
354
        assert_eq!(k, key);
355
        assert_eq!(d.ed_seed, sk.ed_seed);
356
        assert_eq!(d.ml_seed, sk.ml_seed);
357
        assert_ne!(SecretKey::generate().ed_seed, SecretKey::generate().ed_seed);
358
    }
359
}