OXYGENHUB
◆ v0.4.6 oxygen / crates / oxy-core / src / identity.rs
25.5 KiB · Rust · last changed Grants let certified keys act for an author (keyless step 5a) 18 days ago source blame history raw
1983686f Andy McCall ↶ 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};
75167672 Andy McCall 9
use crate::objects::{
10
    array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION,
11
};
1983686f Andy McCall ↶ 12
use ed25519_dalek::ed25519::signature::Signer as _;
75167672 Andy McCall 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
};
1983686f Andy McCall ↶ 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
7ba155f2 Andy McCall ↶ 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
49
    pub fn has(self, other: Scope) -> bool {
50
        self.0 & other.0 == other.0
51
    }
52
    pub fn is_empty(self) -> bool {
53
        self.0 == 0
54
    }
55
    /// The names in the set, in the order they are written.
56
    pub fn names(self) -> Vec<&'static str> {
57
        Self::NAMES
58
            .iter()
59
            .filter(|(s, _)| self.has(*s))
60
            .map(|(_, n)| *n)
61
            .collect()
62
    }
02e69188 Andy McCall ↶ 63
    pub(crate) fn named(name: &str) -> Option<Scope> {
7ba155f2 Andy McCall ↶ 64
        Self::NAMES
65
            .iter()
66
            .find(|(_, n)| *n == name)
67
            .map(|(s, _)| *s)
68
    }
69
    /// `changes,refs` (also with spaces); `all` is every scope.
70
    pub fn parse(s: &str) -> Result<Scope, String> {
71
        let mut out = Scope::NONE;
72
        for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
73
            if part == "all" {
74
                return Ok(Scope::ALL);
75
            }
76
            match Self::named(part) {
77
                Some(sc) => out = out | sc,
78
                None => return Err(format!("unknown scope {part:?}: use changes, refs or seal")),
79
            }
80
        }
81
        if out.is_empty() {
82
            return Err("a delegate needs at least one scope: changes, refs or seal".into());
83
        }
84
        Ok(out)
85
    }
86
}
87
88
impl std::ops::BitOr for Scope {
89
    type Output = Scope;
90
    fn bitor(self, rhs: Scope) -> Scope {
91
        Scope(self.0 | rhs.0)
92
    }
93
}
94
95
impl std::fmt::Display for Scope {
96
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97
        f.write_str(&self.names().join(","))
98
    }
99
}
100
101
impl std::fmt::Debug for Scope {
102
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103
        write!(f, "Scope({self})")
104
    }
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.
118
    pub fn full(key: KeyId) -> Delegate {
119
        Delegate {
120
            key,
121
            scope: Scope::ALL,
122
            expires: None,
123
        }
124
    }
125
    /// Whether the delegation covers `scope` at `at_ms`.
126
    pub fn allows(&self, scope: Scope, at_ms: u64) -> bool {
127
        self.scope.has(scope) && !self.expires.is_some_and(|e| at_ms > e)
128
    }
129
    fn to_value(self) -> Value {
130
        Value::map([
131
            (0, Value::Bytes(self.key.0.to_vec())),
132
            (
133
                1,
134
                Value::Array(
135
                    self.scope
136
                        .names()
137
                        .into_iter()
138
                        .map(|n| Value::Text(n.to_string()))
139
                        .collect(),
140
                ),
141
            ),
142
            (2, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
143
        ])
144
    }
145
    /// An entry, or a bare KeyId as identities carried before scopes
146
    /// existed, which meant every scope.
147
    fn from_value(v: &Value) -> Result<Delegate, ObjectError> {
148
        if let Value::Bytes(_) = v {
149
            return Ok(Delegate::full(oref(v, "delegate")?));
150
        }
151
        let m = v
152
            .as_map()
153
            .ok_or_else(|| ObjectError::Schema("delegate: expected map".into()))?;
154
        let key = oref(field(m, 0, "delegate key")?, "delegate key")?;
155
        let mut scope = Scope::NONE;
156
        for n in array(field(m, 1, "delegate scope")?, "delegate scope")? {
157
            let n = text(n, "delegate scope")?;
158
            scope = scope
159
                | Scope::named(n)
160
                    .ok_or_else(|| ObjectError::Schema(format!("delegate: unknown scope {n:?}")))?;
161
        }
162
        let expires = match field(m, 2, "delegate expires")? {
163
            v if v.is_null() => None,
164
            v => Some(uint(v, "delegate expires")?),
165
        };
166
        Ok(Delegate {
167
            key,
168
            scope,
169
            expires,
170
        })
171
    }
172
}
173
1983686f Andy McCall ↶ 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>,
7ba155f2 Andy McCall ↶ 183
    pub delegates: Vec<Delegate>,
