OXYGENHUB

Coverage / crates/spectra-core/src/identity.rs 87.6%

226 of 258 lines run, 83 of 122 functions · run #36 on main · run missed

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 {
252296×
    pub fn to_hex(&self) -> String {
262296×
        hex::encode(self.0)
272296×
    }
28381×
    pub fn from_hex(s: &str) -> Option<KeyId> {
29381×
        hex::decode(s.trim()).ok()?.try_into().ok().map(KeyId)
30381×
    }
31
    /// The first `n` hex digits, for display.
3291×
    pub fn short(&self, n: usize) -> String {
3391×
        let h = self.to_hex();
3491×
        h[..n.min(h.len())].to_string()
3591×
    }
36
}
37
38
impl std::fmt::Debug for KeyId {
390×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400×
        write!(f, "KeyId({})", self.short(12))
410×
    }
42
}
43
44
impl std::fmt::Display for KeyId {
450×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460×
        f.write_str(&self.to_hex())
470×
    }
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 {
58439×
    pub fn allows(&self, at_ms: u64) -> bool {
59439×
        self.expires.is_none_or(|e| at_ms < e)
60439×
    }
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
920×
fn shape(s: &str) -> Error {
930×
    Error::Shape(s.into())
940×
}
95
9611346×
fn bytes_of(v: Option<&Value>, what: &str) -> Result<Vec<u8>, Error> {
9711346×
    v.and_then(Value::as_bytes).map(|b| b.to_vec()).ok_or_else(|| shape(what))
9811346×
}
99
1004062×
fn key_of(v: Option<&Value>, what: &str) -> Result<KeyId, Error> {
1014062×
    bytes_of(v, what)?.try_into().map(KeyId).map_err(|_| shape(what))
1024062×
}
103
1043119×
fn opt_uint(v: Option<&Value>, what: &str) -> Result<Option<u64>, Error> {
1053119×
    match v {
1063094×
        None | Some(Value::Null) => Ok(None),
10725×
        Some(Value::Uint(u)) => Ok(Some(*u)),
1080×
        _ => Err(shape(what)),
109
    }
1103119×
}
111
11229656×
fn opt_val<T>(o: Option<T>, f: impl Fn(T) -> Value) -> Value {
11329656×
    o.map(f).unwrap_or(Value::Null)
11429656×
}
115
116
impl Identity {
1179951×
    pub fn to_value(&self) -> Value {
1189951×
        Value::map([
1199951×
            (0, Value::Uint(VERSION)),
1209951×
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
1219951×
            (2, Value::Bytes(self.mldsa65_pk.clone())),
1229951×
            (3, Value::Text(self.name.clone())),
1239951×
            (4, Value::Text(self.contact.clone())),
1249951×
            (5, Value::Uint(self.created)),
1259951×
            (6, opt_val(self.expires, Value::Uint)),
1269951×
            (7, opt_val(self.previous, |k| Value::Bytes(k.0.to_vec()))),
127
            (
128
                8,
129
                Value::Array(
1309951×
                    self.delegates
1319951×
                        .iter()
1329951×
                        .map(|d| Value::map([(0, Value::Bytes(d.key.0.to_vec())), (1, opt_val(d.expires, Value::Uint))]))
1339951×
                        .collect(),
134
                ),
135
            ),
136
        ])
1379951×
    }
138
1399951×
    pub fn encode(&self) -> Vec<u8> {
1409951×
        cbor::encode(&self.to_value())
1419951×
    }
142
1437521×
    pub fn hash(&self) -> KeyId {
1447521×
        let mut h = blake3::Hasher::new();
1457521×
        h.update(b"spectra1 identity ");
1467521×
        h.update(&self.encode());
1477521×
        KeyId(*h.finalize().as_bytes())
1487521×
    }
149
1501762×
    pub fn decode(b: &[u8]) -> Result<Identity, Error> {
1511762×
        let v = cbor::decode(b)?;
1521762×
        let m = v.as_map().ok_or_else(|| shape("an identity is a map"))?;
1531762×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
1540×
            return Err(shape("an identity of an unknown version"));
1551762×
        }
1563524×
        let text = |k: u64, what: &str| m.get(&k).and_then(Value::as_text).map(String::from).ok_or_else(|| shape(what));
1571762×
        let delegates = m
1581762×
            .get(&8)
1591762×
            .and_then(Value::as_array)
1601762×
            .ok_or_else(|| shape("delegates"))?
1611762×
            .iter()
1621762×
            .map(|d| {
1631357×
                let dm = d.as_map().ok_or_else(|| shape("a delegate is a map"))?;
1641357×
                Ok(Delegate { key: key_of(dm.get(&0), "a delegate's key")?, expires: opt_uint(dm.get(&1), "a delegate's expiry")? })
1651357×
            })
1661762×
            .collect::<Result<Vec<_>, Error>>()?;
1671762×
        let id = Identity {
1681762×
            ed25519_pk: bytes_of(m.get(&1), "the Ed25519 key")?.try_into().map_err(|_| shape("the Ed25519 key"))?,
1691762×
            mldsa65_pk: bytes_of(m.get(&2), "the ML-DSA-65 key")?,
1701762×
            name: text(3, "the name")?,
1711762×
            contact: text(4, "the contact")?,
1721762×
            created: m.get(&5).and_then(Value::as_uint).ok_or_else(|| shape("the creation time"))?,
1731762×
            expires: opt_uint(m.get(&6), "the expiry")?,
1741762×
            previous: match m.get(&7) {
175937×
                None | Some(Value::Null) => None,
176825×
                v => Some(key_of(v, "the previous identity")?),
177
            },
1781762×
            delegates,
179
        };
180
        // One identity, one encoding: what was read must be what we write.
1811762×
        if id.encode() != b {
1820×
            return Err(shape("an identity not in Spectra's own encoding"));
1831762×
        }
1841762×
        Ok(id)
1851762×
    }
186
187538×
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
1881117×
        self.delegates.iter().find(|d| &d.key == key)
189538×
    }
