OXYGENHUB

Coverage / crates/oxy-core/src/identity.rs 95.5%

489 of 512 lines run, 88 of 133 functions · run #291 on main · run missed

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::{
10
    array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION,
11
};
12
use ed25519_dalek::ed25519::signature::Signer as _;
13
use ed25519_dalek::{
14
    Signature as EdSignature, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey,
15
};
16
use ml_dsa::{
17
    EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature,
18
    SigningKey as MlSigningKey, VerifyingKey as MlVerifyingKey,
19
};
20
use rand::RngCore;
21
22
pub const ED25519_PK_LEN: usize = 32;
23
pub const MLDSA65_PK_LEN: usize = 1952;
24
pub const ED25519_SIG_LEN: usize = 64;
25
pub const MLDSA65_SIG_LEN: usize = 3309;
26
/// `alg` value for Ed25519 + ML-DSA-65.
27
pub const ALG_COMPOSITE: u64 = 1;
28
29
/// What a delegate may do on the delegator's behalf (spec §14.2): a set of
30
/// `changes` (sign individual changes), `seal` (seal batches of them) and
31
/// `refs` (reference and tag attestations). An agent gets `changes` alone,
32
/// so it can never move a reference, whatever its owner may do.
33
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34
pub struct Scope(u8);
35
36
impl Scope {
37
    pub const NONE: Scope = Scope(0);
38
    pub const CHANGES: Scope = Scope(1);
39
    pub const REFS: Scope = Scope(2);
40
    pub const SEAL: Scope = Scope(4);
41
    /// Everything: what a delegate could do before scopes existed.
42
    pub const ALL: Scope = Scope(7);
43
    const NAMES: [(Scope, &'static str); 3] = [
44
        (Scope::CHANGES, "changes"),
45
        (Scope::REFS, "refs"),
46
        (Scope::SEAL, "seal"),
47
    ];
48
49690×
    pub fn has(self, other: Scope) -> bool {
50690×
        self.0 & other.0 == other.0
51690×
    }
5214×
    pub fn is_empty(self) -> bool {
5314×
        self.0 == 0
5414×
    }
55
    /// The names in the set, in the order they are written.
56214×
    pub fn names(self) -> Vec<&'static str> {
57214×
        Self::NAMES
58214×
            .iter()
59642×
            .filter(|(s, _)| self.has(*s))
60214×
            .map(|(_, n)| *n)
61214×
            .collect()
62214×
    }
63475×
    fn named(name: &str) -> Option<Scope> {
64475×
        Self::NAMES
65475×
            .iter()
66837×
            .find(|(_, n)| *n == name)
67475×
            .map(|(s, _)| *s)
68475×
    }
69
    /// `changes,refs` (also with spaces); `all` is every scope.
704×
    pub fn parse(s: &str) -> Result<Scope, String> {
714×
        let mut out = Scope::NONE;
725×
        for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
734×
            if part == "all" {
741×
                return Ok(Scope::ALL);
753×
            }
763×
            match Self::named(part) {
772×
                Some(sc) => out = out | sc,
781×
                None => return Err(format!("unknown scope {part:?}: use changes, refs or seal")),
79
            }
80
        }
812×
        if out.is_empty() {
821×
            return Err("a delegate needs at least one scope: changes, refs or seal".into());
831×
        }
841×
        Ok(out)
854×
    }
86
}
87
88
impl std::ops::BitOr for Scope {
89
    type Output = Scope;
90481×
    fn bitor(self, rhs: Scope) -> Scope {
91481×
        Scope(self.0 | rhs.0)
92481×
    }
93
}
94
95
impl std::fmt::Display for Scope {
9612×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9712×
        f.write_str(&self.names().join(","))
9812×
    }
99
}
100
101
impl std::fmt::Debug for Scope {
1020×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1030×
        write!(f, "Scope({self})")
1040×
    }
