OXYGENHUB
# 5f661562b3a4 oxygen / crates / oxy-core / src / identity.rs
18.3 KiB · Rust · last changed oxy receipt export --format slsa: SLSA provenance cosign can check 19 days 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
    /// Check a [`SecretKey::sign_ed25519`] signature by this identity.
140
    pub fn verify_ed25519(&self, msg: &[u8], sig: &[u8]) -> Result<(), VerifyError> {
141
        let sig = EdSignature::from_slice(sig).map_err(|_| VerifyError::Ed25519)?;
142
        self.ed_key()?
143
            .verify_strict(msg, &sig)
144
            .map_err(|_| VerifyError::Ed25519)
145
    }
146
147
    fn ed_key(&self) -> Result<EdVerifyingKey, VerifyError> {
148
        EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| VerifyError::BadKey)
149
    }
150
    fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, VerifyError> {
151
        let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice())
152
            .map_err(|_| VerifyError::BadKey)?;
153
        Ok(MlVerifyingKey::decode(&enc))
154
    }
155
}
156
157
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158
pub enum Domain {
159
    Change,
160
    Attest,
161
    Policy,
162
    /// A server-issued challenge, proving possession of a key to a host
163
    /// (hub spec §5.3, §5.4). Never a stored object.
164
    Challenge,
165
}
166
167
impl Domain {
168
    pub fn name(self) -> &'static str {
169
        match self {
170
            Domain::Change => "change",
171
            Domain::Attest => "attest",
172
            Domain::Policy => "policy",
173
            Domain::Challenge => "challenge",
174
        }
175
    }
176
}
177
178
/// `"oxy1 sig " || domain || 0x00 || KeyId || target` (spec §14.3).
179
pub fn signing_message(domain: Domain, key: &KeyId, target: &ObjectRef) -> Vec<u8> {
180
    let mut m = Vec::with_capacity(80);
181
    m.extend_from_slice(b"oxy1 sig ");
182
    m.extend_from_slice(domain.name().as_bytes());
183
    m.push(0);
184
    m.extend_from_slice(&key.0);
185
    m.extend_from_slice(&target.0);
186
    m
187
}
188
189
#[derive(Clone, PartialEq, Eq)]
190
pub struct Signature {
191
    pub key: KeyId,
192
    pub ed: [u8; ED25519_SIG_LEN],
193
    pub ml: Vec<u8>,
194
}
195
196
impl std::fmt::Debug for Signature {
197
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198
        write!(f, "Signature(key {:?})", self.key)
199
    }
200
}
201
202
impl Signature {
203
    pub fn to_value(&self) -> Value {
204
        Value::map([
205
            (0, Value::Bytes(self.key.0.to_vec())),
206
            (1, Value::Uint(ALG_COMPOSITE)),
207
            (2, Value::Bytes(self.ed.to_vec())),
208
            (3, Value::Bytes(self.ml.clone())),
209
        ])
210
    }
211
    pub fn from_value(v: &Value) -> Result<Signature, ObjectError> {
212
        let m = v
213
            .as_map()
214
            .ok_or_else(|| ObjectError::Schema("signature: expected map".into()))?;
215
        let alg = uint(field(m, 1, "alg")?, "alg")?;
216
        if alg != ALG_COMPOSITE {
217
            return Err(ObjectError::Schema(format!(
218
                "unknown signature algorithm {alg}"
219
            )));
220
        }
221
        let ed = bytes(field(m, 2, "ed")?, "ed")?;
222
        let ml = bytes(field(m, 3, "ml")?, "ml")?;
223
        if ed.len() != ED25519_SIG_LEN || ml.len() != MLDSA65_SIG_LEN {
224
            return Err(ObjectError::Schema("signature: wrong length".into()));
225
        }
226
        Ok(Signature {
227
            key: oref(field(m, 0, "key")?, "key")?,
228
            ed: ed.try_into().unwrap(),
229
            ml: ml.to_vec(),
230
        })
231
    }
232
    pub fn encode_list(sigs: &[Signature]) -> Vec<u8> {
233
        cbor::encode(&Value::Array(sigs.iter().map(|s| s.to_value()).collect()))
234
    }
235
    pub fn decode_list(b: &[u8]) -> Result<Vec<Signature>, ObjectError> {
236
        let v = cbor::decode(b)?;
237
        array(&v, "signatures")?
238
            .iter()
239
            .map(Signature::from_value)
240
            .collect()
241
    }
242
}
243
244
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
245
pub enum VerifyError {
246
    #[error("signature names a different key than the identity")]
247
    KeyMismatch,
248
    #[error("identity holds a malformed public key")]
249
    BadKey,
250
    #[error("Ed25519 signature invalid")]
251
    Ed25519,
252
    #[error("ML-DSA-65 signature invalid")]
253
    MlDsa,
254
    #[error("a challenge is at least 16 bytes")]
255
    ShortChallenge,
256
}
257
258
/// Verify a composite signature by `identity` over `target` (spec §14.3).
259
pub fn verify(
260
    identity: &Identity,
261
    sig: &Signature,
262
    domain: Domain,
263
    target: &ObjectRef,
264
) -> Result<(), VerifyError> {
265
    let key = identity.hash();
266
    if sig.key != key {
267
        return Err(VerifyError::KeyMismatch);
268
    }
269
    let msg = signing_message(domain, &key, target);
270
    let ed_sig = EdSignature::from_bytes(&sig.ed);
271
    identity
272
        .ed_key()?
273
        .verify_strict(&msg, &ed_sig)
274
        .map_err(|_| VerifyError::Ed25519)?;
275
    let enc =
276
        EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| VerifyError::MlDsa)?;
