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