105
}
106
107
/// A key allowed to act for an identity, within `scope`, until `expires`
108
/// (wall milliseconds; `None` for as long as the delegation stands).
109
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110
pub struct Delegate {
111
    pub key: KeyId,
112
    pub scope: Scope,
113
    pub expires: Option<u64>,
114
}
115
116
impl Delegate {
117
    /// A delegate with every scope and no expiry of its own.
1184×
    pub fn full(key: KeyId) -> Delegate {
1194×
        Delegate {
1204×
            key,
1214×
            scope: Scope::ALL,
1224×
            expires: None,
1234×
        }
1244×
    }
125
    /// Whether the delegation covers `scope` at `at_ms`.
12629×
    pub fn allows(&self, scope: Scope, at_ms: u64) -> bool {
12729×
        self.scope.has(scope) && !self.expires.is_some_and(|e| at_ms > e)
12829×
    }
129202×
    fn to_value(self) -> Value {
130202×
        Value::map([
131202×
            (0, Value::Bytes(self.key.0.to_vec())),
132
            (
133
                1,
134
                Value::Array(
135202×
                    self.scope
136202×
                        .names()
137202×
                        .into_iter()
138457×
                        .map(|n| Value::Text(n.to_string()))
139202×
                        .collect(),
140
                ),
141
            ),
142202×
            (2, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
143
        ])
144202×
    }
145
    /// An entry, or a bare KeyId as identities carried before scopes
146
    /// existed, which meant every scope.
147217×
    fn from_value(v: &Value) -> Result<Delegate, ObjectError> {
148217×
        if let Value::Bytes(_) = v {
1491×
            return Ok(Delegate::full(oref(v, "delegate")?));
150216×
        }
151216×
        let m = v
152216×
            .as_map()
153216×
            .ok_or_else(|| ObjectError::Schema("delegate: expected map".into()))?;
154216×
        let key = oref(field(m, 0, "delegate key")?, "delegate key")?;
155216×
        let mut scope = Scope::NONE;
156472×
        for n in array(field(m, 1, "delegate scope")?, "delegate scope")? {
157472×
            let n = text(n, "delegate scope")?;
158472×
            scope = scope
159472×
                | Scope::named(n)
160472×
                    .ok_or_else(|| ObjectError::Schema(format!("delegate: unknown scope {n:?}")))?;
161
        }
162216×
        let expires = match field(m, 2, "delegate expires")? {
163216×
            v if v.is_null() => None,
1645×
            v => Some(uint(v, "delegate expires")?),
165
        };
166216×
        Ok(Delegate {
167216×
            key,
168216×
            scope,
169216×
            expires,
170216×
        })
171217×
    }
172
}
173
174
#[derive(Clone, Debug, PartialEq, Eq)]
175
pub struct Identity {
176
    pub ed25519_pk: [u8; ED25519_PK_LEN],
177
    pub mldsa65_pk: Vec<u8>,
178
    pub name: String,
179
    pub email: String,
180
    pub created: u64,
181
    pub expires: Option<u64>,
182
    pub previous: Option<KeyId>,
183
    pub delegates: Vec<Delegate>,
184
}
185
186
/// Sorted by key, one entry per key.
1871626×
fn tidy(mut delegates: Vec<Delegate>) -> Vec<Delegate> {
1881626×
    delegates.sort_by_key(|d| d.key);
1891626×
    delegates.dedup_by(|a, b| a.key == b.key);
1901626×
    delegates
1911626×
}
192
193
impl Identity {
194
    /// The delegation to `key`, if any.
19559×
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
19659×
        self.delegates.iter().find(|d| d.key == *key)
19759×
    }
198
    /// Whether `key` may act for this identity within `scope` at `at_ms`.
19915×
    pub fn delegates_to(&self, key: &KeyId, scope: Scope, at_ms: u64) -> bool {
20015×
        self.delegate(key).is_some_and(|d| d.allows(scope, at_ms))
20115×
    }
2021583×
    pub fn to_value(&self) -> Value {
2031583×
        Value::map([
2041583×
            (0, Value::Uint(FORMAT_VERSION)),
2051583×
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
2061583×
            (2, Value::Bytes(self.mldsa65_pk.clone())),
2071583×
            (3, Value::Text(self.name.clone())),
2081583×
            (4, Value::Text(self.email.clone())),
2091583×
            (5, Value::Uint(self.created)),
2101583×
            (6, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
211
            (
212
                7,
2131583×
                self.previous
2141583×
                    .map(|p| Value::Bytes(p.0.to_vec()))
2151583×
                    .unwrap_or(Value::Null),
216
            ),
217
            (
218
                8,
2191583×
                Value::Array(self.delegates.iter().map(|d| d.to_value()).collect()),
220
            ),
221
        ])
2221583×
    }
2231582×
    pub fn encode(&self) -> Vec<u8> {
2241582×
        cbor::encode(&self.to_value())
2251582×
    }
226749×
    pub fn hash(&self) -> KeyId {
227749×
        hash_object(ObjectType::Identity, &self.encode())
228749×
    }
2291613×
    pub fn decode(b: &[u8]) -> Result<Identity, ObjectError> {
2301613×
        let v = cbor::decode(b)?;
2311613×
        let m = v
2321613×
            .as_map()
2331613×
            .ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?;
2341613×
        check_version(m)?;
2351613×
        let ed = bytes(field(m, 1, "ed25519_pk")?, "ed25519_pk")?;
2361613×
        let ml = bytes(field(m, 2, "mldsa65_pk")?, "mldsa65_pk")?;
2371613×
        if ed.len() != ED25519_PK_LEN || ml.len() != MLDSA65_PK_LEN {
2380×
            return Err(ObjectError::Schema(
2390×
                "identity: wrong public key length".into(),
2400×
            ));
2411613×
        }
2421613×
        let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> {
2431613×
            if v.is_null() {
2441609×
                Ok(None)
245
            } else {
2464×
                uint(v, "expires").map(Some)
247
            }
2481613×
        };
2491613×
        let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> {
2501613×
            if v.is_null() {
2511178×
                Ok(None)
252
            } else {
253435×
                oref(v, "previous").map(Some)
254
            }
2551613×
        };
2561613×
        let delegates = tidy(
2571613×
            array(field(m, 8, "delegates")?, "delegates")?
2581613×
                .iter()
2591613×
                .map(Delegate::from_value)
2601613×
                .collect::<Result<Vec<_>, _>>()?,
261
        );
262
        Ok(Identity {
2631613×
            ed25519_pk: ed.try_into().unwrap(),
2641613×
            mldsa65_pk: ml.to_vec(),
2651613×
            name: text(field(m, 3, "name")?, "name")?.to_owned(),
2661613×
            email: text(field(m, 4, "email")?, "email")?.to_owned(),
2671613×
            created: uint(field(m, 5, "created")?, "created")?,
2681613×
            expires: opt_u(field(m, 6, "expires")?)?,
2691613×
            previous: opt_r(field(m, 7, "previous")?)?,
2701613×
            delegates,
271
        })
2721613×
    }
273
274
    /// Whether the identity is past its expiry at `now_ms`.
275218×
    pub fn is_expired_at(&self, now_ms: u64) -> bool {
276218×
        self.expires.is_some_and(|e| now_ms > e)
277218×
    }
278
279
    /// A successor identity with the same keys, `previous` set to this
280
    /// identity's KeyId, and the given delegates and expiry (spec §14.2).
28113×
    pub fn successor(
28213×
        &self,
28313×
        delegates: Vec<Delegate>,
28413×
        expires: Option<u64>,
28513×
        created: u64,
28613×
    ) -> Identity {
28713×
        Identity {
28813×
            previous: Some(self.hash()),
28913×
            delegates: tidy(delegates),
29013×
            expires,
29113×
            created,
29213×
            ..self.clone()
29313×
        }
29413×
    }
295
296439×
    fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> {
297439×
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey)
298439×
    }
299431×
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> {
300431×
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice())
301431×
            .map_err(|_| VerifyError::BadKey)?;
302431×
        Ok(MlVerifyingKey::decode(&enc))
303431×
    }
