| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | | use crate::cbor::{self, Value}; |
| 10 | | use ed25519_dalek::ed25519::signature::Signer as _; |
| 11 | | use ed25519_dalek::{Signature as EdSignature, SigningKey as EdSigningKey, VerifyingKey as EdVerifyingKey}; |
| 12 | | use ml_dsa::{ |
| 13 | | EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed, Signature as MlSignature, SigningKey as MlSigningKey, |
| 14 | | VerifyingKey as MlVerifyingKey, |
| 15 | | }; |
| 16 | | use rand::RngCore; |
| 17 | |
|
| 18 | | const VERSION: u64 = 1; |
| 19 | |
|
| 20 | |
|
| 21 | | #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] |
| 22 | | pub struct KeyId(pub [u8; 32]); |
| 23 | |
|
| 24 | | impl KeyId { |
| 25 | 2154× | pub fn to_hex(&self) -> String { |
| 26 | 2154× | hex::encode(self.0) |
| 27 | 2154× | } |
| 28 | 344× | pub fn from_hex(s: &str) -> Option<KeyId> { |
| 29 | 344× | hex::decode(s.trim()).ok()?.try_into().ok().map(KeyId) |
| 30 | 344× | } |
| 31 | | |
| 32 | 89× | pub fn short(&self, n: usize) -> String { |
| 33 | 89× | let h = self.to_hex(); |
| 34 | 89× | h[..n.min(h.len())].to_string() |
| 35 | 89× | } |
| 36 | | } |
| 37 | |
|
| 38 | | impl std::fmt::Debug for KeyId { |
| 39 | 0× | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 40 | 0× | write!(f, "KeyId({})", self.short(12)) |
| 41 | 0× | } |
| 42 | | } |
| 43 | |
|
| 44 | | impl std::fmt::Display for KeyId { |
| 45 | 0× | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 46 | 0× | f.write_str(&self.to_hex()) |
| 47 | 0× | } |
| 48 | | } |
| 49 | |
|
| 50 | |
|
| 51 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 52 | | pub struct Delegate { |
| 53 | | pub key: KeyId, |
| 54 | | pub expires: Option<u64>, |
| 55 | | } |
| 56 | |
|
| 57 | | impl Delegate { |
| 58 | 385× | pub fn allows(&self, at_ms: u64) -> bool { |
| 59 | 385× | self.expires.is_none_or(|e| at_ms < e) |
| 60 | 385× | } |
| 61 | | } |
| 62 | |
|
| 63 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 64 | | pub struct Identity { |
| 65 | | pub ed25519_pk: [u8; 32], |
| 66 | | pub mldsa65_pk: Vec<u8>, |
| 67 | | pub name: String, |
| 68 | | pub contact: String, |
| 69 | | pub created: u64, |
| 70 | | pub expires: Option<u64>, |
| 71 | | pub previous: Option<KeyId>, |
| 72 | | pub delegates: Vec<Delegate>, |
| 73 | | } |
| 74 | |
|
| 75 | |
|
| 76 | | #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] |
| 77 | | pub enum Error { |
| 78 | | #[error(transparent)] |
| 79 | | Cbor(#[from] cbor::Error), |
| 80 | | #[error("{0}")] |
| 81 | | Shape(String), |
| 82 | | #[error("the signature is by another key")] |
| 83 | | KeyMismatch, |
| 84 | | #[error("a public key that does not decode")] |
| 85 | | BadKey, |
| 86 | | #[error("the Ed25519 half does not verify")] |
| 87 | | Ed25519, |
| 88 | | #[error("the ML-DSA-65 half does not verify")] |
| 89 | | MlDsa, |
| 90 | | } |
| 91 | |
|
| 92 | 0× | fn shape(s: &str) -> Error { |
| 93 | 0× | Error::Shape(s.into()) |
| 94 | 0× | } |
| 95 | |
|
| 96 | 10258× | fn bytes_of(v: Option<&Value>, what: &str) -> Result<Vec<u8>, Error> { |
| 97 | 10258× | v.and_then(Value::as_bytes).map(|b| b.to_vec()).ok_or_else(|| shape(what)) |
| 98 | 10258× | } |
| 99 | |
|
| 100 | 3612× | fn key_of(v: Option<&Value>, what: &str) -> Result<KeyId, Error> { |
| 101 | 3612× | bytes_of(v, what)?.try_into().map(KeyId).map_err(|_| shape(what)) |
| 102 | 3612× | } |
| 103 | |
|
| 104 | 2769× | fn opt_uint(v: Option<&Value>, what: &str) -> Result<Option<u64>, Error> { |
| 105 | 2769× | match v { |
| 106 | 2744× | None | Some(Value::Null) => Ok(None), |
| 107 | 25× | Some(Value::Uint(u)) => Ok(Some(*u)), |
| 108 | 0× | _ => Err(shape(what)), |
| 109 | | } |
| 110 | 2769× | } |
| 111 | |
|
| 112 | 26636× | fn opt_val<T>(o: Option<T>, f: impl Fn(T) -> Value) -> Value { |
| 113 | 26636× | o.map(f).unwrap_or(Value::Null) |
| 114 | 26636× | } |
| 115 | |
|
| 116 | | impl Identity { |
| 117 | 9149× | pub fn to_value(&self) -> Value { |
| 118 | 9149× | Value::map([ |
| 119 | 9149× | (0, Value::Uint(VERSION)), |
| 120 | 9149× | (1, Value::Bytes(self.ed25519_pk.to_vec())), |
| 121 | 9149× | (2, Value::Bytes(self.mldsa65_pk.clone())), |
| 122 | 9149× | (3, Value::Text(self.name.clone())), |
| 123 | 9149× | (4, Value::Text(self.contact.clone())), |
| 124 | 9149× | (5, Value::Uint(self.created)), |
| 125 | 9149× | (6, opt_val(self.expires, Value::Uint)), |
| 126 | 9149× | (7, opt_val(self.previous, |k| Value::Bytes(k.0.to_vec()))), |
| 127 | | ( |
| 128 | | 8, |
| 129 | | Value::Array( |
| 130 | 9149× | self.delegates |
| 131 | 9149× | .iter() |
| 132 | 9149× | .map(|d| Value::map([(0, Value::Bytes(d.key.0.to_vec())), (1, opt_val(d.expires, Value::Uint))])) |
| 133 | 9149× | .collect(), |
| 134 | | ), |
| 135 | | ), |
| 136 | | ]) |
| 137 | 9149× | } |
| 138 | |
|
| 139 | 9149× | pub fn encode(&self) -> Vec<u8> { |
| 140 | 9149× | cbor::encode(&self.to_value()) |
| 141 | 9149× | } |
| 142 | |
|
| 143 | 6895× | pub fn hash(&self) -> KeyId { |
| 144 | 6895× | let mut h = blake3::Hasher::new(); |
| 145 | 6895× | h.update(b"spectra1 identity "); |
| 146 | 6895× | h.update(&self.encode()); |
| 147 | 6895× | KeyId(*h.finalize().as_bytes()) |
| 148 | 6895× | } |
| 149 | |
|
| 150 | 1607× | pub fn decode(b: &[u8]) -> Result<Identity, Error> { |
| 151 | 1607× | let v = cbor::decode(b)?; |
| 152 | 1607× | let m = v.as_map().ok_or_else(|| shape("an identity is a map"))?; |
| 153 | 1607× | if m.get(&0).and_then(Value::as_uint) != Some(VERSION) { |
| 154 | 0× | return Err(shape("an identity of an unknown version")); |
| 155 | 1607× | } |
| 156 | 3214× | let text = |k: u64, what: &str| m.get(&k).and_then(Value::as_text).map(String::from).ok_or_else(|| shape(what)); |
| 157 | 1607× | let delegates = m |
| 158 | 1607× | .get(&8) |
| 159 | 1607× | .and_then(Value::as_array) |
| 160 | 1607× | .ok_or_else(|| shape("delegates"))? |
| 161 | 1607× | .iter() |
| 162 | 1607× | .map(|d| { |
| 163 | 1162× | let dm = d.as_map().ok_or_else(|| shape("a delegate is a map"))?; |
| 164 | 1162× | Ok(Delegate { key: key_of(dm.get(&0), "a delegate's key")?, expires: opt_uint(dm.get(&1), "a delegate's expiry")? }) |
| 165 | 1162× | }) |
| 166 | 1607× | .collect::<Result<Vec<_>, Error>>()?; |
| 167 | 1607× | let id = Identity { |
| 168 | 1607× | ed25519_pk: bytes_of(m.get(&1), "the Ed25519 key")?.try_into().map_err(|_| shape("the Ed25519 key"))?, |
| 169 | 1607× | mldsa65_pk: bytes_of(m.get(&2), "the ML-DSA-65 key")?, |
| 170 | 1607× | name: text(3, "the name")?, |
| 171 | 1607× | contact: text(4, "the contact")?, |
| 172 | 1607× | created: m.get(&5).and_then(Value::as_uint).ok_or_else(|| shape("the creation time"))?, |
| 173 | 1607× | expires: opt_uint(m.get(&6), "the expiry")?, |
| 174 | 1607× | previous: match m.get(&7) { |
| 175 | 873× | None | Some(Value::Null) => None, |
| 176 | 734× | v => Some(key_of(v, "the previous identity")?), |
| 177 | | }, |
| 178 | 1607× | delegates, |
| 179 | | }; |
| 180 | | |
| 181 | 1607× | if id.encode() != b { |
| 182 | 0× | return Err(shape("an identity not in Spectra's own encoding")); |
| 183 | 1607× | } |
| 184 | 1607× | Ok(id) |
| 185 | 1607× | } |
| 186 | |
|
| 187 | 473× | pub fn delegate(&self, key: &KeyId) -> Option<&Delegate> { |
| 188 | 942× | self.delegates.iter().find(|d| &d.key == key) |
| 189 | 473× | } |
| 190 | |
|
| 191 | | |
| 192 | 394× | pub fn delegates_to(&self, key: &KeyId, at_ms: u64) -> bool { |
| 193 | 394× | !self.is_expired_at(at_ms) && self.delegate(key).is_some_and(|d| d.allows(at_ms)) |
| 194 | 394× | } |
| 195 | |
|
| 196 | 394× | pub fn is_expired_at(&self, at_ms: u64) -> bool { |
| 197 | 394× | self.expires.is_some_and(|e| at_ms >= e) |
| 198 | 394× | } |
| 199 | |
|
| 200 | | |
| 201 | 76× | pub fn successor(&self, delegates: Vec<Delegate>, expires: Option<u64>, created: u64) -> Identity { |
| 202 | 76× | Identity { delegates, expires, created, previous: Some(self.hash()), ..self.clone() } |
| 203 | 76× | } |
| 204 | |
|
| 205 | 1288× | fn ed_key(&self) -> Result<EdVerifyingKey, Error> { |
| 206 | 1288× | EdVerifyingKey::from_bytes(&self.ed25519_pk).map_err(|_| Error::BadKey) |
| 207 | 1288× | } |
| 208 | |
|
| 209 | 1285× | fn ml_key(&self) -> Result<MlVerifyingKey<MlDsa65>, Error> { |
| 210 | 1285× | let enc = EncodedVerifyingKey::<MlDsa65>::try_from(self.mldsa65_pk.as_slice()).map_err(|_| Error::BadKey)?; |
| 211 | 1285× | Ok(MlVerifyingKey::decode(&enc)) |
| 212 | 1285× | } |
| 213 | | } |
| 214 | |
|
| 215 | |
|
| 216 | |
|
| 217 | 1841× | fn target(key: &KeyId, message: &[u8]) -> [u8; 32] { |
| 218 | 1841× | let mut h = blake3::Hasher::new(); |
| 219 | 1841× | h.update(b"spectra1 sign "); |
| 220 | 1841× | h.update(&key.0); |
| 221 | 1841× | h.update(message); |
| 222 | 1841× | *h.finalize().as_bytes() |
| 223 | 1841× | } |
| 224 | |
|
| 225 | | #[derive(Clone, PartialEq, Eq)] |
| 226 | | pub struct Signature { |
| 227 | | pub key: KeyId, |
| 228 | | pub ed: [u8; 64], |
| 229 | | pub ml: Vec<u8>, |
| 230 | | } |
| 231 | |
|
| 232 | | impl std::fmt::Debug for Signature { |
| 233 | 0× | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 234 | 0× | write!(f, "Signature({:?})", self.key) |
| 235 | 0× | } |
| 236 | | } |
| 237 | |
|
| 238 | | impl Signature { |
| 239 | 679× | pub fn to_value(&self) -> Value { |
| 240 | 679× | Value::map([(0, Value::Bytes(self.key.0.to_vec())), (1, Value::Bytes(self.ed.to_vec())), (2, Value::Bytes(self.ml.clone()))]) |
| 241 | 679× | } |
| 242 | |
|
| 243 | 1282× | pub fn from_value(v: &Value) -> Result<Signature, Error> { |
| 244 | 1282× | let m = v.as_map().ok_or_else(|| shape("a signature is a map"))?; |
| 245 | | Ok(Signature { |
| 246 | 1282× | key: key_of(m.get(&0), "the signature's key")?, |
| 247 | 1282× | ed: bytes_of(m.get(&1), "the Ed25519 signature")?.try_into().map_err(|_| shape("the Ed25519 signature"))?, |
| 248 | 1282× | ml: bytes_of(m.get(&2), "the ML-DSA-65 signature")?, |
| 249 | | }) |
| 250 | 1282× | } |
| 251 | |
|
| 252 | 1× | pub fn encode(&self) -> Vec<u8> { |
| 253 | 1× | cbor::encode(&self.to_value()) |
| 254 | 1× | } |
| 255 | |
|
| 256 | 1× | pub fn decode(b: &[u8]) -> Result<Signature, Error> { |
| 257 | 1× | Signature::from_value(&cbor::decode(b)?) |
| 258 | 1× | } |
| 259 | | } |
| 260 | |
|
| 261 | |
|
| 262 | 1291× | pub fn verify(identity: &Identity, sig: &Signature, message: &[u8]) -> Result<(), Error> { |
| 263 | 1291× | let key = identity.hash(); |
| 264 | 1291× | if sig.key != key { |
| 265 | 3× | return Err(Error::KeyMismatch); |
| 266 | 1288× | } |
| 267 | 1288× | let t = target(&key, message); |
| 268 | 1288× | identity.ed_key()?.verify_strict(&t, &EdSignature::from_bytes(&sig.ed)).map_err(|_| Error::Ed25519)?; |
| 269 | 1285× | let enc = EncodedSignature::<MlDsa65>::try_from(sig.ml.as_slice()).map_err(|_| Error::MlDsa)?; |
| 270 | 1285× | let ml = MlSignature::<MlDsa65>::decode(&enc).ok_or(Error::MlDsa)?; |
| 271 | 1285× | if !identity.ml_key()?.verify_with_context(&t, b"", &ml) { |
| 272 | 1× | return Err(Error::MlDsa); |
| 273 | 1284× | } |
| 274 | 1284× | Ok(()) |
| 275 | 1291× | } |
| 276 | |
|
| 277 | |
|
| 278 | | #[derive(Clone)] |
| 279 | | pub struct SecretKey { |
| 280 | | ed_seed: [u8; 32], |
| 281 | | ml_seed: [u8; 32], |
| 282 | | } |
| 283 | |
|
| 284 | | impl std::fmt::Debug for SecretKey { |
| 285 | 0× | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 286 | 0× | f.write_str("SecretKey(..)") |
| 287 | 0× | } |
| 288 | | } |
| 289 | |
|
| 290 | | impl SecretKey { |
| 291 | 151× | pub fn generate() -> SecretKey { |
| 292 | 151× | let (mut ed_seed, mut ml_seed) = ([0u8; 32], [0u8; 32]); |
| 293 | 151× | rand::rngs::OsRng.fill_bytes(&mut ed_seed); |
| 294 | 151× | rand::rngs::OsRng.fill_bytes(&mut ml_seed); |
| 295 | 151× | SecretKey { ed_seed, ml_seed } |
| 296 | 151× | } |
| 297 | |
|
| 298 | 1139× | fn ed(&self) -> EdSigningKey { |
| 299 | 1139× | EdSigningKey::from_bytes(&self.ed_seed) |
| 300 | 1139× | } |
| 301 | |
|
| 302 | 1139× | fn ml(&self) -> MlSigningKey<MlDsa65> { |
| 303 | 1139× | MlSigningKey::from_seed(&Seed::from(self.ml_seed)) |
| 304 | 1139× | } |
| 305 | |
|
| 306 | | |
| 307 | 586× | pub fn identity(&self, name: &str, contact: &str, created: u64) -> Identity { |
| 308 | 586× | Identity { |
| 309 | 586× | ed25519_pk: self.ed().verifying_key().to_bytes(), |
| 310 | 586× | mldsa65_pk: self.ml().expanded_key().verifying_key().encode().as_slice().to_vec(), |
| 311 | 586× | name: name.into(), |
| 312 | 586× | contact: contact.into(), |
| 313 | 586× | created, |
| 314 | 586× | expires: None, |
| 315 | 586× | previous: None, |
| 316 | 586× | delegates: vec![], |
| 317 | 586× | } |
| 318 | 586× | } |
| 319 | |
|
| 320 | | |
| 321 | 553× | pub fn sign(&self, key: &KeyId, message: &[u8]) -> Signature { |
| 322 | 553× | let t = target(key, message); |
| 323 | 553× | let ed = self.ed().sign(&t).to_bytes(); |
| 324 | 553× | let ml = self.ml().expanded_key().sign_deterministic(&t, b"").expect("an empty context is valid").encode().as_slice().to_vec(); |
| 325 | 553× | Signature { key: *key, ed, ml } |
| 326 | 553× | } |
| 327 | |
|
| 328 | | |
| 329 | 435× | pub fn matches(&self, identity: &Identity) -> bool { |
| 330 | 435× | let mine = self.identity("", "", 0); |
| 331 | 435× | mine.ed25519_pk == identity.ed25519_pk && mine.mldsa65_pk == identity.mldsa65_pk |
| 332 | 435× | } |
| 333 | |
|
| 334 | 195× | pub fn encode(&self, key: &KeyId) -> Vec<u8> { |
| 335 | 195× | cbor::encode(&Value::map([ |
| 336 | 195× | (0, Value::Uint(VERSION)), |
| 337 | 195× | (1, Value::Bytes(key.0.to_vec())), |
| 338 | 195× | (2, Value::Bytes(self.ed_seed.to_vec())), |
| 339 | 195× | (3, Value::Bytes(self.ml_seed.to_vec())), |
| 340 | 195× | ])) |
| 341 | 195× | } |
| 342 | |
|
| 343 | 434× | pub fn decode(b: &[u8]) -> Result<(KeyId, SecretKey), Error> { |
| 344 | 434× | let v = cbor::decode(b)?; |
| 345 | 434× | let m = v.as_map().ok_or_else(|| shape("a secret key is a map"))?; |
| 346 | 434× | if m.get(&0).and_then(Value::as_uint) != Some(VERSION) { |
| 347 | 0× | return Err(shape("a secret key of an unknown version")); |
| 348 | 434× | } |
| 349 | 868× | let seed = |k: u64| -> Result<[u8; 32], Error> { bytes_of(m.get(&k), "a seed")?.try_into().map_err(|_| shape("a seed")) }; |
| 350 | 434× | Ok((key_of(m.get(&1), "the secret's key")?, SecretKey { ed_seed: seed(2)?, ml_seed: seed(3)? })) |
| 351 | 434× | } |
| 352 | | } |
| 353 | |
|
| 354 | | #[cfg(test)] |
| 355 | | mod tests { |
| 356 | | use super::*; |
| 357 | |
|
| 358 | | #[test] |
| 359 | 1× | fn a_signature_binds_the_key_and_the_message_and_needs_both_halves() { |
| 360 | 1× | let sk = SecretKey::generate(); |
| 361 | 1× | let id = sk.identity("Andy", "a@example", 1); |
| 362 | 1× | let key = id.hash(); |
| 363 | 1× | let sig = sk.sign(&key, b"spectra1 test hello"); |
| 364 | 1× | verify(&id, &sig, b"spectra1 test hello").unwrap(); |
| 365 | 1× | assert_eq!(verify(&id, &sig, b"spectra1 test other"), Err(Error::Ed25519)); |
| 366 | 1× | let other = SecretKey::generate().identity("B", "", 1); |
| 367 | 1× | assert_eq!(verify(&other, &sig, b"spectra1 test hello"), Err(Error::KeyMismatch)); |
| 368 | 1× | let mut half = sig.clone(); |
| 369 | 1× | half.ml[0] ^= 1; |
| 370 | 1× | assert!(verify(&id, &half, b"spectra1 test hello").is_err(), "a broken ML-DSA half fails"); |
| 371 | 1× | assert_eq!(Signature::decode(&sig.encode()).unwrap(), sig); |
| 372 | 1× | } |
| 373 | |
|
| 374 | | #[test] |
| 375 | 1× | fn identities_round_trip_and_successors_chain() { |
| 376 | 1× | let sk = SecretKey::generate(); |
| 377 | 1× | let id = sk.identity("Andy", "", 5); |
| 378 | 1× | assert_eq!(Identity::decode(&id.encode()).unwrap(), id); |
| 379 | 1× | let agent = SecretKey::generate().identity("claude", "", 5).hash(); |
| 380 | 1× | let next = id.successor(vec![Delegate { key: agent, expires: Some(100) }], None, 6); |
| 381 | 1× | assert_eq!(next.previous, Some(id.hash())); |
| 382 | 1× | assert_ne!(next.hash(), id.hash()); |
| 383 | 1× | assert!(next.delegates_to(&agent, 99)); |
| 384 | 1× | assert!(!next.delegates_to(&agent, 100), "expired"); |
| 385 | 1× | assert!(!id.delegates_to(&agent, 1)); |
| 386 | 1× | assert!(sk.matches(&next)); |
| 387 | 1× | let (k, back) = SecretKey::decode(&sk.encode(&next.hash())).unwrap(); |
| 388 | 1× | assert_eq!(k, next.hash()); |
| 389 | 1× | assert!(back.matches(&id)); |
| 390 | 1× | } |
| 391 | | } |