277
    let ml_sig = MlSignature::<MlDsa65>::decode(&enc).ok_or(VerifyError::MlDsa)?;
278
    if !identity.ml_key()?.verify_with_context(&msg, b"", &ml_sig) {
279
        return Err(VerifyError::MlDsa);
280
    }
281
    Ok(())
282
}
283
284
/// The seeds of a composite key pair. Stored only in the user's key directory.
285
#[derive(Clone)]
286
pub struct SecretKey {
287
    pub ed_seed: [u8; 32],
288
    pub ml_seed: [u8; 32],
289
}
290
291
impl std::fmt::Debug for SecretKey {
292
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293
        f.write_str("SecretKey(..)")
294
    }
295
}
296
297
impl SecretKey {
298
    pub fn generate() -> SecretKey {
299
        let mut ed_seed = [0u8; 32];
300
        let mut ml_seed = [0u8; 32];
301
        rand::rngs::OsRng.fill_bytes(&mut ed_seed);
302
        rand::rngs::OsRng.fill_bytes(&mut ml_seed);
303
        SecretKey { ed_seed, ml_seed }
304
    }
305
306
    fn ed(&self) -> EdSigningKey {
307
        EdSigningKey::from_bytes(&self.ed_seed)
308
    }
309
    fn ml(&self) -> MlSigningKey<MlDsa65> {
310
        MlSigningKey::from_seed(&Seed::from(self.ml_seed))
311
    }
312
313
    /// The identity object for these keys.
314
    pub fn identity(&self, name: &str, email: &str, created: u64) -> Identity {
315
        Identity {
316
            ed25519_pk: self.ed().verifying_key().to_bytes(),
317
            mldsa65_pk: self
318
                .ml()
319
                .expanded_key()
320
                .verifying_key()
321
                .encode()
322
                .as_slice()
323
                .to_vec(),
324
            name: name.into(),
325
            email: email.into(),
326
            created,
327
            expires: None,
328
            previous: None,
329
            delegates: vec![],
330
        }
331
    }
332
333
    /// Ed25519 alone over `msg`, for a format that separates its own
334
    /// domain and whose verifiers know only Ed25519 (DSSE: see
335
    /// [`crate::dsse`]). Oxygen's own objects are signed with [`Self::sign`].
336
    pub fn sign_ed25519(&self, msg: &[u8]) -> [u8; 64] {
337
        self.ed().sign(msg).to_bytes()
338
    }
339
340
    pub fn sign(&self, domain: Domain, key: &KeyId, target: &ObjectRef) -> Signature {
341
        let msg = signing_message(domain, key, target);
342
        let ed = self.ed().sign(&msg).to_bytes();
343
        let ml = self
344
            .ml()
345
            .expanded_key()
346
            .sign_deterministic(&msg, b"")
347
            .expect("empty context is valid")
348
            .encode()
349
            .as_slice()
350
            .to_vec();
351
        Signature { key: *key, ed, ml }
352
    }
353
354
    pub fn encode(&self, key: &KeyId) -> Vec<u8> {
355
        cbor::encode(&Value::map([
356
            (0, Value::Uint(FORMAT_VERSION)),
357
            (1, Value::Bytes(key.0.to_vec())),
358
            (2, Value::Bytes(self.ed_seed.to_vec())),
359
            (3, Value::Bytes(self.ml_seed.to_vec())),
360
        ]))
361
    }
362
363
    pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), ObjectError> {
364
        let v = cbor::decode(b)?;
365
        let m = v
366
            .as_map()
367
            .ok_or_else(|| ObjectError::Schema("secret key: expected map".into()))?;
368
        check_version(m)?;
369
        let seed = |k: u64| -> Result<[u8; 32], ObjectError> {
370
            bytes(field(m, k, "seed")?, "seed")?
371
                .try_into()
372
                .map_err(|_| ObjectError::Schema("seed: expected 32 bytes".into()))
373
        };
374
        Ok((
375
            oref(field(m, 1, "key")?, "key")?,
376
            SecretKey {
377
                ed_seed: seed(2)?,
378
                ml_seed: seed(3)?,
379
            },
380
        ))
381
    }