304
}
305
306
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
307
pub enum Domain {
308
    Change,
309
    Attest,
310
    Policy,
311
    /// A server-issued challenge, proving possession of a key to a host
312
    /// (hub spec §5.3, §5.4). Never a stored object.
313
    Challenge,
314
}
315
316
impl Domain {
317698×
    pub fn name(self) -> &'static str {
318698×
        match self {
319153×
            Domain::Change => "change",
320435×
            Domain::Attest => "attest",
321107×
            Domain::Policy => "policy",
3223×
            Domain::Challenge => "challenge",
323
        }
324698×
    }
325
}
326
327
/// `"oxy1 sig " || domain || 0x00 || KeyId || target` (spec §14.3).
328698×
pub fn signing_message(domain: Domain, key: &KeyId, target: &ObjectRef) -> Vec<u8> {
329698×
    let mut m = Vec::with_capacity(80);
330698×
    m.extend_from_slice(b"oxy1 sig ");
331698×
    m.extend_from_slice(domain.name().as_bytes());
332698×
    m.push(0);
333698×
    m.extend_from_slice(&key.0);
334698×
    m.extend_from_slice(&target.0);
335698×
    m
336698×
}
337
338
#[derive(Clone, PartialEq, Eq)]
339
pub struct Signature {
340
    pub key: KeyId,
341
    pub ed: [u8; ED25519_SIG_LEN],
342
    pub ml: Vec<u8>,
343
}
344
345
impl std::fmt::Debug for Signature {
3460×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3470×
        write!(f, "Signature(key {:?})", self.key)
3480×
    }
