OXYGENHUB
# c63fed2e0465 oxygen / crates / oxy-core / src / identity.rs
17.7 KiB · Rust · last changed State is Send + Sync; shared lane layout; log_window 1 month ago source blame history raw
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
#[derive(Clone, Debug, PartialEq, Eq)]
30
pub struct Identity {
31
    pub ed25519_pk: [u8; ED25519_PK_LEN],
32
    pub mldsa65_pk: Vec<u8>,
33
    pub name: String,
34
    pub email: String,
35
    pub created: u64,
36
    pub expires: Option<u64>,
37
    pub previous: Option<KeyId>,
38
    pub delegates: Vec<KeyId>,
39
}
40
41
impl Identity {
42
    pub fn to_value(&self) -> Value {
43
        Value::map([
44
            (0, Value::Uint(FORMAT_VERSION)),
45
            (1, Value::Bytes(self.ed25519_pk.to_vec())),
46
            (2, Value::Bytes(self.mldsa65_pk.clone())),
47
            (3, Value::Text(self.name.clone())),
48
            (4, Value::Text(self.email.clone())),
49
            (5, Value::Uint(self.created)),
50
            (6, self.expires.map(Value::Uint).unwrap_or(Value::Null)),
51
            (
52
                7,
53
                self.previous
54
                    .map(|p| Value::Bytes(p.0.to_vec()))
55
                    .unwrap_or(Value::Null),
56
            ),
57
            (
58
                8,
59
                Value::Array(
60
                    self.delegates
61
                        .iter()
62
                        .map(|d| Value::Bytes(d.0.to_vec()))
63
                        .collect(),
64
                ),
65
            ),
66
        ])
67
    }
68
    pub fn encode(&self) -> Vec<u8> {
69
        cbor::encode(&self.to_value())
70
    }
71
    pub fn hash(&self) -> KeyId {
72
        hash_object(ObjectType::Identity, &self.encode())
73
    }
74
    pub fn decode(b: &[u8]) -> Result<Identity, ObjectError> {
75
        let v = cbor::decode(b)?;
76
        let m = v
77
            .as_map()
78
            .ok_or_else(|| ObjectError::Schema("identity: expected map".into()))?;
79
        check_version(m)?;
80
        let ed = bytes(field(m, 1, "ed25519_pk")?, "ed25519_pk")?;
81
        let ml = bytes(field(m, 2, "mldsa65_pk")?, "mldsa65_pk")?;
82
        if ed.len() != ED25519_PK_LEN || ml.len() != MLDSA65_PK_LEN {
83
            return Err(ObjectError::Schema(
84
                "identity: wrong public key length".into(),
85
            ));
86
        }
87
        let opt_u = |v: &Value| -> Result<Option<u64>, ObjectError> {
88
            if v.is_null() {
89
                Ok(None)
90
            } else {
91
                uint(v, "expires").map(Some)
92
            }
93
        };
94
        let opt_r = |v: &Value| -> Result<Option<KeyId>, ObjectError> {
95
            if v.is_null() {
96
                Ok(None)
97
            } else {
98
                oref(v, "previous").map(Some)
99
            }
100
        };
101
        let mut delegates = array(field(m, 8, "delegates")?, "delegates")?
102
            .iter()
103
            .map(|d| oref(d, "delegate"))
104
            .collect::<Result<Vec<_>, _>>()?;
105
        delegates.sort();
106
        delegates.dedup();
107
        Ok(Identity {
108
            ed25519_pk: ed.try_into().unwrap(),
109
            mldsa65_pk: ml.to_vec(),
110
            name: text(field(m, 3, "name")?, "name")?.to_owned(),
111
            email: text(field(m, 4, "email")?, "email")?.to_owned(),
112
            created: uint(field(m, 5, "created")?, "created")?,
113
            expires: opt_u(field(m, 6, "expires")?)?,
114
            previous: opt_r(field(m, 7, "previous")?)?,
115
            delegates,
116
        })
117
    }
118
119
    /// Whether the identity is past its expiry at `now_ms`.
120
    pub fn is_expired_at(&self, now_ms: u64) -> bool {
121
        self.expires.is_some_and(|e| now_ms > e)
122
    }
123
124
    /// A successor identity with the same keys, `previous` set to this
125
    /// identity's KeyId, and the given delegates and expiry (spec §14.2).
126
    pub fn successor(&self, delegates: Vec<KeyId>, expires: Option<u64>, created: u64) -> Identity {
127
        let mut delegates = delegates;
128
        delegates.sort();
129
        delegates.dedup();
130
        Identity {
131
            previous: Some(self.hash()),
132
            delegates,
133
            expires,
134
            created,
135
            ..self.clone()
136
        }
137
    }
138
139
    fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> {
140
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey)
141
    }
