OXYGENHUB
◆ main spectra / crates / spectra-core / src / identity.rs
13.7 KiB · Rust · last changed Spectra stands alone: its own identities, signatures and CBOR; projects of any kind of work (#26, #27) 1 day ago source blame history raw
1
//! Spectra's identities and composite signatures (spec §8.0).
2
//!
3
//! An identity is a composite key pair, Ed25519 and ML-DSA-65, with a name,
4
//! the identity it succeeds and the keys it delegates to. Both halves sign
5
//! the same target and both must verify, so a signature stays sound while
6
//! either algorithm does. The design follows the lessons of Oxygen's
7
//! identities; the code, encoding and files are Spectra's own.
8
9
use crate::cbor::{self, Value};
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::{
13
    EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature, SigningKey as MlSigningKey,
14
    VerifyingKey as MlVerifyingKey,
15
};
16
use rand::RngCore;
17
18
const VERSION: u64 = 1;
19
20
/// An identity's name: `BLAKE3("spectra1 identity " || encoding)`.
21
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22
pub struct KeyId(pub [u8; 32]);
23
24
impl KeyId {
25
    pub fn to_hex(&self) -> String {
26
        hex::encode(self.0)
27
    }
28
    pub fn from_hex(s: &str) -> Option<KeyId> {
29
        hex::decode(s.trim()).ok()?.try_into().ok().map(KeyId)
30
    }
31
    /// The first `n` hex digits, for display.
32
    pub fn short(&self, n: usize) -> String {
33
        let h = self.to_hex();
34
        h[..n.min(h.len())].to_string()
35
    }
36
}
37
38
impl std::fmt::Debug for KeyId {
39
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40
        write!(f, "KeyId({})", self.short(12))
41
    }
42
}
43
44
impl std::fmt::Display for KeyId {
45
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46
        f.write_str(&self.to_hex())
47
    }