349
}
350
351
impl Signature {
352514×
    pub fn to_value(&self) -> Value {
353514×
        Value::map([
354514×
            (0, Value::Bytes(self.key.0.to_vec())),
355514×
            (1, Value::Uint(ALG_COMPOSITE)),
356514×
            (2, Value::Bytes(self.ed.to_vec())),
357514×
            (3, Value::Bytes(self.ml.clone())),
358514×
        ])
359514×
    }
3601252×
    pub fn from_value(v: &Value) -> Result<Signature, ObjectError> {
3611252×
        let m = v
3621252×
            .as_map()
3631252×
            .ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?;
3641252×
        let alg = uint(field(m, 1, "alg")?, "alg")?;
3651252×
        if alg != ALG_COMPOSITE {
3660×
            return Err(ObjectError::Schema(format!(
3670×
                "unknown signature algorithm {alg}"
3680×
            )));
3691252×
        }
3701252×
        let ed = bytes(field(m, 2, "ed")?, "ed")?;
3711252×
        let ml = bytes(field(m, 3, "ml")?, "ml")?;
3721252×
        if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN {
3731×
            return Err(ObjectError::Schema("signature: wrong length".into()));
3741251×
        }
375
        Ok(Signature {
3761251×
            key: oref(field(m, 0, "key")?, "key")?,
3771251×
            ed: ed.try_into().unwrap(),
3781251×
            ml: ml.to_vec(),
379
        })
3801252×
    }
381479×
    pub fn encode_list(sigs: &[Signature]) -> Vec<u8> {
382479×
        cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect()))
383479×
    }
3841201×
    pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> {
3851201×
        let v = cbor::decode(b)?;
3861201×
        array(&v, "signatures")?
3871201×
            .iter()
3881201×
            .map(Signature::from_value)
3891201×
            .collect()
3901201×
    }
391
}
392
393
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
394
pub enum VerifyError {
395
    #[error("signature names a different key than the identity")]
396
    KeyMismatch,
397
    #[error("identity holds a malformed public key")]
398
    BadKey,
399
    #[error("Ed25519 signature invalid")]
400
    Ed25519,
401
    #[error("ML-DSA-65 signature invalid")]
402
    MlDsa,
403
    #[error("a challenge is at least 16 bytes")]
404
    ShortChallenge,
405
}
406
407
/// Verify a composite signature by `identity` over `target` (spec §14.3).
408440×
pub fn verify(
409440×
    identity: &Identity,
410440×
    sig: &Signature,
411440×
    domain: Domain,
412440×
    target: &ObjectRef,
413440×
) -> Result<(), VerifyError> {
414440×
    let key = identity.hash();
415440×
    if sig.key != key {
4161×
        return Err(VerifyError::KeyMismatch);
417439×
    }
418439×
    let msg = signing_message(domain, &key, target);
419439×
    let ed_sig = EdSignature::from_bytes(&sig.ed);
420439×
    identity
421439×
        .ed_key()?
422439×
        .verify_strict(&msg, &ed_sig)
423439×
        .map_err(|_| VerifyError::Ed25519)?;
424432×
    let enc =
425432×
        EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?;
426432×
    let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?;
427431×
    if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) {
4280×
        return Err(VerifyError::MlDsa);
429431×
    }
430431×
    Ok(())
431440×
}
432
433
/// The seeds of a composite key pair. Stored only in the user's key directory.
434
#[derive(Clone)]
435
pub struct SecretKey {
436
    pub ed_seed: [u8; 32],
437
    pub ml_seed: [u8; 32],
438
}
439
440
impl std::fmt::Debug for SecretKey {
4410×
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4420×
        f.write_str("SecretKey(..)")
4430×
    }