184
}
185
186
/// Sorted by key, one entry per key.
187
fn tidy(mut delegates: Vec<Delegate>) -> Vec<Delegate> {
188
    delegates.sort_by_key(|d| d.key);
189
    delegates.dedup_by(|a, b| a.key == b.key);
190
    delegates
1983686f Andy McCall ↶ 191
}
192
193
impl Identity {
7ba155f2 Andy McCall ↶ 194
    /// The delegation to `key`, if any.
195
    pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> {
196
        self.delegates.iter().find(|d| d.key == *key)
197
    }
198
    /// Whether `key` may act for this identity within `scope` at `at_ms`.
199
    pub fn delegates_to(&self, key: &KeyId, scope: Scope, at_ms: u64) -> bool {
200
        self.delegate(key).is_some_and(|d| d.allows(scope, at_ms))
201
    }
1983686f Andy McCall ↶ 202
    pub fn to_value(&self) -> Value {
203
        Value::map([
204
            (0, Value::Uint(FORMAT_VERSION)),
205
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
206
            (2, Value::Bytes(self.mldsa65_pk.clone())),
207
            (3, Value::Text(self.name.clone())),
208
            (4, Value::Text(self.email.clone())),
209
            (5, Value::Uint(self.created)),
210
            (6, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
75167672 Andy McCall 211
            (
212
                7,
213
                self.previous
214
                    .map(|p| Value::Bytes(p.0.to_vec()))
215
                    .unwrap_or(Value::Null),
216
            ),
217
            (
218
                8,
7ba155f2 Andy McCall ↶ 219
                Value::Array(self.delegates.iter().map(|d| d.to_value()).collect()),
75167672 Andy McCall 220
            ),
1983686f Andy McCall ↶ 221
        ])
222
    }
223
    pub fn encode(&self) -> Vec<u8> {
224
        cbor::encode(&self.to_value())
225
    }
226
    pub fn hash(&self) -> KeyId {
227
        hash_object(ObjectType::Identity, &self.encode())
228
    }
229
    pub fn decode(b: &[u8]) -> Result<Identity, ObjectError> {
230
        let v = cbor::decode(b)?;
75167672 Andy McCall 231
        let m = v
232
            .as_map()
233
            .ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?;
1983686f Andy McCall ↶ 234
        check_version(m)?;
235
        let ed = bytes(field(m, 1, "ed25519_pk")?, "ed25519_pk")?;
236
        let ml = bytes(field(m, 2, "mldsa65_pk")?, "mldsa65_pk")?;
237
        if ed.len() != ED25519_PK_LEN || ml.len() != MLDSA65_PK_LEN {
75167672 Andy McCall 238
            return Err(ObjectError::Schema(
239
                "identity: wrong public key length".into(),
240
            ));
1983686f Andy McCall ↶ 241
        }
75167672 Andy McCall 242
        let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> {
243
            if v.is_null() {
244
                Ok(None)
245
            } else {
246
                uint(v, "expires").map(Some)
247
            }
248
        };
249
        let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> {
250
            if v.is_null() {
251
                Ok(None)
252
            } else {
253
                oref(v, "previous").map(Some)
254
            }
255
        };
7ba155f2 Andy McCall ↶ 256
        let delegates = tidy(
257
            array(field(m, 8, "delegates")?, "delegates")?
258
                .iter()
259
                .map(Delegate::from_value)
260
                .collect::<Result<Vec<_>, _>>()?,
261
        );
1983686f Andy McCall ↶ 262
        Ok(Identity {
263
            ed25519_pk: ed.try_into().unwrap(),
264
            mldsa65_pk: ml.to_vec(),
265
            name: text(field(m, 3, "name")?, "name")?.to_owned(),
266
            email: text(field(m, 4, "email")?, "email")?.to_owned(),
267
            created: uint(field(m, 5, "created")?, "created")?,
268
            expires: opt_u(field(m, 6, "expires")?)?,
269
            previous: opt_r(field(m, 7, "previous")?)?,
270
            delegates,
271
        })
272
    }
273
28fc23d7 Andy McCall ↶ 274
    /// Whether the identity is past its expiry at `now_ms`.
275
    pub fn is_expired_at(&self, now_ms: u64) -> bool {
276
        self.expires.is_some_and(|e| now_ms > e)
277
    }
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).
7ba155f2 Andy McCall ↶ 281
    pub fn successor(
282
        &self,
283
        delegates: Vec<Delegate>,
284
        expires: Option<u64>,
285
        created: u64,
286
    ) -> Identity {
75167672 Andy McCall 287
        Identity {
288
            previous: Some(self.hash()),
7ba155f2 Andy McCall ↶ 289
            delegates: tidy(delegates),
75167672 Andy McCall 290
            expires,
291
            created,
292
            ..self.clone()
293
        }
28fc23d7 Andy McCall ↶ 294
    }
295
a39754d0 Andy McCall ↶ 296
    /// Check a [`SecretKey::sign_ed25519`] signature by this identity.
297
    pub fn verify_ed25519(&self, msg: &[u8], sig: &[u8]) -> Result<(), VerifyError> {
298
        let sig = EdSignature::from_slice(sig).map_err(|_| VerifyError::Ed25519)?;
299
        self.ed_key()?
300
            .verify_strict(msg, &sig)
301
            .map_err(|_| VerifyError::Ed25519)
302
    }
303
1983686f Andy McCall ↶ 304
    fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> {
305
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey)
306
    }
