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