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};
13use mas_keystore::{Encrypter, Keystore, PrivateKey};
14use rand::{
15 Rng, SeedableRng,
16 distributions::{Alphanumeric, DistString, Standard},
17 prelude::Distribution as _,
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 kid: 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<String>>> {
149 Ok(match &self.password {
150 Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)),
151 Some(Password::Value(password)) => Some(Cow::Borrowed(password)),
152 None => None,
153 })
154 }
155
156 async fn key(&self) -> anyhow::Result<Cow<String>> {
160 Ok(match &self.key {
161 Key::File(path) => Cow::Owned(tokio::fs::read_to_string(path).await?),
162 Key::Value(key) => Cow::Borrowed(key),
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.as_bytes(), password.as_bytes())?,
174 None => PrivateKey::load(key.as_bytes())?,
175 };
176
177 Ok(JsonWebKey::new(private_key)
178 .with_kid(self.kid.clone())
179 .with_use(mas_iana::jose::JsonWebKeyUse::Sig))
180 }
181}
182
183#[derive(Debug, Clone)]
185pub enum Encryption {
186 File(Utf8PathBuf),
187 Value([u8; 32]),
188}
189
190#[serde_as]
192#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
193struct EncryptionRaw {
194 #[schemars(with = "Option<String>")]
196 #[serde(skip_serializing_if = "Option::is_none")]
197 encryption_file: Option<Utf8PathBuf>,
198
199 #[schemars(
201 with = "Option<String>",
202 regex(pattern = r"[0-9a-fA-F]{64}"),
203 example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
204 )]
205 #[serde_as(as = "Option<serde_with::hex::Hex>")]
206 #[serde(skip_serializing_if = "Option::is_none")]
207 encryption: Option<[u8; 32]>,
208}
209
210impl TryFrom<EncryptionRaw> for Encryption {
211 type Error = anyhow::Error;
212
213 fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
214 match (value.encryption, value.encryption_file) {
215 (None, None) => bail!("Missing `encryption` or `encryption_file`"),
216 (None, Some(path)) => Ok(Encryption::File(path)),
217 (Some(encryption), None) => Ok(Encryption::Value(encryption)),
218 (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
219 }
220 }
221}
222
223impl From<Encryption> for EncryptionRaw {
224 fn from(value: Encryption) -> Self {
225 match value {
226 Encryption::File(path) => EncryptionRaw {
227 encryption_file: Some(path),
228 encryption: None,
229 },
230 Encryption::Value(encryption) => EncryptionRaw {
231 encryption_file: None,
232 encryption: Some(encryption),
233 },
234 }
235 }
236}
237
238#[serde_as]
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
241pub struct SecretsConfig {
242 #[schemars(with = "EncryptionRaw")]
244 #[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
245 #[serde(flatten)]
246 encryption: Encryption,
247
248 #[serde(default)]
250 keys: Vec<KeyConfig>,
251}
252
253impl SecretsConfig {
254 #[tracing::instrument(name = "secrets.load", skip_all)]
260 pub async fn key_store(&self) -> anyhow::Result<Keystore> {
261 let web_keys = try_join_all(self.keys.iter().map(KeyConfig::json_web_key)).await?;
262
263 Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
264 }
265
266 pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
272 Ok(Encrypter::new(&self.encryption().await?))
273 }
274
275 pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
281 match self.encryption {
283 Encryption::Value(encryption) => Ok(encryption),
284 Encryption::File(ref path) => {
285 let mut bytes = [0; 32];
286 let content = tokio::fs::read(path).await?;
287 hex::decode_to_slice(content, &mut bytes).context(
288 "Content of `encryption_file` must contain hex characters \
289 encoding exactly 32 bytes",
290 )?;
291 Ok(bytes)
292 }
293 }
294 }
295}
296
297impl ConfigurationSection for SecretsConfig {
298 const PATH: Option<&'static str> = Some("secrets");
299}
300
301impl SecretsConfig {
302 #[tracing::instrument(skip_all)]
303 pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
304 where
305 R: Rng + Send,
306 {
307 info!("Generating keys...");
308
309 let span = tracing::info_span!("rsa");
310 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
311 let rsa_key = task::spawn_blocking(move || {
312 let _entered = span.enter();
313 let ret = PrivateKey::generate_rsa(key_rng).unwrap();
314 info!("Done generating RSA key");
315 ret
316 })
317 .await
318 .context("could not join blocking task")?;
319 let rsa_key = KeyConfig {
320 kid: Alphanumeric.sample_string(&mut rng, 10),
321 password: None,
322 key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
323 };
324
325 let span = tracing::info_span!("ec_p256");
326 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
327 let ec_p256_key = task::spawn_blocking(move || {
328 let _entered = span.enter();
329 let ret = PrivateKey::generate_ec_p256(key_rng);
330 info!("Done generating EC P-256 key");
331 ret
332 })
333 .await
334 .context("could not join blocking task")?;
335 let ec_p256_key = KeyConfig {
336 kid: Alphanumeric.sample_string(&mut rng, 10),
337 password: None,
338 key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
339 };
340
341 let span = tracing::info_span!("ec_p384");
342 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
343 let ec_p384_key = task::spawn_blocking(move || {
344 let _entered = span.enter();
345 let ret = PrivateKey::generate_ec_p384(key_rng);
346 info!("Done generating EC P-256 key");
347 ret
348 })
349 .await
350 .context("could not join blocking task")?;
351 let ec_p384_key = KeyConfig {
352 kid: Alphanumeric.sample_string(&mut rng, 10),
353 password: None,
354 key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
355 };
356
357 let span = tracing::info_span!("ec_k256");
358 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
359 let ec_k256_key = task::spawn_blocking(move || {
360 let _entered = span.enter();
361 let ret = PrivateKey::generate_ec_k256(key_rng);
362 info!("Done generating EC secp256k1 key");
363 ret
364 })
365 .await
366 .context("could not join blocking task")?;
367 let ec_k256_key = KeyConfig {
368 kid: Alphanumeric.sample_string(&mut rng, 10),
369 password: None,
370 key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
371 };
372
373 Ok(Self {
374 encryption: Encryption::Value(Standard.sample(&mut rng)),
375 keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
376 })
377 }
378
379 pub(crate) fn test() -> Self {
380 let rsa_key = KeyConfig {
381 kid: "abcdef".to_owned(),
382 password: None,
383 key: Key::Value(
384 indoc::indoc! {r"
385 -----BEGIN PRIVATE KEY-----
386 MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
387 QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
388 scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
389 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
390 vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
391 N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
392 tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
393 Gh7BNzCeN+D6
394 -----END PRIVATE KEY-----
395 "}
396 .to_owned(),
397 ),
398 };
399 let ecdsa_key = KeyConfig {
400 kid: "ghijkl".to_owned(),
401 password: None,
402 key: Key::Value(
403 indoc::indoc! {r"
404 -----BEGIN PRIVATE KEY-----
405 MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
406 NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
407 OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
408 -----END PRIVATE KEY-----
409 "}
410 .to_owned(),
411 ),
412 };
413
414 Self {
415 encryption: Encryption::Value([0xEA; 32]),
416 keys: vec![rsa_key, ecdsa_key],
417 }
418 }
419}