444
}
445
446
impl SecretKey {
44771×
    pub fn generate() -> SecretKey {
44871×
        let mut ed_seed = [0u8; 32];
44971×
        let mut ml_seed = [0u8; 32];
45071×
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
45171×
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
45271×
        SecretKey { ed_seed, ml_seed }
45371×
    }
454
455348×
    fn ed(&self) -> EdSigningKey {
456348×
        EdSigningKey::from_bytes(&self.ed_seed)
457348×
    }
458348×
    fn ml(&self) -> MlSigningKey<MlDsa65> {
459348×
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
460348×
    }
461
462
    /// The identity object for these keys.
46389×
    pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity {
46489×
        Identity {
46589×
            ed25519_pk: self.ed().verifying_key().to_bytes(),
46689×
            mldsa65_pk: self
46789×
                .ml()
46889×
                .expanded_key()
46989×
                .verifying_key()
47089×
                .encode()
47189×
                .as_slice()
47289×
                .to_vec(),
47389×
            name: name.into(),
47489×
            email: email.into(),
47589×
            created,
47689×
            expires: None,
47789×
            previous: None,
47889×
            delegates: vec![],
47989×
        }
48089×
    }
481
482259×
    pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature {
483259×
        let msg = signing_message(domain, key, target);
484259×
        let ed = self.ed().sign(&msg).to_bytes();
485259×
        let ml = self
486259×
            .ml()
487259×
            .expanded_key()
488259×
            .sign_deterministic(&msg, b"")
489259×
            .expect("empty context is valid")
490259×
            .encode()
491259×
            .as_slice()
492259×
            .to_vec();
493259×
        Signature { key: *key, ed, ml }
494259×
    }
495
49661×
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
49761×
        cbor::encode(&Value::map([
49861×
            (0, Value::Uint(FORMAT_VERSION)),
49961×
            (1, Value::Bytes(key.0.to_vec())),
50061×
            (2, Value::Bytes(self.ed_seed.to_vec())),
50161×
            (3, Value::Bytes(self.ml_seed.to_vec())),
50261×
        ]))
50361×
    }
504
505386×
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> {
506386×
        let v = cbor::decode(b)?;
507386×
        let m = v
508386×
            .as_map()
509386×
            .ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?;
510386×
        check_version(m)?;
511772×
        let seed = |k: u64| -> Result<[u8; 32], ObjectError> {
512772×
            bytes(field(m, k, "seed")?, "seed")?
513772×
                .try_into()
514772×
                .map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into()))
515772×
        };
516
        Ok((
517386×
            oref(field(m, 1, "key")?, "key")?,
518
            SecretKey {
519386×
                ed_seed: seed(2)?,
520386×
                ml_seed: seed(3)?,
521
            },
522
        ))
523386×
    }