48
}
49
50
/// A key an identity lets act for it, until `expires` if set (ms).
51
#[derive(Clone, Debug, PartialEq, Eq)]
52
pub struct Delegate {
53
    pub key: KeyId,
54
    pub expires: Option<u64>,
55
}
56
57
impl Delegate {
58
    pub fn allows(&self, at_ms: u64) -> bool {
59
        self.expires.is_none_or(|e| at_ms < e)
60
    }
61
}
62
63
#[derive(Clone, Debug, PartialEq, Eq)]
64
pub struct Identity {
65
    pub ed25519_pk: [u8; 32],
66
    pub mldsa65_pk: Vec<u8>,
67
    pub name: String,
68
    pub contact: String,
69
    pub created: u64,
70
    pub expires: Option<u64>,
71
    pub previous: Option<KeyId>,
72
    pub delegates: Vec<Delegate>,
73
}
74
75
/// Why an identity, a secret or a signature could not be read or checked.
76
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
77
pub enum Error {
78
    #[error(transparent)]
79
    Cbor(#[from] cbor::Error),
80
    #[error("{0}")]
81
    Shape(String),
82
    #[error("the signature is by another key")]
83
    KeyMismatch,
84
    #[error("a public key that does not decode")]
85
    BadKey,
86
    #[error("the Ed25519 half does not verify")]
87
    Ed25519,
88
    #[error("the ML-DSA-65 half does not verify")]
89
    MlDsa,
90
}
91
92
fn shape(s: &str) -> Error {
93
    Error::Shape(s.into())
94
}
95
96
fn bytes_of(v: Option<&Value>, what: &str) -> Result<Vec<u8>, Error> {
97
    v.and_then(Value::as_bytes).map(|b| b.to_vec()).ok_or_else(|| shape(what))
98
}
99
100
fn key_of(v: Option<&Value>, what: &str) -> Result<KeyId, Error> {
101
    bytes_of(v, what)?.try_into().map(KeyId).map_err(|_| shape(what))
102
}
103
104
fn opt_uint(v: Option<&Value>, what: &str) -> Result<Option<u64>, Error> {
105
    match v {
106
        None | Some(Value::Null) => Ok(None),
107
        Some(Value::Uint(u)) => Ok(Some(*u)),
108
        _ => Err(shape(what)),
109
    }
110
}
111
112
fn opt_val<T>(o: Option<T>, f: impl Fn(T) -> Value) -> Value {
113
    o.map(f).unwrap_or(Value::Null)
114
}
115
116
impl Identity {
117
    pub fn to_value(&self) -> Value {
118
        Value::map([
119
            (0, Value::Uint(VERSION)),
120
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
121
            (2, Value::Bytes(self.mldsa65_pk.clone())),
122
            (3, Value::Text(self.name.clone())),
123
            (4, Value::Text(self.contact.clone())),
124
            (5, Value::Uint(self.created)),
125
            (6, opt_val(self.expires, Value::Uint)),
126
            (7, opt_val(self.previous, |k| Value::Bytes(k.0.to_vec()))),
127
            (
128
                8,
129
                Value::Array(
130
                    self.delegates
131
                        .iter()
132
                        .map(|d| Value::map([(0, Value::Bytes(d.key.0.to_vec())), (1, opt_val(d.expires, Value::Uint))]))
133
                        .collect(),
134
                ),
135
            ),
136
        ])
137
    }
138
139
    pub fn encode(&self) -> Vec<u8> {
140
        cbor::encode(&self.to_value())
141
    }
142
143
    pub fn hash(&self) -> KeyId {
144
        let mut h = blake3::Hasher::new();
145
        h.update(b"spectra1 identity ");
146
        h.update(&self.encode());
147
        KeyId(*h.finalize().as_bytes())
148
    }
149
150
    pub fn decode(b: &[u8]) -> Result<Identity, Error> {
151
        let v = cbor::decode(b)?;
152
        let m = v.as_map().ok_or_else(|| shape("an identity is a map"))?;
153
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
154
            return Err(shape("an identity of an unknown version"));
155
        }
156
        let text = |k: u64, what: &str| m.get(&k).and_then(Value::as_text).map(String::from).ok_or_else(|| shape(what));
157
        let delegates = m
158
            .get(&8)
159
            .and_then(Value::as_array)
160
            .ok_or_else(|| shape("delegates"))?
161
            .iter()
162
            .map(|d| {
163
                let dm = d.as_map().ok_or_else(|| shape("a delegate is a map"))?;
164
                Ok(Delegate { key: key_of(dm.get(&0), "a delegate's key")?, expires: opt_uint(dm.get(&1), "a delegate's expiry")? })
165
            })
166
            .collect::<Result<Vec<_>, Error>>()?;
167
        let id = Identity {
168
            ed25519_pk: bytes_of(m.get(&1), "the Ed25519 key")?.try_into().map_err(|_| shape("the Ed25519 key"))?,
169
            mldsa65_pk: bytes_of(m.get(&2), "the ML-DSA-65 key")?,
170
            name: text(3, "the name")?,
171
            contact: text(4, "the contact")?,
172
            created: m.get(&5).and_then(Value::as_uint).ok_or_else(|| shape("the creation time"))?,
173
            expires: opt_uint(m.get(&6), "the expiry")?,
174
            previous: match m.get(&7) {
175
                None | Some(Value::Null) => None,
176
                v => Some(key_of(v, "the previous identity")?),
177
            },
178
            delegates,
179
        };
180
        // One identity, one encoding: what was read must be what we write.
181
        if id.encode() != b {
182
            return Err(shape("an identity not in Spectra's own encoding"));
183
        }
184
        Ok(id)
185
    }
186
187
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
188
        self.delegates.iter().find(|d| &d.key == key)
189
    }
190
191
    /// Whether this identity lets `key` act for it at `at_ms`.
192
    pub fn delegates_to(&self, key: &KeyId, at_ms: u64) -> bool {
193
        !self.is_expired_at(at_ms) && self.delegate(key).is_some_and(|d| d.allows(at_ms))
194
    }
195
196
    pub fn is_expired_at(&self, at_ms: u64) -> bool {
197
        self.expires.is_some_and(|e| at_ms >= e)
198
    }
199
200
    /// The next identity: the same key pair with these delegates.
