OXYGENHUB

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

226 of 258 lines run, 83 of 122 functions · run #29 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 {
252069×
    pub fn to_hex(&self) -> String {
262069×
        hex::encode(self.0)
272069×
    }
28326×
    pub fn from_hex(s: &str) -> Option<KeyId> {
29326×
        hex::decode(s.trim()).ok()?.try_into().ok().map(KeyId)
30326×
    }
31
    /// The first `n` hex digits, for display.
3285×
    pub fn short(&self, n: usize) -> String {
3385×
        let h = self.to_hex();
3485×
        h[..n.min(h.len())].to_string()
3585×
    }
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 {
58369×
    pub fn allows(&self, at_ms: u64) -> bool {
59369×
        self.expires.is_none_or(|e| at_ms < e)
60369×
    }
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
969846×
fn bytes_of(v: Option<&Value>, what: &str) -> Result<Vec<u8>, Error> {
979846×
    v.and_then(Value::as_bytes).map(|b| b.to_vec()).ok_or_else(|| shape(what))
989846×
}
99
1003456×
fn key_of(v: Option<&Value>, what: &str) -> Result<KeyId, Error> {
1013456×
    bytes_of(v, what)?.try_into().map(KeyId).map_err(|_| shape(what))
1023456×
}
103
1042648×
fn opt_uint(v: Option<&Value>, what: &str) -> Result<Option<u64>, Error> {
1052648×
    match v {
1062623×
        None | Some(Value::Null) => Ok(None),
10725×
        Some(Value::Uint(u)) => Ok(Some(*u)),
1080×
        _ => Err(shape(what)),
109
    }
1102648×
}
111
11225591×
fn opt_val<T>(o: Option<T>, f: impl Fn(T) -> Value) -> Value {
11325591×
    o.map(f).unwrap_or(Value::Null)
11425591×
}
115
116
impl Identity {
1178816×
    pub fn to_value(&self) -> Value {
1188816×
        Value::map([
1198816×
            (0, Value::Uint(VERSION)),
1208816×
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
1218816×
            (2, Value::Bytes(self.mldsa65_pk.clone())),
1228816×
            (3, Value::Text(self.name.clone())),
1238816×
            (4, Value::Text(self.contact.clone())),
1248816×
            (5, Value::Uint(self.created)),
1258816×
            (6, opt_val(self.expires, Value::Uint)),
1268816×
            (7, opt_val(self.previous, |k| Value::Bytes(k.0.to_vec()))),
127
            (
128
                8,
129
                Value::Array(
1308816×
                    self.delegates
1318816×
                        .iter()
1328816×
                        .map(|d| Value::map([(0, Value::Bytes(d.key.0.to_vec())), (1, opt_val(d.expires, Value::Uint))]))
1338816×
                        .collect(),
134
                ),
135
            ),
136
        ])
1378816×
    }
138
1398816×
    pub fn encode(&self) -> Vec<u8> {
1408816×
        cbor::encode(&self.to_value())
1418816×
    }
142
1436645×
    pub fn hash(&self) -> KeyId {
1446645×
        let mut h = blake3::Hasher::new();
1456645×
        h.update(b"spectra1 identity ");
1466645×
        h.update(&self.encode());
1476645×
        KeyId(*h.finalize().as_bytes())
1486645×
    }
149
1501543×
    pub fn decode(b: &[u8]) -> Result<Identity, Error> {
1511543×
        let v = cbor::decode(b)?;
1521543×
        let m = v.as_map().ok_or_else(|| shape("an identity is a map"))?;
1531543×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
1540×
            return Err(shape("an identity of an unknown version"));
1551543×
        }
1563086×
        let text = |k: u64, what: &str| m.get(&k).and_then(Value::as_text).map(String::from).ok_or_else(|| shape(what));
1571543×
        let delegates = m
1581543×
            .get(&8)
1591543×
            .and_then(Value::as_array)
1601543×
            .ok_or_else(|| shape("delegates"))?
1611543×
            .iter()
1621543×
            .map(|d| {
1631105×
                let dm = d.as_map().ok_or_else(|| shape("a delegate is a map"))?;
1641105×
                Ok(Delegate { key: key_of(dm.get(&0), "a delegate's key")?, expires: opt_uint(dm.get(&1), "a delegate's expiry")? })
1651105×
            })
1661543×
            .collect::<Result<Vec<_>, Error>>()?;
1671543×
        let id = Identity {
1681543×
            ed25519_pk: bytes_of(m.get(&1), "the Ed25519 key")?.try_into().map_err(|_| shape("the Ed25519 key"))?,
1691543×
            mldsa65_pk: bytes_of(m.get(&2), "the ML-DSA-65 key")?,
1701543×
            name: text(3, "the name")?,
1711543×
            contact: text(4, "the contact")?,
1721543×
            created: m.get(&5).and_then(Value::as_uint).ok_or_else(|| shape("the creation time"))?,
1731543×
            expires: opt_uint(m.get(&6), "the expiry")?,
1741543×
            previous: match m.get(&7) {
175844×
                None | Some(Value::Null) => None,
176699×
                v => Some(key_of(v, "the previous identity")?),
177
            },
1781543×
            delegates,
179
        };
180
        // One identity, one encoding: what was read must be what we write.
1811543×
        if id.encode() != b {
1820×
            return Err(shape("an identity not in Spectra's own encoding"));
1831543×
        }
1841543×
        Ok(id)
1851543×
    }
186
187451×
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
188889×
        self.delegates.iter().find(|d| &d.key == key)
189451×
    }
