OXYGENHUB

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

226 of 258 lines run, 83 of 122 functions · run #31 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 {
252154×
    pub fn to_hex(&self) -> String {
262154×
        hex::encode(self.0)
272154×
    }
28344×
    pub fn from_hex(s: &str) -> Option<KeyId> {
29344×
        hex::decode(s.trim()).ok()?.try_into().ok().map(KeyId)
30344×
    }
31
    /// The first `n` hex digits, for display.
3289×
    pub fn short(&self, n: usize) -> String {
3389×
        let h = self.to_hex();
3489×
        h[..n.min(h.len())].to_string()
3589×
    }
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 {
58385×
    pub fn allows(&self, at_ms: u64) -> bool {
59385×
        self.expires.is_none_or(|e| at_ms < e)
60385×
    }
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
9610258×
fn bytes_of(v: Option<&Value>, what: &str) -> Result<Vec<u8>, Error> {
9710258×
    v.and_then(Value::as_bytes).map(|b| b.to_vec()).ok_or_else(|| shape(what))
9810258×
}
99
1003612×
fn key_of(v: Option<&Value>, what: &str) -> Result<KeyId, Error> {
1013612×
    bytes_of(v, what)?.try_into().map(KeyId).map_err(|_| shape(what))
1023612×
}
103
1042769×
fn opt_uint(v: Option<&Value>, what: &str) -> Result<Option<u64>, Error> {
1052769×
    match v {
1062744×
        None | Some(Value::Null) => Ok(None),
10725×
        Some(Value::Uint(u)) => Ok(Some(*u)),
1080×
        _ => Err(shape(what)),
109
    }
1102769×
}
111
11226636×
fn opt_val<T>(o: Option<T>, f: impl Fn(T) -> Value) -> Value {
11326636×
    o.map(f).unwrap_or(Value::Null)
11426636×
}
115
116
impl Identity {
1179149×
    pub fn to_value(&self) -> Value {
1189149×
        Value::map([
1199149×
            (0, Value::Uint(VERSION)),
1209149×
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
1219149×
            (2, Value::Bytes(self.mldsa65_pk.clone())),
1229149×
            (3, Value::Text(self.name.clone())),
1239149×
            (4, Value::Text(self.contact.clone())),
1249149×
            (5, Value::Uint(self.created)),
1259149×
            (6, opt_val(self.expires, Value::Uint)),
1269149×
            (7, opt_val(self.previous, |k| Value::Bytes(k.0.to_vec()))),
127
            (
128
                8,
129
                Value::Array(
1309149×
                    self.delegates
1319149×
                        .iter()
1329149×
                        .map(|d| Value::map([(0, Value::Bytes(d.key.0.to_vec())), (1, opt_val(d.expires, Value::Uint))]))
1339149×
                        .collect(),
134
                ),
135
            ),
136
        ])
1379149×
    }
138
1399149×
    pub fn encode(&self) -> Vec<u8> {
1409149×
        cbor::encode(&self.to_value())
1419149×
    }
142
1436895×
    pub fn hash(&self) -> KeyId {
1446895×
        let mut h = blake3::Hasher::new();
1456895×
        h.update(b"spectra1 identity ");
1466895×
        h.update(&self.encode());
1476895×
        KeyId(*h.finalize().as_bytes())
1486895×
    }
149
1501607×
    pub fn decode(b: &[u8]) -> Result<Identity, Error> {
1511607×
        let v = cbor::decode(b)?;
1521607×
        let m = v.as_map().ok_or_else(|| shape("an identity is a map"))?;
1531607×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
1540×
            return Err(shape("an identity of an unknown version"));
1551607×
        }
1563214×
        let text = |k: u64, what: &str| m.get(&k).and_then(Value::as_text).map(String::from).ok_or_else(|| shape(what));
1571607×
        let delegates = m
1581607×
            .get(&8)
1591607×
            .and_then(Value::as_array)
1601607×
            .ok_or_else(|| shape("delegates"))?
1611607×
            .iter()
1621607×
            .map(|d| {
1631162×
                let dm = d.as_map().ok_or_else(|| shape("a delegate is a map"))?;
1641162×
                Ok(Delegate { key: key_of(dm.get(&0), "a delegate's key")?, expires: opt_uint(dm.get(&1), "a delegate's expiry")? })
1651162×
            })
1661607×
            .collect::<Result<Vec<_>, Error>>()?;
1671607×
        let id = Identity {
1681607×
            ed25519_pk: bytes_of(m.get(&1), "the Ed25519 key")?.try_into().map_err(|_| shape("the Ed25519 key"))?,
1691607×
            mldsa65_pk: bytes_of(m.get(&2), "the ML-DSA-65 key")?,
1701607×
            name: text(3, "the name")?,
1711607×
            contact: text(4, "the contact")?,
1721607×
            created: m.get(&5).and_then(Value::as_uint).ok_or_else(|| shape("the creation time"))?,
1731607×
            expires: opt_uint(m.get(&6), "the expiry")?,
1741607×
            previous: match m.get(&7) {
175873×
                None | Some(Value::Null) => None,
176734×
                v => Some(key_of(v, "the previous identity")?),
177
            },
1781607×
            delegates,
179
        };
180
        // One identity, one encoding: what was read must be what we write.
1811607×
        if id.encode() != b {
1820×
            return Err(shape("an identity not in Spectra's own encoding"));
1831607×
        }
1841607×
        Ok(id)
1851607×
    }
186
187473×
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
188942×
        self.delegates.iter().find(|d| &d.key == key)
189473×
    }