307
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> {
75167672 Andy McCall 308
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice())
309
            .map_err(|_| VerifyError::BadKey)?;
1983686f Andy McCall ↶ 310
        Ok(MlVerifyingKey::decode(&enc))
311
    }
312
}
313
314
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315
pub enum Domain {
316
    Change,
317
    Attest,
83e0677b Andy McCall ↶ 318
    Policy,
7aa2dc93 Andy McCall ↶ 319
    /// A server-issued challenge, proving possession of a key to a host
320
    /// (hub spec §5.3, §5.4). Never a stored object.
321
    Challenge,
02e69188 Andy McCall ↶ 322
    /// A certificate an issuer gives a short-lived key (§14.7).
323
    Certificate,
a2387d54 Andy McCall ↶ 324
    /// An author's grant to a certified subject (§14.7).
325
    Grant,
1983686f Andy McCall ↶ 326
}
327
328
impl Domain {
329
    pub fn name(self) -> &'static str {
330
        match self {
331
            Domain::Change => "change",
332
            Domain::Attest => "attest",
83e0677b Andy McCall ↶ 333
            Domain::Policy => "policy",
7aa2dc93 Andy McCall ↶ 334
            Domain::Challenge => "challenge",
02e69188 Andy McCall ↶ 335
            Domain::Certificate => "certificate",
a2387d54 Andy McCall ↶ 336
            Domain::Grant => "grant",
1983686f Andy McCall ↶ 337
        }
338
    }
339
}
340
341
/// `"oxy1 sig " || domain || 0x00 || KeyId || target` (spec §14.3).
342
pub fn signing_message(domain: Domain, key: &KeyId, target: &ObjectRef) -> Vec<u8> {
343
    let mut m = Vec::with_capacity(80);
344
    m.extend_from_slice(b"oxy1 sig ");
345
    m.extend_from_slice(domain.name().as_bytes());
346
    m.push(0);
347
    m.extend_from_slice(&key.0);
348
    m.extend_from_slice(&target.0);
349
    m
350
}
351
352
#[derive(Clone, PartialEq, Eq)]
353
pub struct Signature {
354
    pub key: KeyId,
355
    pub ed: [u8; ED25519_SIG_LEN],
356
    pub ml: Vec<u8>,
357
}
358
359
impl std::fmt::Debug for Signature {
360
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361
        write!(f, "Signature(key {:?})", self.key)
362
    }