190
191
    /// Whether this identity lets `key` act for it at `at_ms`.
192378×
    pub fn delegates_to(&self, key: &KeyId, at_ms: u64) -> bool {
193378×
        !self.is_expired_at(at_ms) && self.delegate(key).is_some_and(|d| d.allows(at_ms))
194378×
    }
195
196378×
    pub fn is_expired_at(&self, at_ms: u64) -> bool {
197378×
        self.expires.is_some_and(|e| at_ms >= e)
198378×
    }
199
200
    /// The next identity: the same key pair with these delegates.
20173×
    pub fn successor(&self, delegates: Vec<Delegate>, expires: Option<u64>, created: u64) -> Identity {
20273×
        Identity { delegates, expires, created, previous: Some(self.hash()), ..self.clone() }
20373×
    }
204
2051244×
    fn ed_key(&self) -> Result<EdVerifyingKey, Error> {
2061244×
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| Error::BadKey)
2071244×
    }
208
2091241×
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, Error> {
2101241×
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| Error::BadKey)?;
2111241×
        Ok(MlVerifyingKey::decode(&enc))
2121241×
    }
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.
2171778×
fn target(key: &KeyId, message: &[u8]) -> [u8; 32] {
2181778×
    let mut h = blake3::Hasher::new();
2191778×
    h.update(b"spectra1 sign ");
2201778×
    h.update(&key.0);
2211778×
    h.update(message);
2221778×
    *h.finalize().as_bytes()
2231778×
}
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 {
239652×
    pub fn to_value(&self) -> Value {
240652×
        Value::map([(0, Value::Bytes(self.key.0.to_vec())), (1, Value::Bytes(self.ed.to_vec())), (2, Value::Bytes(self.ml.clone()))])
241652×
    }
242
2431238×
    pub fn from_value(v: &Value) -> Result<Signature, Error> {
2441238×
        let m = v.as_map().ok_or_else(|| shape("a signature is a map"))?;
245
        Ok(Signature {
2461238×
            key: key_of(m.get(&0), "the signature's key")?,
2471238×
            ed: bytes_of(m.get(&1), "the Ed25519 signature")?.try_into().map_err(|_| shape("the Ed25519 signature"))?,
2481238×
            ml: bytes_of(m.get(&2), "the ML-DSA-65 signature")?,
249
        })
2501238×
    }
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`.
2621247×
pub fn verify(identity: &Identity, sig: &Signature, message: &[u8]) -> Result<(), Error> {
2631247×
    let key = identity.hash();
2641247×
    if sig.key != key {
2653×
        return Err(Error::KeyMismatch);
2661244×
    }
2671244×
    let t = target(&key, message);
2681244×
    identity.ed_key()?.verify_strict(&t, &EdSignature::from_bytes(&sig.ed)).map_err(|_| Error::Ed25519)?;
2691241×
    let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| Error::MlDsa)?;
2701241×
    let ml = MlSignature::<MlDsa65>::decode(&enc).ok_or(Error::MlDsa)?;
2711241×
    if !identity.ml_key()?.verify_with_context(&t, b"", &ml) {
2721×
        return Err(Error::MlDsa);
2731240×
    }
2741240×
    Ok(())
2751247×
}
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 {
291147×
    pub fn generate() -> SecretKey {
292147×
        let (mut ed_seed, mut ml_seed) = ([0u8; 32], [0u8; 32]);
293147×
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
294147×
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
295147×
        SecretKey { ed_seed, ml_seed }
296147×
    }
297
2981096×
    fn ed(&self) -> EdSigningKey {
2991096×
        EdSigningKey::from_bytes(&self.ed_seed)
3001096×
    }
301
3021096×
    fn ml(&self) -> MlSigningKey<MlDsa65> {
3031096×
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
3041096×
    }
305
306
    /// The identity of this key pair, delegating to nobody.
307562×
    pub fn identity(&self, name: &str, contact: &str, created: u64) -> Identity {
308562×
        Identity {
309562×
            ed25519_pk: self.ed().verifying_key().to_bytes(),
310562×
            mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(),
311562×
            name: name.into(),
312562×
            contact: contact.into(),
313562×
            created,
314562×
            expires: None,
315562×
            previous: None,
316562×
            delegates: vec![],
317562×
        }
318562×
    }
319
320
    /// Sign `message` as the identity named `key` (this key pair's).
321534×
    pub fn sign(&self, key: &KeyId, message: &[u8]) -> Signature {
322534×
        let t = target(key, message);
323534×
        let ed = self.ed().sign(&t).to_bytes();
324534×
        let ml = self.ml().expanded_key().sign_deterministic(&t, b"").expect("an empty context is valid").encode().as_slice().to_vec();
325534×
        Signature { key: *key, ed, ml }
326534×
    }
327
328
    /// Whether this is the key pair of `identity`.
329415×
    pub fn matches(&self, identity: &Identity) -> bool {
330415×
        let mine = self.identity("", "", 0);
331415×
        mine.ed25519_pk == identity.ed25519_pk && mine.mldsa65_pk == identity.mldsa65_pk
332415×
    }
333
334188×
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
335188×
        cbor::encode(&Value::map([
336188×
            (0, Value::Uint(VERSION)),
337188×
            (1, Value::Bytes(key.0.to_vec())),
338188×
            (2, Value::Bytes(self.ed_seed.to_vec())),
339188×
            (3, Value::Bytes(self.ml_seed.to_vec())),
340188×
        ]))
341188×
    }
342
343414×
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), Error> {
344414×
        let v = cbor::decode(b)?;
345414×
        let m = v.as_map().ok_or_else(|| shape("a secret key is a map"))?;
346414×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
3470×
            return Err(shape("a secret key of an unknown version"));
348414×
        }
349828×
        let seed = |k: u64| -> Result<[u8; 32], Error> { bytes_of(m.get(&k), "a seed")?.try_into().map_err(|_| shape("a seed")) };
350414×
        Ok((key_of(m.get(&1), "the secret's key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? }))
351414×
    }
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
}