524
}
525
526
#[cfg(test)]
527
mod tests {
528
    use super::*;
529
5306×
    fn fixed_key() -> SecretKey {
5316×
        SecretKey {
5326×
            ed_seed: [1; 32],
5336×
            ml_seed: [2; 32],
5346×
        }
5356×
    }
536
537
    #[test]
5381×
    fn identity_round_trip_and_sizes() {
5391×
        let sk = fixed_key();
5401×
        let id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
5411×
        assert_eq!(id.mldsa65_pk.len(), MLDSA65_PK_LEN);
5421×
        let d = Identity::decode(&id.encode()).unwrap();
5431×
        assert_eq!(d, id);
5441×
        assert_eq!(d.hash(), id.hash());
545
        // Deterministic from seeds.
5461×
        assert_eq!(
5471×
            fixed_key()
5481×
                .identity("Ada", "[email protected]", 1_700_000_000_000)
5491×
                .hash(),
5501×
            id.hash()
551
        );
5521×
        let mut other = id.clone();
5531×
        other.delegates.push(Delegate::full(ObjectRef([9; 32])));
5541×
        assert_ne!(other.hash(), id.hash());
5551×
    }
556
557
    #[test]
5581×
    fn delegates_carry_scope_and_expiry_and_bare_keys_mean_every_scope() {
5591×
        let sk = fixed_key();
5601×
        let mut id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
5611×
        let agent = ObjectRef([7; 32]);
5621×
        let host = ObjectRef([3; 32]);
5631×
        id.delegates = vec![
5641×
            Delegate {
5651×
                key: agent,
5661×
                scope: Scope::CHANGES,
5671×
                expires: Some(2_000),
5681×
            },
5691×
            Delegate {
5701×
                key: host,
5711×
                scope: Scope::CHANGES | Scope::REFS,
5721×
                expires: None,
5731×
            },
574
        ];
5751×
        let d = Identity::decode(&id.encode()).unwrap();
5761×
        assert_eq!(d.delegates.len(), 2);
5771×
        assert!(d.delegates_to(&agent, Scope::CHANGES, 1_000));
5781×
        assert!(!d.delegates_to(&agent, Scope::CHANGES, 2_001), "expired");
5791×
        assert!(!d.delegates_to(&agent, Scope::REFS, 1_000));
5801×
        assert!(!d.delegates_to(&agent, Scope::SEAL, 1_000));
5811×
        assert!(d.delegates_to(&host, Scope::REFS, u64::MAX));
5821×
        assert!(!d.delegates_to(&host, Scope::SEAL, 0));
5831×
        assert!(!d.delegates_to(&ObjectRef([1; 32]), Scope::CHANGES, 0));
5841×
        assert_eq!(
5851×
            Scope::parse("changes, refs").unwrap(),
5861×
            Scope::CHANGES | Scope::REFS
587
        );
5881×
        assert_eq!(Scope::parse("all").unwrap(), Scope::ALL);
5891×
        assert_eq!((Scope::CHANGES | Scope::SEAL).to_string(), "changes,seal");
5901×
        assert!(Scope::parse("push").is_err() && Scope::parse("").is_err());
591
        // An identity written before scopes existed lists bare KeyIds.
5921×
        let mut v = id.to_value();
5931×
        if let Value::Map(m) = &mut v {
5941×
            m.insert(8, Value::Array(vec![Value::Bytes(host.0.to_vec())]));
5951×
        }
5961×
        let old = Identity::decode(&cbor::encode(&v)).unwrap();
5971×
        assert_eq!(old.delegates, vec![Delegate::full(host)]);
5981×
        assert!(old.delegates_to(&host, Scope::ALL, u64::MAX));
5991×
    }
600
601
    #[test]
6021×
    fn sign_and_verify() {
6031×
        let sk = fixed_key();
6041×
        let id = sk.identity("Ada", "[email protected]", 1);
6051×
        let key = id.hash();
6061×
        let target = ObjectRef([7; 32]);
6071×
        let sig = sk.sign(Domain::Change, &key, &target);
6081×
        assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN);
6091×
        assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(()));
610
        // Different target, different domain: invalid.
6111×
        assert_eq!(
6121×
            verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])),
613
            Err(VerifyError::Ed25519)
614
        );
6151×
        assert_eq!(
6161×
            verify(&id, &sig, Domain::Attest, &target),
617
            Err(VerifyError::Ed25519)
618
        );
619
        // Deterministic signatures.
6201×
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
621
        // Signature list round trip.
6221×
        let l =
6231×
            Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
6241×
        assert_eq!(l, vec![sig]);
6251×
    }
626
627
    #[test]
6281×
    fn stripping_or_forging_either_half_fails() {
6291×
        let sk = fixed_key();
6301×
        let id = sk.identity("Ada", "a@b", 1);
6311×
        let key = id.hash();
6321×
        let target = ObjectRef([7; 32]);
6331×
        let good = sk.sign(Domain::Change, &key, &target);
634
6351×
        let mut no_ml = good.clone();
6361×
        no_ml.ml = vec![0; MLDSA65_SIG_LEN];
6371×
        assert_eq!(
6381×
            verify(&id, &no_ml, Domain::Change, &target),
639
            Err(VerifyError::MlDsa)
640
        );
641
6421×
        let mut bad_ed = good.clone();
6431×
        bad_ed.ed[10] ^= 1;
6441×
        assert_eq!(
6451×
            verify(&id, &bad_ed, Domain::Change, &target),
646
            Err(VerifyError::Ed25519)
647
        );
648
649
        // A signature made for another KeyId over the same target does not transfer.
6501×
        let other = SecretKey {
6511×
            ed_seed: [3; 32],
6521×
            ml_seed: [4; 32],
6531×
        };
6541×
        let other_id = other.identity("Bob", "b@b", 1);
6551×
        let theirs = other.sign(Domain::Change, &other_id.hash(), &target);
6561×
        assert_eq!(
6571×
            verify(&id, &theirs, Domain::Change, &target),
658
            Err(VerifyError::KeyMismatch)
659
        );
6601×
        let mut relabelled = theirs.clone();
6611×
        relabelled.key = key;
6621×
        assert_eq!(
6631×
            verify(&id, &relabelled, Domain::Change, &target),
664
            Err(VerifyError::Ed25519)
665
        );
666
667
        // Wrong length is rejected at decode time.
6681×
        let mut short = good.to_value();
6691×
        if let Value::Map(m) = &mut short {
6701×
            m.insert(3, Value::Bytes(vec![0; 10]));
6711×
        }
6721×
        assert!(Signature::from_value(&short).is_err());
6731×
    }