190
191
    /// Whether this identity lets `key` act for it at `at_ms`.
192394×
    pub fn delegates_to(&self, key: &KeyId, at_ms: u64) -> bool {
193394×
        !self.is_expired_at(at_ms) && self.delegate(key).is_some_and(|d| d.allows(at_ms))
194394×
    }
195
196394×
    pub fn is_expired_at(&self, at_ms: u64) -> bool {
197394×
        self.expires.is_some_and(|e| at_ms >= e)
198394×
    }
199
200
    /// The next identity: the same key pair with these delegates.
20176×
    pub fn successor(&self, delegates: Vec<Delegate>, expires: Option<u64>, created: u64) -> Identity {
20276×
        Identity { delegates, expires, created, previous: Some(self.hash()), ..self.clone() }
20376×
    }
204
2051288×
    fn ed_key(&self) -> Result<EdVerifyingKey, Error> {
2061288×
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| Error::BadKey)
2071288×
    }
208
2091285×
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, Error> {
2101285×
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| Error::BadKey)?;
2111285×
        Ok(MlVerifyingKey::decode(&enc))
2121285×
    }
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.
2171841×
fn target(key: &KeyId, message: &[u8]) -> [u8; 32] {
2181841×
    let mut h = blake3::Hasher::new();
2191841×
    h.update(b"spectra1 sign ");
2201841×
    h.update(&key.0);
2211841×
    h.update(message);
2221841×
    *h.finalize().as_bytes()
2231841×
}
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 {
239679×
    pub fn to_value(&self) -> Value {
240679×
        Value::map([(0, Value::Bytes(self.key.0.to_vec())), (1, Value::Bytes(self.ed.to_vec())), (2, Value::Bytes(self.ml.clone()))])
241679×
    }
242
2431282×
    pub fn from_value(v: &Value) -> Result<Signature, Error> {
2441282×
        let m = v.as_map().ok_or_else(|| shape("a signature is a map"))?;
245
        Ok(Signature {
2461282×
            key: key_of(m.get(&0), "the signature's key")?,
2471282×
            ed: bytes_of(m.get(&1), "the Ed25519 signature")?.try_into().map_err(|_| shape("the Ed25519 signature"))?,
2481282×
            ml: bytes_of(m.get(&2), "the ML-DSA-65 signature")?,
249
        })
2501282×
    }
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`.
2621291×
pub fn verify(identity: &Identity, sig: &Signature, message: &[u8]) -> Result<(), Error> {
2631291×
    let key = identity.hash();
2641291×
    if sig.key != key {
2653×
        return Err(Error::KeyMismatch);
2661288×
    }
2671288×
    let t = target(&key, message);
2681288×
    identity.ed_key()?.verify_strict(&t, &EdSignature::from_bytes(&sig.ed)).map_err(|_| Error::Ed25519)?;
2691285×
    let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| Error::MlDsa)?;
2701285×
    let ml = MlSignature::<MlDsa65>::decode(&enc).ok_or(Error::MlDsa)?;
2711285×
    if !identity.ml_key()?.verify_with_context(&t, b"", &ml) {
2721×
        return Err(Error::MlDsa);
2731284×
    }
2741284×
    Ok(())
2751291×
}
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 {
291151×
    pub fn generate() -> SecretKey {
292151×
        let (mut ed_seed, mut ml_seed) = ([0u8; 32], [0u8; 32]);
293151×
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
294151×
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
295151×
        SecretKey { ed_seed, ml_seed }
296151×
    }
297
2981139×
    fn ed(&self) -> EdSigningKey {
2991139×
        EdSigningKey::from_bytes(&self.ed_seed)
3001139×
    }
301
3021139×
    fn ml(&self) -> MlSigningKey<MlDsa65> {
3031139×
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
3041139×
    }
305
306
    /// The identity of this key pair, delegating to nobody.
307586×
    pub fn identity(&self, name: &str, contact: &str, created: u64) -> Identity {
308586×
        Identity {
309586×
            ed25519_pk: self.ed().verifying_key().to_bytes(),
310586×
            mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(),
311586×
            name: name.into(),
312586×
            contact: contact.into(),
313586×
            created,
314586×
            expires: None,
315586×
            previous: None,
316586×
            delegates: vec![],
317586×
        }
318586×
    }
319
320
    /// Sign `message` as the identity named `key` (this key pair's).
321553×
    pub fn sign(&self, key: &KeyId, message: &[u8]) -> Signature {
322553×
        let t = target(key, message);
323553×
        let ed = self.ed().sign(&t).to_bytes();
324553×
        let ml = self.ml().expanded_key().sign_deterministic(&t, b"").expect("an empty context is valid").encode().as_slice().to_vec();
325553×
        Signature { key: *key, ed, ml }
326553×
    }
327
328
    /// Whether this is the key pair of `identity`.
329435×
    pub fn matches(&self, identity: &Identity) -> bool {
330435×
        let mine = self.identity("", "", 0);
331435×
        mine.ed25519_pk == identity.ed25519_pk && mine.mldsa65_pk == identity.mldsa65_pk
332435×
    }
333
334195×
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
335195×
        cbor::encode(&Value::map([
336195×
            (0, Value::Uint(VERSION)),
337195×
            (1, Value::Bytes(key.0.to_vec())),
338195×
            (2, Value::Bytes(self.ed_seed.to_vec())),
339195×
            (3, Value::Bytes(self.ml_seed.to_vec())),
340195×
        ]))
341195×
    }
342
343434×
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), Error> {
344434×
        let v = cbor::decode(b)?;
345434×
        let m = v.as_map().ok_or_else(|| shape("a secret key is a map"))?;
346434×
        if m.get(&0).and_then(Value::as_uint) != Some(VERSION) {
3470×
            return Err(shape("a secret key of an unknown version"));
348434×
        }
349868×
        let seed = |k: u64| -> Result<[u8; 32], Error> { bytes_of(m.get(&k), "a seed")?.try_into().map_err(|_| shape("a seed")) };
350434×
        Ok((key_of(m.get(&1), "the secret's key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? }))
351434×
    }
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
}