142
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> {
143
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice())
144
            .map_err(|_| VerifyError::BadKey)?;
145
        Ok(MlVerifyingKey::decode(&enc))
146
    }
147
}
148
149
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150
pub enum Domain {
151
    Change,
152
    Attest,
153
    Policy,
154
    /// A server-issued challenge, proving possession of a key to a host
155
    /// (hub spec §5.3, §5.4). Never a stored object.
156
    Challenge,
157
}
158
159
impl Domain {
160
    pub fn name(self) -> &'static str {
161
        match self {
162
            Domain::Change => "change",
163
            Domain::Attest => "attest",
164
            Domain::Policy => "policy",
165
            Domain::Challenge => "challenge",
166
        }
167
    }
168
}
169
170
/// `"oxy1 sig " || domain || 0x00 || KeyId || target` (spec §14.3).
171
pub fn signing_message(domain: Domain, key: &KeyId, target: &ObjectRef) -> Vec<u8> {
172
    let mut m = Vec::with_capacity(80);
173
    m.extend_from_slice(b"oxy1 sig ");
174
    m.extend_from_slice(domain.name().as_bytes());
175
    m.push(0);
176
    m.extend_from_slice(&key.0);
177
    m.extend_from_slice(&target.0);
178
    m
179
}
180
181
#[derive(Clone, PartialEq, Eq)]
182
pub struct Signature {
183
    pub key: KeyId,
184
    pub ed: [u8; ED25519_SIG_LEN],
185
    pub ml: Vec<u8>,
186
}
187
188
impl std::fmt::Debug for Signature {
189
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190
        write!(f, "Signature(key {:?})", self.key)
191
    }
192
}
193
194
impl Signature {
195
    pub fn to_value(&self) -> Value {
196
        Value::map([
197
            (0, Value::Bytes(self.key.0.to_vec())),
198
            (1, Value::Uint(ALG_COMPOSITE)),
199
            (2, Value::Bytes(self.ed.to_vec())),
200
            (3, Value::Bytes(self.ml.clone())),
201
        ])
202
    }
203
    pub fn from_value(v: &Value) -> Result<Signature, ObjectError> {
204
        let m = v
205
            .as_map()
206
            .ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?;
207
        let alg = uint(field(m, 1, "alg")?, "alg")?;
208
        if alg != ALG_COMPOSITE {
209
            return Err(ObjectError::Schema(format!(
210
                "unknown signature algorithm {alg}"
211
            )));
212
        }
213
        let ed = bytes(field(m, 2, "ed")?, "ed")?;
214
        let ml = bytes(field(m, 3, "ml")?, "ml")?;
215
        if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN {
216
            return Err(ObjectError::Schema("signature: wrong length".into()));
217
        }
218
        Ok(Signature {
219
            key: oref(field(m, 0, "key")?, "key")?,
220
            ed: ed.try_into().unwrap(),
221
            ml: ml.to_vec(),
222
        })
223
    }
224
    pub fn encode_list(sigs: &[Signature]) -> Vec<u8> {
225
        cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect()))
226
    }
227
    pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> {
228
        let v = cbor::decode(b)?;
229
        array(&v, "signatures")?
230
            .iter()
231
            .map(Signature::from_value)
232
            .collect()
233
    }
234
}
235
236
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
237
pub enum VerifyError {
238
    #[error("signature names a different key than the identity")]
239
    KeyMismatch,
240
    #[error("identity holds a malformed public key")]
241
    BadKey,
242
    #[error("Ed25519 signature invalid")]
243
    Ed25519,
244
    #[error("ML-DSA-65 signature invalid")]
245
    MlDsa,
246
    #[error("a challenge is at least 16 bytes")]
247
    ShortChallenge,
248
}
249
250
/// Verify a composite signature by `identity` over `target` (spec §14.3).
251
pub fn verify(
252
    identity: &Identity,
253
    sig: &Signature,
254
    domain: Domain,
255
    target: &ObjectRef,
256
) -> Result<(), VerifyError> {
257
    let key = identity.hash();
258
    if sig.key != key {
259
        return Err(VerifyError::KeyMismatch);
260
    }
261
    let msg = signing_message(domain, &key, target);
262
    let ed_sig = EdSignature::from_bytes(&sig.ed);
263
    identity
264
        .ed_key()?
265
        .verify_strict(&msg, &ed_sig)
266
        .map_err(|_| VerifyError::Ed25519)?;
267
    let enc =
268
        EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?;
269
    let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?;
270
    if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) {
271
        return Err(VerifyError::MlDsa);
272
    }
273
    Ok(())
274
}
275
276
/// The seeds of a composite key pair. Stored only in the user's key directory.
277
#[derive(Clone)]
278
pub struct SecretKey {
279
    pub ed_seed: [u8; 32],
280
    pub ml_seed: [u8; 32],
281
}
282
283
impl std::fmt::Debug for SecretKey {
284
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285
        f.write_str("SecretKey(..)")
286
    }
