1use std::fmt;
9
10use aws_lc_rs::encoding::AsDer;
11use aws_lc_rs::rsa::KeyPair as RsaKeyPair;
12use aws_lc_rs::signature::KeyPair as _;
13use macula_mldsa::{PrivateKey, Zeroizing, ML_DSA_87};
14
15use super::{der, verify, KeyError, NodeKey, Purpose, RsaHalf};
16use crate::keystore::{KeyStore, KeyStoreError};
17use crate::profile::Profile;
18
19const MAGIC: &[u8] = b"macula-node-key-seed-v1\0";
23
24const STORE_MAGIC: &[u8] = b"macula-node-key-private-v1\0";
29
30const LAYOUT_CAPACITY: usize = 8 * 1024;
32
33#[derive(Clone, Copy, PartialEq, Eq)]
36enum Form {
37 File,
38 Store,
39}
40
41const TAG_MLDSA_SEED: u8 = 1;
42const TAG_RSA_PSS: u8 = 2;
43#[derive(Debug)]
45pub enum KeyFileError {
46 Io(std::io::Error),
48 NotRegular,
51 Owner,
53 Permissions,
55 TooLarge,
57 BadKeyFile,
59 WrongPurpose(Purpose),
61 WrongProfile(Profile),
63 WrongAlgorithms,
65 WrongKeySize,
67 PrivateKeyInvalid,
69 PublicKeyMismatch,
71 RoundTripFailed,
73 KeyStore(KeyStoreError),
75 Generate(KeyError),
77 NoKeyFile,
83 KeptInTheKeyFileForm,
86}
87
88impl fmt::Display for KeyFileError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 KeyFileError::Io(e) => write!(f, "key file: {e}"),
92 KeyFileError::NotRegular => f.write_str("the key file is not a regular file"),
93 KeyFileError::Owner => f.write_str("the key file is owned by another user"),
94 KeyFileError::Permissions => {
95 f.write_str("the key file can be read by its group or others")
96 }
97 KeyFileError::TooLarge => f.write_str("the key file is longer than 64 KiB"),
98 KeyFileError::BadKeyFile => f.write_str("not a key file in the seed form"),
99 KeyFileError::WrongPurpose(p) => write!(f, "the key file holds a key for {p}"),
100 KeyFileError::WrongProfile(p) => write!(f, "the key file holds a key for {p}"),
101 KeyFileError::WrongAlgorithms => f.write_str("the key's halves do not fit its profile"),
102 KeyFileError::WrongKeySize => {
103 f.write_str("the RSA-PSS half is not a 4096-bit key with exponent 65537")
104 }
105 KeyFileError::PrivateKeyInvalid => {
106 f.write_str("the key file's private key is not valid")
107 }
108 KeyFileError::PublicKeyMismatch => {
109 f.write_str("the stored public key is not the one its private key derives")
110 }
111 KeyFileError::RoundTripFailed => f.write_str("the key does not sign and verify"),
112 KeyFileError::KeyStore(e) => write!(f, "key store: {e}"),
113 KeyFileError::Generate(e) => write!(f, "a new key: {e}"),
114 KeyFileError::KeptInTheKeyFileForm => f.write_str(
115 "the key store holds a key in the key-file form macula-rust 0.7.0 kept; \
116 it no longer loads: create the identity again \
117 (NodeKey::generate_identity, then save_to_keystore)",
118 ),
119 KeyFileError::NoKeyFile => f.write_str(
120 "no key file on this platform: keep the key in Credential Manager \
121 through keystore::KeyringStore (NodeKey::save_to_keystore)",
122 ),
123 }
124 }
125}
126
127impl std::error::Error for KeyFileError {}
128
129impl From<std::io::Error> for KeyFileError {
130 fn from(e: std::io::Error) -> Self {
131 KeyFileError::Io(e)
132 }
133}
134
135impl NodeKey {
136 pub fn save_to_keystore(&self, store: &dyn KeyStore) -> Result<(), KeyFileError> {
141 store
142 .save_key(&self.laid_out(Form::Store)?)
143 .map_err(KeyFileError::KeyStore)
144 }
145
146 pub fn load_from_keystore(
152 store: &dyn KeyStore,
153 purpose: Purpose,
154 profile: Profile,
155 ) -> Result<NodeKey, KeyFileError> {
156 let contents = store.load_key().map_err(KeyFileError::KeyStore)?;
157 if contents.starts_with(MAGIC) {
158 return Err(KeyFileError::KeptInTheKeyFileForm);
159 }
160 let key = parse_form(&contents, Form::Store, purpose, profile)?;
161 round_trip(&key)?;
162 Ok(key)
163 }
164
165 pub(super) fn file_bytes(&self) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
167 self.laid_out(Form::File)
168 }
169
170 fn laid_out(&self, form: Form) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
173 let (magic, with_public) = match form {
174 Form::File => (MAGIC, true),
175 Form::Store => (STORE_MAGIC, false),
176 };
177 let public = |key: &[u8]| -> Vec<u8> {
178 match with_public {
179 true => key.to_vec(),
180 false => Vec::new(),
181 }
182 };
183 let mut out = Zeroizing::new(Vec::with_capacity(LAYOUT_CAPACITY));
186 out.extend_from_slice(magic);
187 out.extend([
188 purpose_tag(self.purpose),
189 profile_tag(self.profile),
190 if self.rsa.is_some() { 2 } else { 1 },
191 ]);
192 append_half(
193 &mut out,
194 TAG_MLDSA_SEED,
195 &public(&self.mldsa_public),
196 &self.mldsa_seed[..],
197 );
198 if let Some(rsa) = &self.rsa {
199 let private = rsa_private_pkcs1(&rsa.pair)?;
200 append_half(&mut out, TAG_RSA_PSS, &public(&rsa.public_der), &private);
201 }
202 Ok(out)
203 }
204}
205
206fn purpose_tag(purpose: Purpose) -> u8 {
207 match purpose {
208 Purpose::Identity => 1,
209 Purpose::Connect => 2,
210 }
211}
212
213fn profile_tag(profile: Profile) -> u8 {
214 match profile {
215 Profile::PqPure => 1,
216 Profile::PqHybrid => 2,
217 }
218}
219
220fn append_half(out: &mut Vec<u8>, tag: u8, public: &[u8], private: &[u8]) {
221 out.push(tag);
222 out.extend((public.len() as u32).to_be_bytes());
223 out.extend_from_slice(public);
224 out.extend((private.len() as u32).to_be_bytes());
225 out.extend_from_slice(private);
226}
227
228fn rsa_private_pkcs1(pair: &RsaKeyPair) -> Result<Zeroizing<Vec<u8>>, KeyFileError> {
231 let pkcs8 = pair.as_der().map_err(|_| KeyFileError::PrivateKeyInvalid)?;
232 der::pkcs1_of_pkcs8(pkcs8.as_ref())
233 .map(Zeroizing::new)
234 .ok_or(KeyFileError::PrivateKeyInvalid)
235}
236
237struct StoredHalf<'a> {
239 tag: u8,
240 public: &'a [u8],
241 private: &'a [u8],
242}
243
244pub(super) fn parse(
246 bytes: &[u8],
247 purpose: Purpose,
248 profile: Profile,
249) -> Result<NodeKey, KeyFileError> {
250 parse_form(bytes, Form::File, purpose, profile)
251}
252
253fn parse_form(
257 bytes: &[u8],
258 form: Form,
259 purpose: Purpose,
260 profile: Profile,
261) -> Result<NodeKey, KeyFileError> {
262 let magic = match form {
263 Form::File => MAGIC,
264 Form::Store => STORE_MAGIC,
265 };
266 let rest = bytes.strip_prefix(magic).ok_or(KeyFileError::BadKeyFile)?;
267 let [purpose_byte, profile_byte, count, halves_bytes @ ..] = rest else {
268 return Err(KeyFileError::BadKeyFile);
269 };
270 let stored_purpose = match purpose_byte {
271 1 => Purpose::Identity,
272 2 => Purpose::Connect,
273 _ => return Err(KeyFileError::BadKeyFile),
274 };
275 let stored_profile = match profile_byte {
276 1 => Profile::PqPure,
277 2 => Profile::PqHybrid,
278 _ => return Err(KeyFileError::BadKeyFile),
279 };
280 let halves = parse_halves(halves_bytes)?;
281 if halves.len() != *count as usize {
282 return Err(KeyFileError::BadKeyFile);
283 }
284 if stored_purpose != purpose {
285 return Err(KeyFileError::WrongPurpose(stored_purpose));
286 }
287 if stored_profile != profile {
288 return Err(KeyFileError::WrongProfile(stored_profile));
289 }
290 let fits = match profile {
291 Profile::PqPure => halves.len() == 1 && halves[0].tag == TAG_MLDSA_SEED,
292 Profile::PqHybrid => {
293 halves.len() == 2 && halves[0].tag == TAG_MLDSA_SEED && halves[1].tag == TAG_RSA_PSS
294 }
295 };
296 if !fits {
297 return Err(KeyFileError::WrongAlgorithms);
298 }
299 let (mldsa_seed, mldsa_public) = mldsa_from_half(&halves[0], form)?;
300 let rsa = if profile.hybrid() {
301 Some(rsa_from_half(&halves[1], form)?)
302 } else {
303 None
304 };
305 Ok(NodeKey {
306 purpose,
307 profile,
308 mldsa_seed,
309 mldsa_public,
310 rsa,
311 })
312}
313
314fn parse_halves(mut bytes: &[u8]) -> Result<Vec<StoredHalf<'_>>, KeyFileError> {
315 let mut halves = Vec::new();
316 while let Some((&tag, rest)) = bytes.split_first() {
317 if tag != TAG_MLDSA_SEED && tag != TAG_RSA_PSS {
318 return Err(KeyFileError::BadKeyFile);
319 }
320 let (public, rest) = length_prefixed(rest)?;
321 let (private, rest) = length_prefixed(rest)?;
322 halves.push(StoredHalf {
323 tag,
324 public,
325 private,
326 });
327 bytes = rest;
328 }
329 Ok(halves)
330}
331
332fn length_prefixed(bytes: &[u8]) -> Result<(&[u8], &[u8]), KeyFileError> {
333 let (len, rest) = bytes
334 .split_first_chunk::<4>()
335 .ok_or(KeyFileError::BadKeyFile)?;
336 let len = u32::from_be_bytes(*len) as usize;
337 if len > rest.len() {
338 return Err(KeyFileError::BadKeyFile);
339 }
340 Ok(rest.split_at(len))
341}
342
343fn stored_public_fits(stored: &[u8], derived: &[u8], form: Form) -> Result<(), KeyFileError> {
346 let fits = match form {
347 Form::File => stored == derived,
348 Form::Store => stored.is_empty(),
349 };
350 match fits {
351 true => Ok(()),
352 false => Err(KeyFileError::PublicKeyMismatch),
353 }
354}
355
356fn mldsa_from_half(
357 half: &StoredHalf<'_>,
358 form: Form,
359) -> Result<(Zeroizing<[u8; 32]>, Vec<u8>), KeyFileError> {
360 let seed: [u8; 32] = half
361 .private
362 .try_into()
363 .map_err(|_| KeyFileError::PrivateKeyInvalid)?;
364 let seed = Zeroizing::new(seed);
365 let derived = macula_mldsa::public_key(ML_DSA_87, PrivateKey::Seed(&seed))
366 .map_err(|_| KeyFileError::PrivateKeyInvalid)?;
367 stored_public_fits(half.public, &derived, form)?;
368 Ok((seed, derived))
369}
370
371fn rsa_from_half(half: &StoredHalf<'_>, form: Form) -> Result<RsaHalf, KeyFileError> {
372 let pair = RsaKeyPair::from_der(half.private).map_err(|_| KeyFileError::PrivateKeyInvalid)?;
373 let derived = pair.public_key().as_ref().to_vec();
374 stored_public_fits(half.public, &derived, form)?;
375 if !der::rsa_public_key_is_4096_f4(&derived) {
376 return Err(KeyFileError::WrongKeySize);
377 }
378 Ok(RsaHalf {
379 pair,
380 public_der: derived,
381 })
382}
383
384pub(super) fn round_trip(key: &NodeKey) -> Result<(), KeyFileError> {
387 let mut message = [0u8; 32];
388 aws_lc_rs::rand::fill(&mut message).map_err(|_| KeyFileError::RoundTripFailed)?;
389 let signature = key
390 .sign(&message)
391 .map_err(|_| KeyFileError::RoundTripFailed)?;
392 if verify(&message, &signature, &key.public_key(), key.profile) {
393 Ok(())
394 } else {
395 Err(KeyFileError::RoundTripFailed)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use std::sync::Mutex;
407
408 use super::*;
409
410 const CREDENTIAL_MANAGER_MAX: usize = 2560;
412
413 const MAX_RSA4096_PKCS1: usize = 4 + 3 + (4 + 513) + (2 + 3) + (4 + 513) + 5 * (4 + 257);
419
420 #[derive(Default)]
422 struct Memory(Mutex<Option<Vec<u8>>>);
423
424 impl KeyStore for Memory {
425 fn save_key(&self, key: &[u8]) -> Result<(), KeyStoreError> {
426 *self.0.lock().unwrap() = Some(key.to_vec());
427 Ok(())
428 }
429
430 fn load_key(&self) -> Result<Zeroizing<Vec<u8>>, KeyStoreError> {
431 let held = self.0.lock().unwrap().clone();
432 held.map(Zeroizing::new).ok_or(KeyStoreError::NotFound)
433 }
434
435 fn delete_key(&self) -> Result<(), KeyStoreError> {
436 *self.0.lock().unwrap() = None;
437 Ok(())
438 }
439 }
440
441 fn kept(store: &Memory) -> usize {
442 store.0.lock().unwrap().as_ref().map_or(0, Vec::len)
443 }
444
445 fn kept_at_worst(profile: Profile) -> usize {
448 let key = NodeKey::generate_identity(profile, 0).unwrap();
449 let store = Memory::default();
450 key.save_to_keystore(&store).unwrap();
451 let loaded = NodeKey::load_from_keystore(&store, Purpose::Identity, profile).unwrap();
452 assert_eq!(loaded.public_key(), key.public_key(), "{profile:?}");
453 let Some(rsa) = &key.rsa else {
454 return kept(&store);
455 };
456 let pkcs1 = rsa_private_pkcs1(&rsa.pair).unwrap().len();
457 assert!(pkcs1 <= MAX_RSA4096_PKCS1, "{pkcs1}");
458 kept(&store) - pkcs1 + MAX_RSA4096_PKCS1
459 }
460
461 #[test]
462 fn a_kept_key_fits_credential_manager_at_the_worst_rsa_size() {
463 for profile in [Profile::PqPure, Profile::PqHybrid] {
464 let worst = kept_at_worst(profile);
465 assert!(
466 worst <= CREDENTIAL_MANAGER_MAX,
467 "{profile:?}: {worst} bytes"
468 );
469 }
470 }
471
472 #[test]
473 fn a_key_kept_in_the_0_7_0_form_is_refused_naming_the_fix() {
474 let key = NodeKey::generate_identity(Profile::PqPure, 0).unwrap();
475 let store = Memory::default();
476 store.save_key(&key.file_bytes().unwrap()).unwrap();
477 let Err(e) = NodeKey::load_from_keystore(&store, Purpose::Identity, Profile::PqPure) else {
478 panic!("the 0.7.0 form is refused");
479 };
480 assert!(matches!(e, KeyFileError::KeptInTheKeyFileForm), "{e:?}");
481 assert!(e.to_string().contains("create the identity again"), "{e}");
482 }
483}