201
    pub fn successor(&self, delegates: Vec<Delegate>, expires: Option<u64>, created: u64) -> Identity {
202
        Identity { delegates, expires, created, previous: Some(self.hash()), ..self.clone() }
203
    }
204
205
    fn ed_key(&self) -> Result<EdVerifyingKey, Error> {
206
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| Error::BadKey)
207
    }
208
209
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, Error> {
210
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| Error::BadKey)?;
211
        Ok(MlVerifyingKey::decode(&enc))
212
    }
213
}
214
215
/// What both halves sign: the signer's KeyId and the message, under
216
/// Spectra's prefix, so a signature means one thing by one key.
217
fn target(key: &KeyId, message: &[u8]) -> [u8; 32] {
218
    let mut h = blake3::Hasher::new();
219
    h.update(b"spectra1 sign ");
220
    h.update(&key.0);
221
    h.update(message);
222
    *h.finalize().as_bytes()
223
}
224
225
#[derive(Clone, PartialEq, Eq)]
226
pub struct Signature {
227
    pub key: KeyId,
228
    pub ed: [u8; 64],
229
    pub ml: Vec<u8>,
230
}
231
232
impl std::fmt::Debug for Signature {
233
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234
        write!(f, "Signature({:?})", self.key)
235
    }
236
}
237
238
impl Signature {
239
    pub fn to_value(&self) -> Value {
240
        Value::map([(0, Value::Bytes(self.key.0.to_vec())), (1, Value::Bytes(self.ed.to_vec())), (2, Value::Bytes(self.ml.clone()))])
241
    }
242
243
    pub fn from_value(v: &Value) -> Result<Signature, Error> {
244
        let m = v.as_map().ok_or_else(|| shape("a signature is a map"))?;
245
        Ok(Signature {
246
            key: key_of(m.get(&0), "the signature's key")?,
247
            ed: bytes_of(m.get(&1), "the Ed25519 signature")?.try_into().map_err(|_| shape("the Ed25519 signature"))?,
248
            ml: bytes_of(m.get(&2), "the ML-DSA-65 signature")?,
249
        })
250
    }
251
252
    pub fn encode(&self) -> Vec<u8> {
253
        cbor::encode(&self.to_value())
254
    }
255
256
    pub fn decode(b: &[u8]) -> Result<Signature, Error> {
257
        Signature::from_value(&cbor::decode(b)?)
258
    }
259
}
260
261
/// Check a signature by `identity` over `message`.
262
pub fn verify(identity: &Identity, sig: &Signature, message: &[u8]) -> Result<(), Error> {
263
    let key = identity.hash();
264
    if sig.key != key {
265
        return Err(Error::KeyMismatch);
266
    }
267
    let t = target(&key, message);
268
    identity.ed_key()?.verify_strict(&t, &EdSignature::from_bytes(&sig.ed)).map_err(|_| Error::Ed25519)?;
269
    let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| Error::MlDsa)?;
270
    let ml = MlSignature::<MlDsa65>::decode(&enc).ok_or(Error::MlDsa)?;
271
    if !identity.ml_key()?.verify_with_context(&t, b"", &ml) {
272
        return Err(Error::MlDsa);
273
    }
274
    Ok(())
275
}
276
277
/// The seeds of a key pair; stored only in its identity home, 0600.
278
#[derive(Clone)]
279
pub struct SecretKey {
280
    ed_seed: [u8; 32],
281
    ml_seed: [u8; 32],
282
}
283
284
impl std::fmt::Debug for SecretKey {
285
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286
        f.write_str("SecretKey(..)")
287
    }