363
}
364
365
impl Signature {
366
    pub fn to_value(&self) -> Value {
367
        Value::map([
368
            (0, Value::Bytes(self.key.0.to_vec())),
369
            (1, Value::Uint(ALG_COMPOSITE)),
370
            (2, Value::Bytes(self.ed.to_vec())),
371
            (3, Value::Bytes(self.ml.clone())),
372
        ])
373
    }
374
    pub fn from_value(v: &Value) -> Result<Signature, ObjectError> {
75167672 Andy McCall 375
        let m = v
376
            .as_map()
377
            .ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?;
1983686f Andy McCall ↶ 378
        let alg = uint(field(m, 1, "alg")?, "alg")?;
379
        if alg != ALG_COMPOSITE {
75167672 Andy McCall 380
            return Err(ObjectError::Schema(format!(
381
                "unknown signature algorithm {alg}"
382
            )));
1983686f Andy McCall ↶ 383
        }
384
        let ed = bytes(field(m, 2, "ed")?, "ed")?;
385
        let ml = bytes(field(m, 3, "ml")?, "ml")?;
386
        if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN {
387
            return Err(ObjectError::Schema("signature: wrong length".into()));
388
        }
75167672 Andy McCall 389
        Ok(Signature {
390
            key: oref(field(m, 0, "key")?, "key")?,
391
            ed: ed.try_into().unwrap(),
392
            ml: ml.to_vec(),
393
        })
1983686f Andy McCall ↶ 394
    }
395
    pub fn encode_list(sigs: &[Signature]) -> Vec<u8> {
396
        cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect()))
397
    }
398
    pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> {
399
        let v = cbor::decode(b)?;
75167672 Andy McCall 400
        array(&v, "signatures")?
401
            .iter()
402
            .map(Signature::from_value)
403
            .collect()
1983686f Andy McCall ↶ 404
    }
405
}
406
407
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
408
pub enum VerifyError {
409
    #[error("signature names a different key than the identity")]
410
    KeyMismatch,
411
    #[error("identity holds a malformed public key")]
412
    BadKey,
413
    #[error("Ed25519 signature invalid")]
414
    Ed25519,
415
    #[error("ML-DSA-65 signature invalid")]
416
    MlDsa,
7aa2dc93 Andy McCall ↶ 417
    #[error("a challenge is at least 16 bytes")]
418
    ShortChallenge,
1983686f Andy McCall ↶ 419
}
420
421
/// Verify a composite signature by `identity` over `target` (spec §14.3).
75167672 Andy McCall 422
pub fn verify(
423
    identity: &Identity,
424
    sig: &Signature,
425
    domain: Domain,
426
    target: &ObjectRef,
427
) -> Result<(), VerifyError> {
1983686f Andy McCall ↶ 428
    let key = identity.hash();
429
    if sig.key != key {
430
        return Err(VerifyError::KeyMismatch);
431
    }
432
    let msg = signing_message(domain, &key, target);
433
    let ed_sig = EdSignature::from_bytes(&sig.ed);
75167672 Andy McCall 434
    identity
435
        .ed_key()?
436
        .verify_strict(&msg, &ed_sig)
437
        .map_err(|_| VerifyError::Ed25519)?;
438
    let enc =
439
        EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?;
1983686f Andy McCall ↶ 440
    let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?;
441
    if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) {
442
        return Err(VerifyError::MlDsa);
443
    }
444
    Ok(())
445
}
446
447
/// The seeds of a composite key pair. Stored only in the user's key directory.
448
#[derive(Clone)]
449
pub struct SecretKey {
450
    pub ed_seed: [u8; 32],
451
    pub ml_seed: [u8; 32],
452
}
453
454
impl std::fmt::Debug for SecretKey {
455
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456
        f.write_str("SecretKey(..)")
457
    }