190
191
    /// Whether this identity lets `key` act for it at `at_ms`.
192447×
    pub fn delegates_to(&self, key: &KeyId, at_ms: u64) -> bool {
193447×
        !self.is_expired_at(at_ms) && self.delegate(key).is_some_and(|d| d.allows(at_ms))
194447×
    }
195
196447×
    pub fn is_expired_at(&self, at_ms: u64) -> bool {
197447×
        self.expires.is_some_and(|e| at_ms >= e)
198447×
    }
199
200
    /// The next identity: the same key pair with these delegates.
20179×
    pub fn successor(&self, delegates: Vec<Delegate>, expires: Option<u64>, created: u64) -> Identity {
20279×
        Identity { delegates, expires, created, previous: Some(self.hash()), ..self.clone() }
20379×
    }
204
2051419×
    fn ed_key(&self) -> Result<EdVerifyingKey, Error> {
2061419×
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| Error::BadKey)
2071419×
    }
208
2091416×
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, Error> {
2101416×
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| Error::BadKey)?;
2111416×
        Ok(MlVerifyingKey::decode(&enc))
2121416×
    }
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.
2172010×
fn target(key: &KeyId, message: &[u8]) -> [u8; 32] {
2182010×
    let mut h = blake3::Hasher::new();
2192010×
    h.update(b"spectra1 sign ");
2202010×
    h.update(&key.0);
2212010×
    h.update(message);
2222010×
    *h.finalize().as_bytes()
2232010×
}
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 {
2330×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2340×
        write!(f, "Signature({:?})", self.key)
2350×
    }
236
}
237
238
impl Signature {
239734×
    pub fn to_value(&self) -> Value {
240734×
        Value::map([(0, Value::Bytes(self.key.0.to_vec())), (1, Value::Bytes(self.ed.to_vec())), (2, Value::Bytes(self.ml.clone()))])
241734×
    }
242
2431413×
    pub fn from_value(v: &Value) -> Result<Signature, Error> {
2441413×
        let m = v.as_map().ok_or_else(|| shape("a signature is a map"))?;
245
        Ok(Signature {
2461413×
            key: key_of(m.get(&0), "the signature's key")?,
2471413×
            ed: bytes_of(m.get(&1), "the Ed25519 signature")?.try_into().map_err(|_| shape("the Ed25519 signature"))?,
2481413×
            ml: bytes_of(m.get(&2), "the ML-DSA-65 signature")?,
249
        })
2501413×
    }
251
2521×
    pub fn encode(&self) -> Vec<u8> {
2531×
        cbor::encode(&self.to_value())
2541×
    }
255
2561×
    pub fn decode(b: &[u8]) -> Result<Signature, Error> {
2571×
        Signature::from_value(&cbor::decode(b)?)
2581×
    }
259
}
260
261
/// Check a signature by `identity` over `message`.
2621422×
pub fn verify(identity: &Identity, sig: &Signature, message: &[u8]) -> Result<(), Error> {
2631422×
    let key = identity.hash();
2641422×
    if sig.key != key {
2653×
        return Err(Error::KeyMismatch);
2661419×
    }
2671419×
    let t = target(&key, message);
2681419×
    identity.ed_key()?.verify_strict(&t, &EdSignature::from_bytes(&sig.ed)).map_err(|_| Error::Ed25519)?;
2691416×
    let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| Error::MlDsa)?;
2701416×
    let ml = MlSignature::<MlDsa65>::decode(&enc).ok_or(Error::MlDsa)?;
2711416×
    if !identity.ml_key()?.verify_with_context(&t, b"", &ml) {
2721×
        return Err(Error::MlDsa);
2731415×
    }
2741415×
    Ok(())
2751422×
}
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 {
2850×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2860×
        f.write_str("SecretKey(..)")
2870×
    }