288
}
289
290
impl SecretKey {
291
    pub fn generate() -> SecretKey {
292
        let (mut ed_seed, mut ml_seed) = ([0u8; 32], [0u8; 32]);
293
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
294
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
295
        SecretKey { ed_seed, ml_seed }
296
    }
297
298
    fn ed(&self) -> EdSigningKey {
299
        EdSigningKey::from_bytes(&self.ed_seed)
300
    }
301
302
    fn ml(&self) -> MlSigningKey<MlDsa65> {
303
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
304
    }
305
306
    /// The identity of this key pair, delegating to nobody.
307
    pub fn identity(&self, name: &str, contact: &str, created: u64) -> Identity {
308
        Identity {
309
            ed25519_pk: self.ed().verifying_key().to_bytes(),
310
            mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(),
311
            name: name.into(),
312
            contact: contact.into(),
313
            created,
314
            expires: None,
315
            previous: None,
316
            delegates: vec![],
317
        }
318
    }
319
320
    /// Sign `message` as the identity named `key` (this key pair's).
321
    pub fn sign(&self, key: &KeyId, message: &[u8]) -> Signature {
322
        let t = target(key, message);
323
        let ed = self.ed().sign(&t).to_bytes();
324
        let ml = self.ml().expanded_key().sign_deterministic(&t, b"").expect("an empty context is valid").encode().as_slice().to_vec();
325
        Signature { key: *key, ed, ml }
326
    }
327
328
    /// Whether this is the key pair of `identity`.
329
    pub fn matches(&self, identity: &Identity) -> bool {
330
        let mine = self.identity("", "", 0);
331
        mine.ed25519_pk == identity.ed25519_pk && mine.mldsa65_pk == identity.mldsa65_pk
332
    }
333
334
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
335
        cbor::encode(&Value::map([
336
            (0, Value::Uint(VERSION)),
337
            (1, Value::Bytes(key.0.to_vec())),
338
            (2, Value::Bytes(self.ed_seed.to_vec())),
339
            (3, Value::Bytes(self.ml_seed.to_vec())),
340
        ]))
341
    }
342
343
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), Error> {
344
        let v = cbor::decode(b)?;
345
        let m = v.as_map().ok_or_else(|| shape("a secret key is a map"))?;
346
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
347
            return Err(shape("a secret key of an unknown version"));
348
        }
349
        let seed = |k: u64| -> Result<[u8; 32], Error> { bytes_of(m.get(&k), "a seed")?.try_into().map_err(|_| shape("a seed")) };
350
        Ok((key_of(m.get(&1), "the secret's key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? }))
351
    }
352
}
353
354
#[cfg(test)]
355
mod tests {
356
    use super::*;
357
358
    #[test]
359
    fn a_signature_binds_the_key_and_the_message_and_needs_both_halves() {
360
        let sk = SecretKey::generate();
361
        let id = sk.identity("Andy", "a@example", 1);
362
        let key = id.hash();
363
        let sig = sk.sign(&key, b"spectra1 test hello");
364
        verify(&id, &sig, b"spectra1 test hello").unwrap();
365
        assert_eq!(verify(&id, &sig, b"spectra1 test other"), Err(Error::Ed25519));
366
        let other = SecretKey::generate().identity("B", "", 1);
367
        assert_eq!(verify(&other, &sig, b"spectra1 test hello"), Err(Error::KeyMismatch));
368
        let mut half = sig.clone();
369
        half.ml[0] ^= 1;
370
        assert!(verify(&id, &half, b"spectra1 test hello").is_err(), "a broken ML-DSA half fails");
371
        assert_eq!(Signature::decode(&sig.encode()).unwrap(), sig);
372
    }
373
374
    #[test]
375
    fn identities_round_trip_and_successors_chain() {
376
        let sk = SecretKey::generate();
377
        let id = sk.identity("Andy", "", 5);
378
        assert_eq!(Identity::decode(&id.encode()).unwrap(), id);
379
        let agent = SecretKey::generate().identity("claude", "", 5).hash();
380
        let next = id.successor(vec![Delegate { key: agent, expires: Some(100) }], None, 6);
381
        assert_eq!(next.previous, Some(id.hash()));
382
        assert_ne!(next.hash(), id.hash());
383
        assert!(next.delegates_to(&agent, 99));
384
        assert!(!next.delegates_to(&agent, 100), "expired");
385
        assert!(!id.delegates_to(&agent, 1));
386
        assert!(sk.matches(&next));
387
        let (k, back) = SecretKey::decode(&sk.encode(&next.hash())).unwrap();
388
        assert_eq!(k, next.hash());
389
        assert!(back.matches(&id));
390
    }
391
}