287
}
288
289
impl SecretKey {
290
    pub fn generate() -> SecretKey {
291
        let mut ed_seed = [0u8; 32];
292
        let mut ml_seed = [0u8; 32];
293
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
294
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
295
        SecretKey { ed_seed, ml_seed }
296
    }
297
298
    fn ed(&self) -> EdSigningKey {
299
        EdSigningKey::from_bytes(&self.ed_seed)
300
    }
301
    fn ml(&self) -> MlSigningKey<MlDsa65> {
302
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
303
    }
304
305
    /// The identity object for these keys.
306
    pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity {
307
        Identity {
308
            ed25519_pk: self.ed().verifying_key().to_bytes(),
309
            mldsa65_pk: self
310
                .ml()
311
                .expanded_key()
312
                .verifying_key()
313
                .encode()
314
                .as_slice()
315
                .to_vec(),
316
            name: name.into(),
317
            email: email.into(),
318
            created,
319
            expires: None,
320
            previous: None,
321
            delegates: vec![],
322
        }
323
    }
324
325
    pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature {
326
        let msg = signing_message(domain, key, target);
327
        let ed = self.ed().sign(&msg).to_bytes();
328
        let ml = self
329
            .ml()
330
            .expanded_key()
331
            .sign_deterministic(&msg, b"")
332
            .expect("empty context is valid")
333
            .encode()
334
            .as_slice()
335
            .to_vec();
336
        Signature { key: *key, ed, ml }
337
    }
338
339
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
340
        cbor::encode(&Value::map([
341
            (0, Value::Uint(FORMAT_VERSION)),
342
            (1, Value::Bytes(key.0.to_vec())),
343
            (2, Value::Bytes(self.ed_seed.to_vec())),
344
            (3, Value::Bytes(self.ml_seed.to_vec())),
345
        ]))
346
    }
347
348
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> {
349
        let v = cbor::decode(b)?;
350
        let m = v
351
            .as_map()
352
            .ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?;
353
        check_version(m)?;
354
        let seed = |k: u64| -> Result<[u8; 32], ObjectError> {
355
            bytes(field(m, k, "seed")?, "seed")?
356
                .try_into()
357
                .map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into()))
358
        };
359
        Ok((
360
            oref(field(m, 1, "key")?, "key")?,
361
            SecretKey {
362
                ed_seed: seed(2)?,
363
                ml_seed: seed(3)?,
364
            },
365
        ))
366
    }
367
}
368
369
#[cfg(test)]
370
mod tests {
371
    use super::*;
372
373
    fn fixed_key() -> SecretKey {
374
        SecretKey {
375
            ed_seed: [1; 32],
376
            ml_seed: [2; 32],
377
        }
378
    }
379
380
    #[test]
381
    fn identity_round_trip_and_sizes() {
382
        let sk = fixed_key();
383
        let id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
384
        assert_eq!(id.mldsa65_pk.len(), MLDSA65_PK_LEN);
385
        let d = Identity::decode(&id.encode()).unwrap();
386
        assert_eq!(d, id);
387
        assert_eq!(d.hash(), id.hash());
388
        // Deterministic from seeds.
389
        assert_eq!(
390
            fixed_key()
391
                .identity("Ada", "[email protected]", 1_700_000_000_000)
392
                .hash(),
393
            id.hash()
394
        );
395
        let mut other = id.clone();
396
        other.delegates.push(ObjectRef([9; 32]));
397
        assert_ne!(other.hash(), id.hash());
398
    }
399
400
    #[test]
401
    fn sign_and_verify() {
402
        let sk = fixed_key();
403
        let id = sk.identity("Ada", "[email protected]", 1);
404
        let key = id.hash();
405
        let target = ObjectRef([7; 32]);
406
        let sig = sk.sign(Domain::Change, &key, &target);
407
        assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN);
408
        assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(()));
409
        // Different target, different domain: invalid.
410
        assert_eq!(
411
            verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])),
412
            Err(VerifyError::Ed25519)
413
        );
414
        assert_eq!(
415
            verify(&id, &sig, Domain::Attest, &target),
416
            Err(VerifyError::Ed25519)
417
        );
418
        // Deterministic signatures.
419
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
420
        // Signature list round trip.
421
        let l =
422
            Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
423
        assert_eq!(l, vec![sig]);
424
    }
425
426
    #[test]