458
}
459
460
impl SecretKey {
461
    pub fn generate() -> SecretKey {
462
        let mut ed_seed = [0u8; 32];
463
        let mut ml_seed = [0u8; 32];
464
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
465
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
466
        SecretKey { ed_seed, ml_seed }
467
    }
468
469
    fn ed(&self) -> EdSigningKey {
470
        EdSigningKey::from_bytes(&self.ed_seed)
471
    }
472
    fn ml(&self) -> MlSigningKey<MlDsa65> {
473
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
474
    }
475
476
    /// The identity object for these keys.
477
    pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity {
478
        Identity {
479
            ed25519_pk: self.ed().verifying_key().to_bytes(),
75167672 Andy McCall 480
            mldsa65_pk: self
481
                .ml()
482
                .expanded_key()
483
                .verifying_key()
484
                .encode()
485
                .as_slice()
486
                .to_vec(),
1983686f Andy McCall ↶ 487
            name: name.into(),
488
            email: email.into(),
489
            created,
490
            expires: None,
491
            previous: None,
492
            delegates: vec![],
493
        }
a39754d0 Andy McCall ↶ 494
    }
495
496
    /// Ed25519 alone over `msg`, for a format that separates its own
497
    /// domain and whose verifiers know only Ed25519 (DSSE: see
498
    /// [`crate::dsse`]). Oxygen's own objects are signed with [`Self::sign`].
499
    pub fn sign_ed25519(&self, msg: &[u8]) -> [u8; 64] {
500
        self.ed().sign(msg).to_bytes()
1983686f Andy McCall ↶ 501
    }
502
503
    pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature {
504
        let msg = signing_message(domain, key, target);
505
        let ed = self.ed().sign(&msg).to_bytes();
75167672 Andy McCall 506
        let ml = self
507
            .ml()
508
            .expanded_key()
509
            .sign_deterministic(&msg, b"")
510
            .expect("empty context is valid")
511
            .encode()
512
            .as_slice()
513
            .to_vec();
1983686f Andy McCall ↶ 514
        Signature { key: *key, ed, ml }
515
    }
516
517
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
518
        cbor::encode(&Value::map([
519
            (0, Value::Uint(FORMAT_VERSION)),
520
            (1, Value::Bytes(key.0.to_vec())),
521
            (2, Value::Bytes(self.ed_seed.to_vec())),
522
            (3, Value::Bytes(self.ml_seed.to_vec())),
523
        ]))
524
    }
525
526
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> {
527
        let v = cbor::decode(b)?;
75167672 Andy McCall 528
        let m = v
529
            .as_map()
530
            .ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?;
1983686f Andy McCall ↶ 531
        check_version(m)?;
532
        let seed = |k: u64| -> Result<[u8; 32], ObjectError> {
75167672 Andy McCall 533
            bytes(field(m, k, "seed")?, "seed")?
534
                .try_into()
535
                .map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into()))
1983686f Andy McCall ↶ 536
        };
75167672 Andy McCall 537
        Ok((
538
            oref(field(m, 1, "key")?, "key")?,
539
            SecretKey {
540
                ed_seed: seed(2)?,
541
                ml_seed: seed(3)?,
542
            },
543
        ))
1983686f Andy McCall ↶ 544
    }
