1use std::borrow::Cow;
9
10use anyhow::{Context, bail};
11use camino::Utf8PathBuf;
12use futures_util::future::{try_join, try_join_all};
13use mas_jose::jwk::{JsonWebKey, JsonWebKeySet, Thumbprint};
14use mas_keystore::{Encrypter, Keystore, PrivateKey};
15use rand::{
16 RngExt, SeedableRng,
17 distr::{Distribution as _, StandardUniform},
18};
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use serde_with::serde_as;
22use tokio::task;
23use tracing::info;
24
25use super::ConfigurationSection;
26
27#[derive(Clone, Debug)]
32pub enum Password {
33 File(Utf8PathBuf),
34 Value(String),
35}
36
37#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
39struct PasswordRaw {
40 #[schemars(with = "Option<String>")]
41 #[serde(skip_serializing_if = "Option::is_none")]
42 password_file: Option<Utf8PathBuf>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 password: Option<String>,
45}
46
47impl TryFrom<PasswordRaw> for Option<Password> {
48 type Error = anyhow::Error;
49
50 fn try_from(value: PasswordRaw) -> Result<Self, Self::Error> {
51 match (value.password, value.password_file) {
52 (None, None) => Ok(None),
53 (None, Some(path)) => Ok(Some(Password::File(path))),
54 (Some(password), None) => Ok(Some(Password::Value(password))),
55 (Some(_), Some(_)) => bail!("Cannot specify both `password` and `password_file`"),
56 }
57 }
58}
59
60impl From<Option<Password>> for PasswordRaw {
61 fn from(value: Option<Password>) -> Self {
62 match value {
63 Some(Password::File(path)) => PasswordRaw {
64 password_file: Some(path),
65 password: None,
66 },
67 Some(Password::Value(password)) => PasswordRaw {
68 password_file: None,
69 password: Some(password),
70 },
71 None => PasswordRaw {
72 password_file: None,
73 password: None,
74 },
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
84pub enum Key {
85 File(Utf8PathBuf),
86 Value(String),
87}
88
89#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
91struct KeyRaw {
92 #[schemars(with = "Option<String>")]
93 #[serde(skip_serializing_if = "Option::is_none")]
94 key_file: Option<Utf8PathBuf>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 key: Option<String>,
97}
98
99impl TryFrom<KeyRaw> for Key {
100 type Error = anyhow::Error;
101
102 fn try_from(value: KeyRaw) -> Result<Key, Self::Error> {
103 match (value.key, value.key_file) {
104 (None, None) => bail!("Missing `key` or `key_file`"),
105 (None, Some(path)) => Ok(Key::File(path)),
106 (Some(key), None) => Ok(Key::Value(key)),
107 (Some(_), Some(_)) => bail!("Cannot specify both `key` and `key_file`"),
108 }
109 }
110}
111
112impl From<Key> for KeyRaw {
113 fn from(value: Key) -> Self {
114 match value {
115 Key::File(path) => KeyRaw {
116 key_file: Some(path),
117 key: None,
118 },
119 Key::Value(key) => KeyRaw {
120 key_file: None,
121 key: Some(key),
122 },
123 }
124 }
125}
126
127#[serde_as]
129#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
130pub struct KeyConfig {
131 #[serde(skip_serializing_if = "Option::is_none")]
135 kid: Option<String>,
136
137 #[schemars(with = "PasswordRaw")]
138 #[serde_as(as = "serde_with::TryFromInto<PasswordRaw>")]
139 #[serde(flatten)]
140 password: Option<Password>,
141
142 #[schemars(with = "KeyRaw")]
143 #[serde_as(as = "serde_with::TryFromInto<KeyRaw>")]
144 #[serde(flatten)]
145 key: Key,
146}
147
148impl KeyConfig {
149 async fn password(&self) -> anyhow::Result<Option<Cow<'_, [u8]>>> {
153 Ok(match &self.password {
154 Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read(path).await?)),
155 Some(Password::Value(password)) => Some(Cow::Borrowed(password.as_bytes())),
156 None => None,
157 })
158 }
159
160 async fn key(&self) -> anyhow::Result<Cow<'_, [u8]>> {
164 Ok(match &self.key {
165 Key::File(path) => Cow::Owned(tokio::fs::read(path).await?),
166 Key::Value(key) => Cow::Borrowed(key.as_bytes()),
167 })
168 }
169
170 async fn json_web_key(&self) -> anyhow::Result<JsonWebKey<mas_keystore::PrivateKey>> {
174 let (key, password) = try_join(self.key(), self.password()).await?;
175
176 let private_key = match password {
177 Some(password) => PrivateKey::load_encrypted(&key, password)?,
178 None => PrivateKey::load(&key)?,
179 };
180
181 let kid = match self.kid.clone() {
182 Some(kid) => kid,
183 None => private_key.thumbprint_sha256_base64(),
184 };
185
186 Ok(JsonWebKey::new(private_key)
187 .with_kid(kid)
188 .with_use(mas_iana::jose::JsonWebKeyUse::Sig))
189 }
190}
191
192#[derive(Debug, Clone)]
194pub enum Encryption {
195 File(Utf8PathBuf),
196 Value([u8; 32]),
197}
198
199#[serde_as]
201#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
202struct EncryptionRaw {
203 #[schemars(with = "Option<String>")]
205 #[serde(skip_serializing_if = "Option::is_none")]
206 encryption_file: Option<Utf8PathBuf>,
207
208 #[schemars(
210 with = "Option<String>",
211 regex(pattern = r"[0-9a-fA-F]{64}"),
212 example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
213 )]
214 #[serde_as(as = "Option<serde_with::hex::Hex>")]
215 #[serde(skip_serializing_if = "Option::is_none")]
216 encryption: Option<[u8; 32]>,
217}
218
219impl TryFrom<EncryptionRaw> for Encryption {
220 type Error = anyhow::Error;
221
222 fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
223 match (value.encryption, value.encryption_file) {
224 (None, None) => bail!("Missing `encryption` or `encryption_file`"),
225 (None, Some(path)) => Ok(Encryption::File(path)),
226 (Some(encryption), None) => Ok(Encryption::Value(encryption)),
227 (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
228 }
229 }
230}
231
232impl From<Encryption> for EncryptionRaw {
233 fn from(value: Encryption) -> Self {
234 match value {
235 Encryption::File(path) => EncryptionRaw {
236 encryption_file: Some(path),
237 encryption: None,
238 },
239 Encryption::Value(encryption) => EncryptionRaw {
240 encryption_file: None,
241 encryption: Some(encryption),
242 },
243 }
244 }
245}
246
247async fn key_configs_from_path(path: &Utf8PathBuf) -> anyhow::Result<Vec<KeyConfig>> {
249 let mut result = vec![];
250 let mut read_dir = tokio::fs::read_dir(path).await?;
251 while let Some(dir_entry) = read_dir.next_entry().await? {
252 if !dir_entry.path().is_file() {
253 continue;
254 }
255 result.push(KeyConfig {
256 kid: None,
257 password: None,
258 key: Key::File(dir_entry.path().try_into()?),
259 });
260 }
261 Ok(result)
262}
263
264#[serde_as]
266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
267pub struct SecretsConfig {
268 #[schemars(with = "EncryptionRaw")]
270 #[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
271 #[serde(flatten)]
272 encryption: Encryption,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 keys: Option<Vec<KeyConfig>>,
277
278 #[schemars(with = "Option<String>")]
280 #[serde(skip_serializing_if = "Option::is_none")]
281 keys_dir: Option<Utf8PathBuf>,
282}
283
284impl SecretsConfig {
285 #[tracing::instrument(name = "secrets.load", skip_all)]
291 pub async fn key_store(&self) -> anyhow::Result<Keystore> {
292 let key_configs = self.key_configs().await?;
293 let web_keys = try_join_all(key_configs.iter().map(KeyConfig::json_web_key)).await?;
294
295 Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
296 }
297
298 pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
304 Ok(Encrypter::new(&self.encryption().await?))
305 }
306
307 pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
313 match self.encryption {
315 Encryption::Value(encryption) => Ok(encryption),
316 Encryption::File(ref path) => {
317 let mut bytes = [0; 32];
318 let content = tokio::fs::read(path).await?;
319 hex::decode_to_slice(content, &mut bytes).context(
320 "Content of `encryption_file` must contain hex characters \
321 encoding exactly 32 bytes",
322 )?;
323 Ok(bytes)
324 }
325 }
326 }
327
328 async fn key_configs(&self) -> anyhow::Result<Vec<KeyConfig>> {
332 let mut key_configs = match &self.keys_dir {
333 Some(keys_dir) => key_configs_from_path(keys_dir).await?,
334 None => vec![],
335 };
336
337 let inline_key_configs = self.keys.as_deref().unwrap_or_default();
338 key_configs.extend(inline_key_configs.iter().cloned());
339
340 Ok(key_configs)
341 }
342}
343
344impl ConfigurationSection for SecretsConfig {
345 const PATH: Option<&'static str> = Some("secrets");
346}
347
348impl SecretsConfig {
349 #[expect(clippy::similar_names, reason = "Key type names are very similar")]
350 #[tracing::instrument(skip_all)]
351 pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
352 where
353 R: RngExt + Send,
354 {
355 info!("Generating keys...");
356
357 let span = tracing::info_span!("rsa");
358 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng);
359 let rsa_key = task::spawn_blocking(move || {
360 let _entered = span.enter();
361 let ret = PrivateKey::generate_rsa(key_rng).unwrap();
362 info!("Done generating RSA key");
363 ret
364 })
365 .await
366 .context("could not join blocking task")?;
367 let rsa_key = KeyConfig {
368 kid: None,
369 password: None,
370 key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
371 };
372
373 let span = tracing::info_span!("ec_p256");
374 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng);
375 let ec_p256_key = task::spawn_blocking(move || {
376 let _entered = span.enter();
377 let ret = PrivateKey::generate_ec_p256(key_rng);
378 info!("Done generating EC P-256 key");
379 ret
380 })
381 .await
382 .context("could not join blocking task")?;
383 let ec_p256_key = KeyConfig {
384 kid: None,
385 password: None,
386 key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
387 };
388
389 let span = tracing::info_span!("ec_p384");
390 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng);
391 let ec_p384_key = task::spawn_blocking(move || {
392 let _entered = span.enter();
393 let ret = PrivateKey::generate_ec_p384(key_rng);
394 info!("Done generating EC P-384 key");
395 ret
396 })
397 .await
398 .context("could not join blocking task")?;
399 let ec_p384_key = KeyConfig {
400 kid: None,
401 password: None,
402 key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
403 };
404
405 let span = tracing::info_span!("ec_k256");
406 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng);
407 let ec_k256_key = task::spawn_blocking(move || {
408 let _entered = span.enter();
409 let ret = PrivateKey::generate_ec_k256(key_rng);
410 info!("Done generating EC secp256k1 key");
411 ret
412 })
413 .await
414 .context("could not join blocking task")?;
415 let ec_k256_key = KeyConfig {
416 kid: None,
417 password: None,
418 key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
419 };
420
421 Ok(Self {
422 encryption: Encryption::Value(StandardUniform.sample(&mut rng)),
423 keys: Some(vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key]),
424 keys_dir: None,
425 })
426 }
427
428 pub(crate) fn test() -> Self {
429 let rsa_key = KeyConfig {
430 kid: None,
431 password: None,
432 key: Key::Value(
433 indoc::indoc! {r"
434 -----BEGIN PRIVATE KEY-----
435 MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
436 QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
437 scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
438 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
439 vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
440 N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
441 tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
442 Gh7BNzCeN+D6
443 -----END PRIVATE KEY-----
444 "}
445 .to_owned(),
446 ),
447 };
448 let ecdsa_key = KeyConfig {
449 kid: None,
450 password: None,
451 key: Key::Value(
452 indoc::indoc! {r"
453 -----BEGIN PRIVATE KEY-----
454 MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
455 NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
456 OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
457 -----END PRIVATE KEY-----
458 "}
459 .to_owned(),
460 ),
461 };
462
463 Self {
464 encryption: Encryption::Value([0xEA; 32]),
465 keys: Some(vec![rsa_key, ecdsa_key]),
466 keys_dir: None,
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 #![expect(clippy::result_large_err)]
476
477 use figment::{
478 Figment, Jail,
479 providers::{Format, Yaml},
480 };
481 use mas_jose::constraints::Constrainable;
482 use tokio::{runtime::Handle, task};
483
484 use super::*;
485
486 #[tokio::test]
487 async fn load_config() {
488 task::spawn_blocking(|| {
489 Jail::expect_with(|jail| {
490 jail.create_file(
491 "config.yaml",
492 indoc::indoc! {r"
493 secrets:
494 encryption_file: encryption
495 keys_dir: keys
496 "},
497 )?;
498 jail.create_file(
499 "encryption",
500 "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
501 )?;
502 jail.create_dir("keys")?;
503 jail.create_file(
504 "keys/key1",
505 indoc::indoc! {r"
506 -----BEGIN RSA PRIVATE KEY-----
507 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
556 -----END RSA PRIVATE KEY-----
557 "},
558 )?;
559 jail.create_file(
560 "keys/key2",
561 indoc::indoc! {r"
562 -----BEGIN EC PRIVATE KEY-----
563 MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
564 AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
565 h27LAir5RqxByHvua2XsP46rSTChof78uw==
566 -----END EC PRIVATE KEY-----
567 "},
568 )?;
569
570 let config = Figment::new()
571 .merge(Yaml::file("config.yaml"))
572 .extract_inner::<SecretsConfig>("secrets")?;
573
574 Handle::current().block_on(async move {
575 assert!(
576 matches!(config.encryption, Encryption::File(ref p) if p == "encryption")
577 );
578 assert_eq!(
579 config.encryption().await.unwrap(),
580 [
581 0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
582 136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
583 255
584 ]
585 );
586
587 let mut key_config = config.key_configs().await.unwrap();
588 key_config.sort_by_key(|a| {
589 if let Key::File(p) = &a.key {
590 Some(p.clone())
591 } else {
592 None
593 }
594 });
595 let key_store = config.key_store().await.unwrap();
596
597 assert!(key_config[0].kid.is_none());
598 assert!(matches!(&key_config[0].key, Key::File(p) if p == "keys/key1"));
599 assert!(key_store.iter().any(|k| k.kid() == Some("xmgGCzGtQFmhEOP0YAqBt-oZyVauSVMXcf4kwcgGZLc")));
600 assert!(key_config[1].kid.is_none());
601 assert!(matches!(&key_config[1].key, Key::File(p) if p == "keys/key2"));
602 assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
603 });
604
605 Ok(())
606 });
607 })
608 .await
609 .unwrap();
610 }
611
612 #[tokio::test]
613 async fn load_config_inline_secrets() {
614 task::spawn_blocking(|| {
615 Jail::expect_with(|jail| {
616 jail.create_file(
617 "config.yaml",
618 indoc::indoc! {r"
619 secrets:
620 encryption: >-
621 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
622 keys:
623 - kid: lekid0
624 key: |
625 -----BEGIN EC PRIVATE KEY-----
626 MHcCAQEEIOtZfDuXZr/NC0V3sisR4Chf7RZg6a2dpZesoXMlsPeRoAoGCCqGSM49
627 AwEHoUQDQgAECfpqx64lrR85MOhdMxNmIgmz8IfmM5VY9ICX9aoaArnD9FjgkBIl
628 fGmQWxxXDSWH6SQln9tROVZaduenJqDtDw==
629 -----END EC PRIVATE KEY-----
630 - key: |
631 -----BEGIN EC PRIVATE KEY-----
632 MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
633 AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
634 h27LAir5RqxByHvua2XsP46rSTChof78uw==
635 -----END EC PRIVATE KEY-----
636 "},
637 )?;
638
639 let config = Figment::new()
640 .merge(Yaml::file("config.yaml"))
641 .extract_inner::<SecretsConfig>("secrets")?;
642
643 Handle::current().block_on(async move {
644 assert_eq!(
645 config.encryption().await.unwrap(),
646 [
647 0, 0, 17, 17, 34, 34, 51, 51, 68, 68, 85, 85, 102, 102, 119, 119, 136,
648 136, 153, 153, 170, 170, 187, 187, 204, 204, 221, 221, 238, 238, 255,
649 255
650 ]
651 );
652
653 let key_store = config.key_store().await.unwrap();
654 assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
655 assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
656 });
657
658 Ok(())
659 });
660 })
661 .await
662 .unwrap();
663 }
664
665 #[tokio::test]
666 async fn load_config_mixed_key_sources() {
667 task::spawn_blocking(|| {
668 Jail::expect_with(|jail| {
669 jail.create_file(
670 "config.yaml",
671 indoc::indoc! {r"
672 secrets:
673 encryption_file: encryption
674 keys_dir: keys
675 keys:
676 - kid: lekid0
677 key: |
678 -----BEGIN EC PRIVATE KEY-----
679 MHcCAQEEIOtZfDuXZr/NC0V3sisR4Chf7RZg6a2dpZesoXMlsPeRoAoGCCqGSM49
680 AwEHoUQDQgAECfpqx64lrR85MOhdMxNmIgmz8IfmM5VY9ICX9aoaArnD9FjgkBIl
681 fGmQWxxXDSWH6SQln9tROVZaduenJqDtDw==
682 -----END EC PRIVATE KEY-----
683 "},
684 )?;
685 jail.create_dir("keys")?;
686 jail.create_file(
687 "keys/key_from_file",
688 indoc::indoc! {r"
689 -----BEGIN EC PRIVATE KEY-----
690 MHcCAQEEIKlZz/GnH0idVH1PnAF4HQNwRafgBaE2tmyN1wjfdOQqoAoGCCqGSM49
691 AwEHoUQDQgAEHrgPeG+Mt8eahih1h4qaPjhl7jT25cdzBkg3dbVks6gBR2Rx4ug9
692 h27LAir5RqxByHvua2XsP46rSTChof78uw==
693 -----END EC PRIVATE KEY-----
694 "},
695 )?;
696
697 let config = Figment::new()
698 .merge(Yaml::file("config.yaml"))
699 .extract_inner::<SecretsConfig>("secrets")?;
700
701 Handle::current().block_on(async move {
702 let key_config = config.key_configs().await.unwrap();
703 let key_store = config.key_store().await.unwrap();
704
705 assert!(key_config[0].kid.is_none());
706 assert!(matches!(&key_config[0].key, Key::File(p) if p == "keys/key_from_file"));
707 assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
708 assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
709 });
710
711 Ok(())
712 });
713 })
714 .await
715 .unwrap();
716 }
717}