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