674
675
    #[test]
6761×
    fn secret_key_round_trip() {
6771×
        let sk = fixed_key();
6781×
        let key = sk.identity("x", "y", 0).hash();
6791×
        let (k, d) = SecretKey::decode(&sk.encode(&key)).unwrap();
6801×
        assert_eq!(k, key);
6811×
        assert_eq!(d.ed_seed, sk.ed_seed);
6821×
        assert_eq!(d.ml_seed, sk.ml_seed);
6831×
        assert_ne!(SecretKey::generate().ed_seed, SecretKey::generate().ed_seed);
6841×
    }
685
}
686
687
/// The signing target of a challenge: `BLAKE3-256("oxy1 challenge " || bytes)`,
688
/// so a challenge can never collide with a stored object's reference.
6894×
pub fn challenge_target(challenge: &[u8]) -> ObjectRef {
6904×
    let mut h = blake3::Hasher::new();
6914×
    h.update(b"oxy1 challenge ");
6924×
    h.update(challenge);
6934×
    ObjectRef(*h.finalize().as_bytes())
6944×
}
695
696
impl SecretKey {
697
    /// Sign a host's challenge (at least 16 bytes) with this key.
6982×
    pub fn sign_challenge(&self, key: &KeyId, challenge: &[u8]) -> Result<Signature, VerifyError> {
6992×
        if challenge.len() < 16 {
7001×
            return Err(VerifyError::ShortChallenge);
7011×
        }
7021×
        Ok(self.sign(Domain::Challenge, key, &challenge_target(challenge)))
7032×
    }
704
}
705
706
/// Check a challenge signature against the identity that claims it.
7073×
pub fn verify_challenge(
7083×
    identity: &Identity,
7093×
    sig: &Signature,
7103×
    challenge: &[u8],
7113×
) -> Result<(), VerifyError> {
7123×
    if challenge.len() < 16 {
7130×
        return Err(VerifyError::ShortChallenge);
7143×
    }
7153×
    if sig.key != identity.hash() {
7161×
        return Err(VerifyError::KeyMismatch);
7172×
    }
7182×
    verify(
7192×
        identity,
7202×
        sig,
7212×
        Domain::Challenge,
7222×
        &challenge_target(challenge),
723
    )
7243×
}
725
726
#[cfg(test)]
727
mod challenge_tests {
728
    use super::*;
729
730
    #[test]
7311×
    fn a_challenge_signature_binds_key_and_bytes() {
7321×
        let sk = SecretKey::generate();
7331×
        let id = sk.identity("A", "a@example", 1);
7341×
        let key = id.hash();
7351×
        let challenge = [7u8; 32];
7361×
        let sig = sk.sign_challenge(&key, &challenge).unwrap();
7371×
        assert!(verify_challenge(&id, &sig, &challenge).is_ok());
7381×
        assert!(
7391×
            verify_challenge(&id, &sig, &[8u8; 32]).is_err(),
740
            "other bytes"
741
        );
7421×
        let other = SecretKey::generate().identity("B", "b@example", 1);
7431×
        assert!(
7441×
            verify_challenge(&other, &sig, &challenge).is_err(),
745
            "other identity"
746
        );
7471×
        assert!(sk.sign_challenge(&key, b"short").is_err());
748
        // A challenge target never equals a change signature's target for the same bytes.
7491×
        assert_ne!(challenge_target(&challenge), ObjectRef(challenge));
7501×
    }
751
}