1use std::fmt;
9use std::path::PathBuf;
10
11use aws_lc_rs::encoding::AsDer;
12use aws_lc_rs::rsa::KeyPair as RsaKeyPair;
13use aws_lc_rs::signature::KeyPair as _;
14use macula_mldsa::{PrivateKey, Zeroizing, ML_DSA_87};
15
16use super::{der, verify, KeyError, NodeKey, Purpose, RsaHalf};
17use crate::keystore::{KeyStore, KeyStoreError};
18use crate::profile::Profile;
19
20const MAGIC: &[u8] = b"macula-node-key-seed-v1\0";
24
25const STORE_MAGIC: &[u8] = b"macula-node-key-private-v1\0";
30
31const LAYOUT_CAPACITY: usize = 8 * 1024;
33
34#[derive(Clone, Copy, PartialEq, Eq)]
37enum Form {
38 File,
39 Store,
40}
41
42const TAG_MLDSA_SEED: u8 = 1;
43const TAG_RSA_PSS: u8 = 2;
44#[derive(Debug)]
46pub enum KeyFileError {
47 Io(std::io::Error),
49 NotRegular,
52 Owner,
54 Permissions,
56 TooLarge,
58 BadKeyFile,
60 WrongPurpose(Purpose),
62 WrongProfile(Profile),
64 WrongAlgorithms,
66 WrongKeySize,
68 PrivateKeyInvalid,
70 PublicKeyMismatch,
72 RoundTripFailed,
74 KeyStore(KeyStoreError),
76 Generate(KeyError),
78 NoKeyFile,
84 KeptInTheKeyFileForm,
87 IdentityName(String),
90 OldKey {
93 from: PathBuf,
94 reason: Box<KeyFileError>,
95 },
96 OldKeyPlaceTaken { from: PathBuf, to: PathBuf },
99 StoredKey {
102 path: PathBuf,
103 reason: Box<KeyFileError>,
104 },
105}
106
107impl fmt::Display for KeyFileError {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 KeyFileError::Io(e) => write!(f, "key file: {e}"),
111 KeyFileError::NotRegular => f.write_str("the key file is not a regular file"),
112 KeyFileError::Owner => f.write_str("the key file is owned by another user"),
113 KeyFileError::Permissions => {
114 f.write_str("the key file can be read by its group or others")
115 }
116 KeyFileError::TooLarge => f.write_str("the key file is longer than 64 KiB"),
117 KeyFileError::BadKeyFile => f.write_str("not a key file in the seed form"),
118 KeyFileError::WrongPurpose(p) => write!(f, "the key file holds a key for {p}"),
119 KeyFileError::WrongProfile(p) => write!(f, "the key file holds a key for {p}"),
120 KeyFileError::WrongAlgorithms => f.write_str("the key's halves do not fit its profile"),
121 KeyFileError::WrongKeySize => {
122 f.write_str("the RSA-PSS half is not a 4096-bit key with exponent 65537")
123 }
124 KeyFileError::PrivateKeyInvalid => {
125 f.write_str("the key file's private key is not valid")
126 }
127 KeyFileError::PublicKeyMismatch => {
128 f.write_str("the stored public key is not the one its private key derives")
129 }
130 KeyFileError::RoundTripFailed => f.write_str("the key does not sign and verify"),
131 KeyFileError::KeyStore(e) => write!(f, "key store: {e}"),
132 KeyFileError::Generate(e) => write!(f, "a new key: {e}"),
133 KeyFileError::KeptInTheKeyFileForm => f.write_str(
134 "the key store holds a key in the key-file form macula-rust 0.7.0 kept; \
135 it no longer loads: create the identity again \
136 (NodeKey::generate_identity, then save_to_keystore)",
137 ),
138 KeyFileError::IdentityName(name) => write!(
139 f,
140 "not an identity name: {name:?} (1 to 64 lowercase letters, digits, - and _, \
141 starting with a letter or digit)"
142 ),
143 KeyFileError::OldKey { from, reason } => {
144 write!(
145 f,
146 "the old identity key {} was not moved: {reason}",
147 from.display()
148 )
149 }
150 KeyFileError::OldKeyPlaceTaken { from, to } => write!(
151 f,
152 "the old identity key {} was not moved: {} holds another key; both are kept",
153 from.display(),
154 to.display()
155 ),
156 KeyFileError::StoredKey { path, reason } => {
157 write!(
158 f,
159 "the identity stored at {} does not load: {reason}",
160 path.display()
161 )
162 }
163 KeyFileError::NoKeyFile => f.write_str(
164 "no key file on this platform: keep the key in Credential Manager \
165 through keystore::KeyringStore (NodeKey::save_to_keystore)",
166 ),
167 }
168 }
169}
170
171impl std::error::Error for KeyFileError {}
172
173impl From<std::io::Error> for KeyFileError {
174 fn from(e: std::io::Error) -> Self {
175 KeyFileError::Io(e)
176 }
177}
178
179impl NodeKey {
180 pub fn save_to_keystore(&self, store: &dyn KeyStore) -> Result<(), KeyFileError> {
185 store
186 .save_key(&self.laid_out(Form::Store)?)
187 .map_err(KeyFileError::KeyStore)
188 }
189
190 pub fn load_from_keystore(
196 store: &dyn KeyStore,
197 purpose: Purpose,
198 profile: Profile,
199 ) -> Result<NodeKey, KeyFileError> {
200 let contents = store.load_key().map_err(KeyFileError::KeyStore)?;
201 if contents.starts_with(MAGIC) {
202 return Err(KeyFileError::KeptInTheKeyFileForm);
203 }
204 let key = parse_form(&contents, Form::Store, purpose, profile)?;
205 round_trip(&key)?;
206 Ok(key)
207 }
208
209 pub(super) fn file_bytes(&self) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
211 self.laid_out(Form::File)
212 }
213
214 fn laid_out(&self, form: Form) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
217 let (magic, with_public) = match form {
218 Form::File => (MAGIC, true),
219 Form::Store => (STORE_MAGIC, false),
220 };
221 let public = |key: &[u8]| -> Vec<u8> {
222 match with_public {
223 true => key.to_vec(),
224 false => Vec::new(),
225 }
226 };
227 let mut out = Zeroizing::new(Vec::with_capacity(LAYOUT_CAPACITY));
230 out.extend_from_slice(magic);
231 out.extend([
232 purpose_tag(self.purpose),
233 profile_tag(self.profile),
234 if self.rsa.is_some() { 2 } else { 1 },
235 ]);
236 append_half(
237 &mut out,
238 TAG_MLDSA_SEED,
239 &public(&self.mldsa_public),
240 &self.mldsa_seed[..],
241 );
242 if let Some(rsa) = &self.rsa {
243 let private = rsa_private_pkcs1(&rsa.pair)?;
244 append_half(&mut out, TAG_RSA_PSS, &public(&rsa.public_der), &private);
245 }
246 Ok(out)
247 }
248}
249
250fn purpose_tag(purpose: Purpose) -> u8 {
251 match purpose {
252 Purpose::Identity => 1,
253 Purpose::Connect => 2,
254 }
255}
256
257fn profile_tag(profile: Profile) -> u8 {
258 match profile {
259 Profile::PqPure => 1,
260 Profile::PqHybrid => 2,
261 }
262}
263
264fn append_half(out: &mut Vec<u8>, tag: u8, public: &[u8], private: &[u8]) {
265 out.push(tag);
266 out.extend((public.len() as u32).to_be_bytes());
267 out.extend_from_slice(public);
268 out.extend((private.len() as u32).to_be_bytes());
269 out.extend_from_slice(private);
270}
271
272fn rsa_private_pkcs1(pair: &RsaKeyPair) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
275 let pkcs8 = pair.as_der().map_err(|_| KeyFileError::PrivateKeyInvalid)?;
276 der::pkcs1_of_pkcs8(pkcs8.as_ref())
277 .map(Zeroizing::new)
278 .ok_or(KeyFileError::PrivateKeyInvalid)
279}
280
281struct StoredHalf<'a> {
283 tag: u8,
284 public: &'a [u8],
285 private: &'a [u8],
286}
287
288pub(super) fn parse(
290 bytes: &[u8],
291 purpose: Purpose,
292 profile: Profile,
293) -> Result<NodeKey, KeyFileError> {
294 parse_form(bytes, Form::File, purpose, profile)
295}
296
297fn parse_form(
301 bytes: &[u8],
302 form: Form,
303 purpose: Purpose,
304 profile: Profile,
305) -> Result<NodeKey, KeyFileError> {
306 let magic = match form {
307 Form::File => MAGIC,
308 Form::Store => STORE_MAGIC,
309 };
310 let rest = bytes.strip_prefix(magic).ok_or(KeyFileError::BadKeyFile)?;
311 let [purpose_byte, profile_byte, count, halves_bytes @ ..] = rest else {
312 return Err(KeyFileError::BadKeyFile);
313 };
314 let stored_purpose = match purpose_byte {
315 1 => Purpose::Identity,
316 2 => Purpose::Connect,
317 _ => return Err(KeyFileError::BadKeyFile),
318 };
319 let stored_profile = match profile_byte {
320 1 => Profile::PqPure,
321 2 => Profile::PqHybrid,
322 _ => return Err(KeyFileError::BadKeyFile),
323 };
324 let halves = parse_halves(halves_bytes)?;
325 if halves.len() != *count as usize {
326 return Err(KeyFileError::BadKeyFile);
327 }
328 if stored_purpose != purpose {
329 return Err(KeyFileError::WrongPurpose(stored_purpose));
330 }
331 if stored_profile != profile {
332 return Err(KeyFileError::WrongProfile(stored_profile));
333 }
334 let fits = match profile {
335 Profile::PqPure => halves.len() == 1 && halves[0].tag == TAG_MLDSA_SEED,
336 Profile::PqHybrid => {
337 halves.len() == 2 && halves[0].tag == TAG_MLDSA_SEED && halves[1].tag == TAG_RSA_PSS
338 }
339 };
340 if !fits {
341 return Err(KeyFileError::WrongAlgorithms);
342 }
343 let (mldsa_seed, mldsa_public) = mldsa_from_half(&halves[0], form)?;
344 let rsa = if profile.hybrid() {
345 Some(rsa_from_half(&halves[1], form)?)
346 } else {
347 None
348 };
349 Ok(NodeKey {
350 purpose,
351 profile,
352 mldsa_seed,
353 mldsa_public,
354 rsa,
355 })
356}
357
358fn parse_halves(mut bytes: &[u8]) -> Result<Vec<StoredHalf<'_>>, KeyFileError> {
359 let mut halves = Vec::new();
360 while let Some((&tag, rest)) = bytes.split_first() {
361 if tag != TAG_MLDSA_SEED && tag != TAG_RSA_PSS {
362 return Err(KeyFileError::BadKeyFile);
363 }
364 let (public, rest) = length_prefixed(rest)?;
365 let (private, rest) = length_prefixed(rest)?;
366 halves.push(StoredHalf {
367 tag,
368 public,
369 private,
370 });
371 bytes = rest;
372 }
373 Ok(halves)
374}
375
376fn length_prefixed(bytes: &[u8]) -> Result<(&[u8], &[u8]), KeyFileError> {
377 let (len, rest) = bytes
378 .split_first_chunk::<4>()
379 .ok_or(KeyFileError::BadKeyFile)?;
380 let len = u32::from_be_bytes(*len) as usize;
381 if len > rest.len() {
382 return Err(KeyFileError::BadKeyFile);
383 }
384 Ok(rest.split_at(len))
385}
386
387fn stored_public_fits(stored: &[u8], derived: &[u8], form: Form) -> Result<(), KeyFileError> {
390 let fits = match form {
391 Form::File => stored == derived,
392 Form::Store => stored.is_empty(),
393 };
394 match fits {
395 true => Ok(()),
396 false => Err(KeyFileError::PublicKeyMismatch),
397 }
398}
399
400fn mldsa_from_half(
401 half: &StoredHalf<'_>,
402 form: Form,
403) -> Result<(Zeroizing<[u8; 32]>, Vec<u8>), KeyFileError> {
404 let seed: [u8; 32] = half
405 .private
406 .try_into()
407 .map_err(|_| KeyFileError::PrivateKeyInvalid)?;
408 let seed = Zeroizing::new(seed);
409 let derived = macula_mldsa::public_key(ML_DSA_87, PrivateKey::Seed(&seed))
410 .map_err(|_| KeyFileError::PrivateKeyInvalid)?;
411 stored_public_fits(half.public, &derived, form)?;
412 Ok((seed, derived))
413}
414
415fn rsa_from_half(half: &StoredHalf<'_>, form: Form) -> Result<RsaHalf, KeyFileError> {
416 let pair = RsaKeyPair::from_der(half.private).map_err(|_| KeyFileError::PrivateKeyInvalid)?;
417 let derived = pair.public_key().as_ref().to_vec();
418 stored_public_fits(half.public, &derived, form)?;
419 if !der::rsa_public_key_is_4096_f4(&derived) {
420 return Err(KeyFileError::WrongKeySize);
421 }
422 Ok(RsaHalf {
423 pair,
424 public_der: derived,
425 })
426}
427
428pub(super) fn round_trip(key: &NodeKey) -> Result<(), KeyFileError> {
431 let mut message = [0u8; 32];
432 aws_lc_rs::rand::fill(&mut message).map_err(|_| KeyFileError::RoundTripFailed)?;
433 let signature = key
434 .sign(&message)
435 .map_err(|_| KeyFileError::RoundTripFailed)?;
436 if verify(&message, &signature, &key.public_key(), key.profile) {
437 Ok(())
438 } else {
439 Err(KeyFileError::RoundTripFailed)
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use std::sync::Mutex;
451
452 use super::*;
453
454 const CREDENTIAL_MANAGER_MAX: usize = 2560;
456
457 const MAX_RSA4096_PKCS1: usize = 4 + 3 + (4 + 513) + (2 + 3) + (4 + 513) + 5 * (4 + 257);
463
464 #[derive(Default)]
466 struct Memory(Mutex<Option<Vec<u8>>>);
467
468 impl KeyStore for Memory {
469 fn save_key(&self, key: &[u8]) -> Result<(), KeyStoreError> {
470 *self.0.lock().unwrap() = Some(key.to_vec());
471 Ok(())
472 }
473
474 fn load_key(&self) -> Result<Zeroizing<Vec<u8>>, KeyStoreError> {
475 let held = self.0.lock().unwrap().clone();
476 held.map(Zeroizing::new).ok_or(KeyStoreError::NotFound)
477 }
478
479 fn delete_key(&self) -> Result<(), KeyStoreError> {
480 *self.0.lock().unwrap() = None;
481 Ok(())
482 }
483 }
484
485 fn kept(store: &Memory) -> usize {
486 store.0.lock().unwrap().as_ref().map_or(0, Vec::len)
487 }
488
489 fn kept_at_worst(profile: Profile) -> usize {
492 let key = NodeKey::generate_identity(profile, 0).unwrap();
493 let store = Memory::default();
494 key.save_to_keystore(&store).unwrap();
495 let loaded = NodeKey::load_from_keystore(&store, Purpose::Identity, profile).unwrap();
496 assert_eq!(loaded.public_key(), key.public_key(), "{profile:?}");
497 let Some(rsa) = &key.rsa else {
498 return kept(&store);
499 };
500 let pkcs1 = rsa_private_pkcs1(&rsa.pair).unwrap().len();
501 assert!(pkcs1 <= MAX_RSA4096_PKCS1, "{pkcs1}");
502 kept(&store) - pkcs1 + MAX_RSA4096_PKCS1
503 }
504
505 #[test]
506 fn a_kept_key_fits_credential_manager_at_the_worst_rsa_size() {
507 for profile in [Profile::PqPure, Profile::PqHybrid] {
508 let worst = kept_at_worst(profile);
509 assert!(
510 worst <= CREDENTIAL_MANAGER_MAX,
511 "{profile:?}: {worst} bytes"
512 );
513 }
514 }
515
516 #[test]
517 fn a_key_kept_in_the_0_7_0_form_is_refused_naming_the_fix() {
518 let key = NodeKey::generate_identity(Profile::PqPure, 0).unwrap();
519 let store = Memory::default();
520 store.save_key(&key.file_bytes().unwrap()).unwrap();
521 let Err(e) = NodeKey::load_from_keystore(&store, Purpose::Identity, Profile::PqPure) else {
522 panic!("the 0.7.0 form is refused");
523 };
524 assert!(matches!(e, KeyFileError::KeptInTheKeyFileForm), "{e:?}");
525 assert!(e.to_string().contains("create the identity again"), "{e}");
526 }
527}