545
}
546
547
#[cfg(test)]
548
mod tests {
549
    use super::*;
550
551
    fn fixed_key() -> SecretKey {
75167672 Andy McCall 552
        SecretKey {
553
            ed_seed: [1; 32],
554
            ml_seed: [2; 32],
555
        }
1983686f Andy McCall ↶ 556
    }
557
558
    #[test]
559
    fn identity_round_trip_and_sizes() {
560
        let sk = fixed_key();
561
        let id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
562
        assert_eq!(id.mldsa65_pk.len(), MLDSA65_PK_LEN);
563
        let d = Identity::decode(&id.encode()).unwrap();
564
        assert_eq!(d, id);
565
        assert_eq!(d.hash(), id.hash());
566
        // Deterministic from seeds.
75167672 Andy McCall 567
        assert_eq!(
568
            fixed_key()
569
                .identity("Ada", "[email protected]", 1_700_000_000_000)
570
                .hash(),
571
            id.hash()
572
        );
1983686f Andy McCall ↶ 573
        let mut other = id.clone();
7ba155f2 Andy McCall ↶ 574
        other.delegates.push(Delegate::full(ObjectRef([9; 32])));
1983686f Andy McCall ↶ 575
        assert_ne!(other.hash(), id.hash());
7ba155f2 Andy McCall ↶ 576
    }
577
578
    #[test]
579
    fn delegates_carry_scope_and_expiry_and_bare_keys_mean_every_scope() {
580
        let sk = fixed_key();
581
        let mut id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
582
        let agent = ObjectRef([7; 32]);
583
        let host = ObjectRef([3; 32]);
584
        id.delegates = vec![
585
            Delegate {
586
                key: agent,
587
                scope: Scope::CHANGES,
588
                expires: Some(2_000),
589
            },
590
            Delegate {
591
                key: host,
592
                scope: Scope::CHANGES | Scope::REFS,
593
                expires: None,
594
            },
595
        ];
596
        let d = Identity::decode(&id.encode()).unwrap();
597
        assert_eq!(d.delegates.len(), 2);
598
        assert!(d.delegates_to(&agent, Scope::CHANGES, 1_000));
599
        assert!(!d.delegates_to(&agent, Scope::CHANGES, 2_001), "expired");
600
        assert!(!d.delegates_to(&agent, Scope::REFS, 1_000));
601
        assert!(!d.delegates_to(&agent, Scope::SEAL, 1_000));
602
        assert!(d.delegates_to(&host, Scope::REFS, u64::MAX));
603
        assert!(!d.delegates_to(&host, Scope::SEAL, 0));
604
        assert!(!d.delegates_to(&ObjectRef([1; 32]), Scope::CHANGES, 0));
605
        assert_eq!(
606
            Scope::parse("changes, refs").unwrap(),
607
            Scope::CHANGES | Scope::REFS
608
        );
609
        assert_eq!(Scope::parse("all").unwrap(), Scope::ALL);
610
        assert_eq!((Scope::CHANGES | Scope::SEAL).to_string(), "changes,seal");
611
        assert!(Scope::parse("push").is_err() && Scope::parse("").is_err());
612
        // An identity written before scopes existed lists bare KeyIds.
613
        let mut v = id.to_value();
614
        if let Value::Map(m) = &mut v {
615
            m.insert(8, Value::Array(vec![Value::Bytes(host.0.to_vec())]));
616
        }
617
        let old = Identity::decode(&cbor::encode(&v)).unwrap();
618
        assert_eq!(old.delegates, vec![Delegate::full(host)]);
619
        assert!(old.delegates_to(&host, Scope::ALL, u64::MAX));
1983686f Andy McCall ↶ 620
    }
621
622
    #[test]
623
    fn sign_and_verify() {
624
        let sk = fixed_key();
625
        let id = sk.identity("Ada", "[email protected]", 1);
626
        let key = id.hash();
627
        let target = ObjectRef([7; 32]);
628
        let sig = sk.sign(Domain::Change, &key, &target);
629
        assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN);
630
        assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(()));
631
        // Different target, different domain: invalid.
75167672 Andy McCall 632
        assert_eq!(
633
            verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])),
634
            Err(VerifyError::Ed25519)
635
        );
636
        assert_eq!(
637
            verify(&id, &sig, Domain::Attest, &target),
638
            Err(VerifyError::Ed25519)
639
        );
1983686f Andy McCall ↶ 640
        // Deterministic signatures.
641
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
642
        // Signature list round trip.
75167672 Andy McCall 643
        let l =
644
            Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
1983686f Andy McCall ↶ 645
        assert_eq!(l, vec![sig]);
646
    }
647
648
    #[test]
649
    fn stripping_or_forging_either_half_fails() {
650
        let sk = fixed_key();
651
        let id = sk.identity("Ada", "a@b", 1);
652
        let key = id.hash();
653
        let target = ObjectRef([7; 32]);
654
        let good = sk.sign(Domain::Change, &key, &target);
655
656
        let mut no_ml = good.clone();
657
        no_ml.ml = vec![0; MLDSA65_SIG_LEN];
75167672 Andy McCall 658
        assert_eq!(
659
            verify(&id, &no_ml, Domain::Change, &target),
660
            Err(VerifyError::MlDsa)
661
        );
1983686f Andy McCall ↶ 662
663
        let mut bad_ed = good.clone();
664
        bad_ed.ed[10] ^= 1;
75167672 Andy McCall 665
        assert_eq!(
666
            verify(&id, &bad_ed, Domain::Change, &target),
667
            Err(VerifyError::Ed25519)
668
        );
1983686f Andy McCall ↶ 669
670
        // A signature made for another KeyId over the same target does not transfer.
75167672 Andy McCall 671
        let other = SecretKey {
672
            ed_seed: [3; 32],
673
            ml_seed: [4; 32],
674
        };
1983686f Andy McCall ↶ 675
        let other_id = other.identity("Bob", "b@b", 1);
676
        let theirs = other.sign(Domain::Change, &other_id.hash(), &target);
75167672 Andy McCall 677
        assert_eq!(
678
            verify(&id, &theirs, Domain::Change, &target),
679
            Err(VerifyError::KeyMismatch)
680
        );
1983686f Andy McCall ↶ 681
        let mut relabelled = theirs.clone();
682
        relabelled.key = key;
75167672 Andy McCall 683
        assert_eq!(
684
            verify(&id, &relabelled, Domain::Change, &target),
685
            Err(VerifyError::Ed25519)
686
        );
1983686f Andy McCall ↶ 687
688
        // Wrong length is rejected at decode time.
689
        let mut short = good.to_value();
690
        if let Value::Map(m) = &mut short {
691
            m.insert(3, Value::Bytes(vec![0; 10]));
692
        }
693
        assert!(Signature::from_value(&short).is_err());
694
    }