288
}
289
290
impl SecretKey {
291154×
    pub fn generate() -> SecretKey {
292154×
        let (mut ed_seed, mut ml_seed) = ([0u8; 32], [0u8; 32]);
293154×
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
294154×
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
295154×
        SecretKey { ed_seed, ml_seed }
296154×
    }
297
2981213×
    fn ed(&self) -> EdSigningKey {
2991213×
        EdSigningKey::from_bytes(&self.ed_seed)
3001213×
    }
301
3021213×
    fn ml(&self) -> MlSigningKey<MlDsa65> {
3031213×
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
3041213×
    }
305
306
    /// The identity of this key pair, delegating to nobody.
307622×
    pub fn identity(&self, name: &str, contact: &str, created: u64) -> Identity {
308622×
        Identity {
309622×
            ed25519_pk: self.ed().verifying_key().to_bytes(),
310622×
            mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(),
311622×
            name: name.into(),
312622×
            contact: contact.into(),
313622×
            created,
314622×
            expires: None,
315622×
            previous: None,
316622×
            delegates: vec![],
317622×
        }
318622×
    }
319
320
    /// Sign `message` as the identity named `key` (this key pair's).
321591×
    pub fn sign(&self, key: &KeyId, message: &[u8]) -> Signature {
322591×
        let t = target(key, message);
323591×
        let ed = self.ed().sign(&t).to_bytes();
324591×
        let ml = self.ml().expanded_key().sign_deterministic(&t, b"").expect("an empty context is valid").encode().as_slice().to_vec();
325591×
        Signature { key: *key, ed, ml }
326591×
    }
327
328
    /// Whether this is the key pair of `identity`.
329468×
    pub fn matches(&self, identity: &Identity) -> bool {
330468×
        let mine = self.identity("", "", 0);
331468×
        mine.ed25519_pk == identity.ed25519_pk && mine.mldsa65_pk == identity.mldsa65_pk
332468×
    }
333
334201×
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
335201×
        cbor::encode(&Value::map([
336201×
            (0, Value::Uint(VERSION)),
337201×
            (1, Value::Bytes(key.0.to_vec())),
338201×
            (2, Value::Bytes(self.ed_seed.to_vec())),
339201×
            (3, Value::Bytes(self.ml_seed.to_vec())),
340201×
        ]))
341201×
    }
342
343467×
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), Error> {
344467×
        let v = cbor::decode(b)?;
345467×
        let m = v.as_map().ok_or_else(|| shape("a secret key is a map"))?;
346467×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
3470×
            return Err(shape("a secret key of an unknown version"));
348467×
        }
349934×
        let seed = |k: u64| -> Result<[u8; 32], Error> { bytes_of(m.get(&k), "a seed")?.try_into().map_err(|_| shape("a seed")) };
350467×
        Ok((key_of(m.get(&1), "the secret's key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? }))
351467×
    }
352
}
353
354
#[cfg(test)]
355
mod tests {
356
    use super::*;
357
358
    #[test]
3591×
    fn a_signature_binds_the_key_and_the_message_and_needs_both_halves() {
3601×
        let sk = SecretKey::generate();
3611×
        let id = sk.identity("Andy", "a@example", 1);
3621×
        let key = id.hash();
3631×
        let sig = sk.sign(&key, b"spectra1 test hello");
3641×
        verify(&id, &sig, b"spectra1 test hello").unwrap();
3651×
        assert_eq!(verify(&id, &sig, b"spectra1 test other"), Err(Error::Ed25519));
3661×
        let other = SecretKey::generate().identity("B", "", 1);
3671×
        assert_eq!(verify(&other, &sig, b"spectra1 test hello"), Err(Error::KeyMismatch));
3681×
        let mut half = sig.clone();
3691×
        half.ml[0] ^= 1;
3701×
        assert!(verify(&id, &half, b"spectra1 test hello").is_err(), "a broken ML-DSA half fails");
3711×
        assert_eq!(Signature::decode(&sig.encode()).unwrap(), sig);
3721×
    }
373
374
    #[test]
3751×
    fn identities_round_trip_and_successors_chain() {
3761×
        let sk = SecretKey::generate();
3771×
        let id = sk.identity("Andy", "", 5);
3781×
        assert_eq!(Identity::decode(&id.encode()).unwrap(), id);
3791×
        let agent = SecretKey::generate().identity("claude", "", 5).hash();
3801×
        let next = id.successor(vec![Delegate { key: agent, expires: Some(100) }], None, 6);
3811×
        assert_eq!(next.previous, Some(id.hash()));
3821×
        assert_ne!(next.hash(), id.hash());
3831×
        assert!(next.delegates_to(&agent, 99));
3841×
        assert!(!next.delegates_to(&agent, 100), "expired");
3851×
        assert!(!id.delegates_to(&agent, 1));
3861×
        assert!(sk.matches(&next));
3871×
        let (k, back) = SecretKey::decode(&sk.encode(&next.hash())).unwrap();
3881×
        assert_eq!(k, next.hash());
3891×
        assert!(back.matches(&id));
3901×
    }
391
}