382
}
383
384
#[cfg(test)]
385
mod tests {
386
    use super::*;
387
388
    fn fixed_key() -> SecretKey {
389
        SecretKey {
390
            ed_seed: [1; 32],
391
            ml_seed: [2; 32],
392
        }
393
    }
394
395
    #[test]
396
    fn identity_round_trip_and_sizes() {
397
        let sk = fixed_key();
398
        let id = sk.identity("Ada", "[email protected]", 1_700_000_000_000);
399
        assert_eq!(id.mldsa65_pk.len(), MLDSA65_PK_LEN);
400
        let d = Identity::decode(&id.encode()).unwrap();
401
        assert_eq!(d, id);
402
        assert_eq!(d.hash(), id.hash());
403
        // Deterministic from seeds.
404
        assert_eq!(
405
            fixed_key()
406
                .identity("Ada", "[email protected]", 1_700_000_000_000)
407
                .hash(),
408
            id.hash()
409
        );
410
        let mut other = id.clone();
411
        other.delegates.push(ObjectRef([9; 32]));
412
        assert_ne!(other.hash(), id.hash());
413
    }
414
415
    #[test]
416
    fn sign_and_verify() {
417
        let sk = fixed_key();
418
        let id = sk.identity("Ada", "[email protected]", 1);
419
        let key = id.hash();
420
        let target = ObjectRef([7; 32]);
421
        let sig = sk.sign(Domain::Change, &key, &target);
422
        assert_eq!(sig.ml.len(), MLDSA65_SIG_LEN);
423
        assert_eq!(verify(&id, &sig, Domain::Change, &target), Ok(()));
424
        // Different target, different domain: invalid.
425
        assert_eq!(
426
            verify(&id, &sig, Domain::Change, &ObjectRef([8; 32])),
427
            Err(VerifyError::Ed25519)
428
        );
429
        assert_eq!(
430
            verify(&id, &sig, Domain::Attest, &target),
431
            Err(VerifyError::Ed25519)
432
        );
433
        // Deterministic signatures.
434
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
435
        // Signature list round trip.
436
        let l =
437
            Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
438
        assert_eq!(l, vec![sig]);
439
    }
440
441
    #[test]
442
    fn stripping_or_forging_either_half_fails() {
443
        let sk = fixed_key();
444
        let id = sk.identity("Ada", "a@b", 1);
445
        let key = id.hash();
446
        let target = ObjectRef([7; 32]);
447
        let good = sk.sign(Domain::Change, &key, &target);
448
449
        let mut no_ml = good.clone();
450
        no_ml.ml = vec![0; MLDSA65_SIG_LEN];
451
        assert_eq!(
452
            verify(&id, &no_ml, Domain::Change, &target),
453
            Err(VerifyError::MlDsa)
454
        );
455
456
        let mut bad_ed = good.clone();
457
        bad_ed.ed[10] ^= 1;
458
        assert_eq!(
459
            verify(&id, &bad_ed, Domain::Change, &target),
460
            Err(VerifyError::Ed25519)
461
        );
462
463
        // A signature made for another KeyId over the same target does not transfer.
464
        let other = SecretKey {
465
            ed_seed: [3; 32],
466
            ml_seed: [4; 32],
467
        };
468
        let other_id = other.identity("Bob", "b@b", 1);
469
        let theirs = other.sign(Domain::Change, &other_id.hash(), &target);
470
        assert_eq!(
471
            verify(&id, &theirs, Domain::Change, &target),
472
            Err(VerifyError::KeyMismatch)
473
        );
474
        let mut relabelled = theirs.clone();
475
        relabelled.key = key;
476
        assert_eq!(
477
            verify(&id, &relabelled, Domain::Change, &target),
478
            Err(VerifyError::Ed25519)
479
        );
480
481
        // Wrong length is rejected at decode time.
482
        let mut short = good.to_value();
483
        if let Value::Map(m) = &mut short {
484
            m.insert(3, Value::Bytes(vec![0; 10]));
485
        }
486
        assert!(Signature::from_value(&short).is_err());
487
    }
488
489
    #[test]
490
    fn secret_key_round_trip() {
491
        let sk = fixed_key();
492
        let key = sk.identity("x", "y", 0).hash();
493
        let (k, d) = SecretKey::decode(&sk.encode(&key)).unwrap();
494
        assert_eq!(k, key);
495
        assert_eq!(d.ed_seed, sk.ed_seed);
496
        assert_eq!(d.ml_seed, sk.ml_seed);
497
        assert_ne!(SecretKey::generate().ed_seed, SecretKey::generate().ed_seed);
498
    }
499
}
500
501
/// The signing target of a challenge: `BLAKE3-256("oxy1 challenge " || bytes)`,
502
/// so a challenge can never collide with a stored object's reference.
503
pub fn challenge_target(challenge: &[u8]) -> ObjectRef {
504
    let mut h = blake3::Hasher::new();
505
    h.update(b"oxy1 challenge ");
506
    h.update(challenge);
507
    ObjectRef(*h.finalize().as_bytes())
508
}
509
510
impl SecretKey {
511
    /// Sign a host's challenge (at least 16 bytes) with this key.
512
    pub fn sign_challenge(&self, key: &KeyId, challenge: &[u8]) -> Result<Signature, VerifyError> {
513
        if challenge.len() < 16 {
514
            return Err(VerifyError::ShortChallenge);
515
        }
516
        Ok(self.sign(Domain::Challenge, key, &challenge_target(challenge)))
517
    }
518
}
519
520
/// Check a challenge signature against the identity that claims it.
521
pub fn verify_challenge(
522
    identity: &Identity,
523
    sig: &Signature,
524
    challenge: &[u8],
525
) -> Result<(), VerifyError> {
526
    if challenge.len() < 16 {
527
        return Err(VerifyError::ShortChallenge);
528
    }
529
    if sig.key != identity.hash() {
530
        return Err(VerifyError::KeyMismatch);
531
    }
532
    verify(
533
        identity,
534
        sig,
535
        Domain::Challenge,
536
        &challenge_target(challenge),
537
    )
538
}
539
540
#[cfg(test)]
541
mod challenge_tests {
542
    use super::*;
543
544
    #[test]
545
    fn a_challenge_signature_binds_key_and_bytes() {
546
        let sk = SecretKey::generate();
547
        let id = sk.identity("A", "a@example", 1);
548
        let key = id.hash();
549
        let challenge = [7u8; 32];
550
        let sig = sk.sign_challenge(&key, &challenge).unwrap();
551
        assert!(verify_challenge(&id, &sig, &challenge).is_ok());
552
        assert!(
553
            verify_challenge(&id, &sig, &[8u8; 32]).is_err(),
554
            "other bytes"
555
        );
556
        let other = SecretKey::generate().identity("B", "b@example", 1);
557
        assert!(
558
            verify_challenge(&other, &sig, &challenge).is_err(),
559
            "other identity"
560
        );
561
        assert!(sk.sign_challenge(&key, b"short").is_err());
562
        // A challenge target never equals a change signature's target for the same bytes.
563
        assert_ne!(challenge_target(&challenge), ObjectRef(challenge));
564
    }
565
}