695
696
    #[test]
697
    fn secret_key_round_trip() {
698
        let sk = fixed_key();
699
        let key = sk.identity("x", "y", 0).hash();
700
        let (k, d) = SecretKey::decode(&sk.encode(&key)).unwrap();
701
        assert_eq!(k, key);
702
        assert_eq!(d.ed_seed, sk.ed_seed);
703
        assert_eq!(d.ml_seed, sk.ml_seed);
704
        assert_ne!(SecretKey::generate().ed_seed, SecretKey::generate().ed_seed);
705
    }
706
}
7aa2dc93 Andy McCall ↶ 707
708
/// The signing target of a challenge: `BLAKE3-256("oxy1 challenge " || bytes)`,
709
/// so a challenge can never collide with a stored object's reference.
710
pub fn challenge_target(challenge: &[u8]) -> ObjectRef {
711
    let mut h = blake3::Hasher::new();
712
    h.update(b"oxy1 challenge ");
713
    h.update(challenge);
714
    ObjectRef(*h.finalize().as_bytes())
715
}
716
717
impl SecretKey {
718
    /// Sign a host's challenge (at least 16 bytes) with this key.
719
    pub fn sign_challenge(&self, key: &KeyId, challenge: &[u8]) -> Result<Signature, VerifyError> {
720
        if challenge.len() < 16 {
721
            return Err(VerifyError::ShortChallenge);
722
        }
723
        Ok(self.sign(Domain::Challenge, key, &challenge_target(challenge)))
724
    }
725
}
726
727
/// Check a challenge signature against the identity that claims it.
119bbf50 Andy McCall ↶ 728
pub fn verify_challenge(
729
    identity: &Identity,
730
    sig: &Signature,
731
    challenge: &[u8],
732
) -> Result<(), VerifyError> {
7aa2dc93 Andy McCall ↶ 733
    if challenge.len() < 16 {
734
        return Err(VerifyError::ShortChallenge);
735
    }
736
    if sig.key != identity.hash() {
737
        return Err(VerifyError::KeyMismatch);
738
    }
119bbf50 Andy McCall ↶ 739
    verify(
740
        identity,
741
        sig,
742
        Domain::Challenge,
743
        &challenge_target(challenge),
744
    )
7aa2dc93 Andy McCall ↶ 745
}
746
747
#[cfg(test)]
748
mod challenge_tests {
749
    use super::*;
750
751
    #[test]
752
    fn a_challenge_signature_binds_key_and_bytes() {
753
        let sk = SecretKey::generate();
754
        let id = sk.identity("A", "a@example", 1);
755
        let key = id.hash();
756
        let challenge = [7u8; 32];
757
        let sig = sk.sign_challenge(&key, &challenge).unwrap();
758
        assert!(verify_challenge(&id, &sig, &challenge).is_ok());
119bbf50 Andy McCall ↶ 759
        assert!(
760
            verify_challenge(&id, &sig, &[8u8; 32]).is_err(),
761
            "other bytes"
762
        );
7aa2dc93 Andy McCall ↶ 763
        let other = SecretKey::generate().identity("B", "b@example", 1);
119bbf50 Andy McCall ↶ 764
        assert!(
765
            verify_challenge(&other, &sig, &challenge).is_err(),
766
            "other identity"
767
        );
7aa2dc93 Andy McCall ↶ 768
        assert!(sk.sign_challenge(&key, b"short").is_err());
769
        // A challenge target never equals a change signature's target for the same bytes.
770
        assert_ne!(challenge_target(&challenge), ObjectRef(challenge));
771
    }
772
}