427
    fn stripping_or_forging_either_half_fails() {
428
        let sk = fixed_key();
429
        let id = sk.identity("Ada", "a@b", 1);
430
        let key = id.hash();
431
        let target = ObjectRef([7; 32]);
432
        let good = sk.sign(Domain::Change, &key, &target);
433
434
        let mut no_ml = good.clone();
435
        no_ml.ml = vec![0; MLDSA65_SIG_LEN];
436
        assert_eq!(
437
            verify(&id, &no_ml, Domain::Change, &target),
438
            Err(VerifyError::MlDsa)
439
        );
440
441
        let mut bad_ed = good.clone();
442
        bad_ed.ed[10] ^= 1;
443
        assert_eq!(
444
            verify(&id, &bad_ed, Domain::Change, &target),
445
            Err(VerifyError::Ed25519)
446
        );
447
448
        // A signature made for another KeyId over the same target does not transfer.
449
        let other = SecretKey {
450
            ed_seed: [3; 32],
451
            ml_seed: [4; 32],
452
        };
453
        let other_id = other.identity("Bob", "b@b", 1);
454
        let theirs = other.sign(Domain::Change, &other_id.hash(), &target);
455
        assert_eq!(
456
            verify(&id, &theirs, Domain::Change, &target),
457
            Err(VerifyError::KeyMismatch)
458
        );
459
        let mut relabelled = theirs.clone();
460
        relabelled.key = key;
461
        assert_eq!(
462
            verify(&id, &relabelled, Domain::Change, &target),
463
            Err(VerifyError::Ed25519)
464
        );
465
466
        // Wrong length is rejected at decode time.
467
        let mut short = good.to_value();
468
        if let Value::Map(m) = &mut short {
469
            m.insert(3, Value::Bytes(vec![0; 10]));
470
        }
471
        assert!(Signature::from_value(&short).is_err());
472
    }
473
474
    #[test]
475
    fn secret_key_round_trip() {
476
        let sk = fixed_key();
477
        let key = sk.identity("x", "y", 0).hash();
478
        let (k, d) = SecretKey::decode(&sk.encode(&key)).unwrap();
479
        assert_eq!(k, key);
480
        assert_eq!(d.ed_seed, sk.ed_seed);
481
        assert_eq!(d.ml_seed, sk.ml_seed);
482
        assert_ne!(SecretKey::generate().ed_seed, SecretKey::generate().ed_seed);
483
    }
484
}
485
486
/// The signing target of a challenge: `BLAKE3-256("oxy1 challenge " || bytes)`,
487
/// so a challenge can never collide with a stored object's reference.
488
pub fn challenge_target(challenge: &[u8]) -> ObjectRef {
489
    let mut h = blake3::Hasher::new();
490
    h.update(b"oxy1 challenge ");
491
    h.update(challenge);
492
    ObjectRef(*h.finalize().as_bytes())
493
}
494
495
impl SecretKey {
496
    /// Sign a host's challenge (at least 16 bytes) with this key.
497
    pub fn sign_challenge(&self, key: &KeyId, challenge: &[u8]) -> Result<Signature, VerifyError> {
498
        if challenge.len() < 16 {
499
            return Err(VerifyError::ShortChallenge);
500
        }
501
        Ok(self.sign(Domain::Challenge, key, &challenge_target(challenge)))
502
    }
503
}
504
505
/// Check a challenge signature against the identity that claims it.
506
pub fn verify_challenge(
507
    identity: &Identity,
508
    sig: &Signature,
509
    challenge: &[u8],
510
) -> Result<(), VerifyError> {
511
    if challenge.len() < 16 {
512
        return Err(VerifyError::ShortChallenge);
513
    }
514
    if sig.key != identity.hash() {
515
        return Err(VerifyError::KeyMismatch);
516
    }
517
    verify(
518
        identity,
519
        sig,
520
        Domain::Challenge,
521
        &challenge_target(challenge),
522
    )
523
}
524
525
#[cfg(test)]
526
mod challenge_tests {
527
    use super::*;
528
529
    #[test]
530
    fn a_challenge_signature_binds_key_and_bytes() {
531
        let sk = SecretKey::generate();
532
        let id = sk.identity("A", "a@example", 1);
533
        let key = id.hash();
534
        let challenge = [7u8; 32];
535
        let sig = sk.sign_challenge(&key, &challenge).unwrap();
536
        assert!(verify_challenge(&id, &sig, &challenge).is_ok());
537
        assert!(
538
            verify_challenge(&id, &sig, &[8u8; 32]).is_err(),
539
            "other bytes"
540
        );
541
        let other = SecretKey::generate().identity("B", "b@example", 1);
542
        assert!(
543
            verify_challenge(&other, &sig, &challenge).is_err(),
544
            "other identity"
545
        );
546
        assert!(sk.sign_challenge(&key, b"short").is_err());
547
        // A challenge target never equals a change signature's target for the same bytes.
548
        assert_ne!(challenge_target(&challenge), ObjectRef(challenge));
549
    }
550
}