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
27fn example_secret() -> &'static str {
28 "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
29}
30
31#[derive(Clone, Debug)]
36pub enum Password {
37 File(Utf8PathBuf),
38 Value(String),
39}
40
41#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
43struct PasswordRaw {
44 #[schemars(with = "Option<String>")]
45 #[serde(skip_serializing_if = "Option::is_none")]
46 password_file: Option<Utf8PathBuf>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 password: Option<String>,
49}
50
51impl TryFrom<PasswordRaw> for Option<Password> {
52 type Error = anyhow::Error;
53
54 fn try_from(value: PasswordRaw) -> Result<Self, Self::Error> {
55 match (value.password, value.password_file) {
56 (None, None) => Ok(None),
57 (None, Some(path)) => Ok(Some(Password::File(path))),
58 (Some(password), None) => Ok(Some(Password::Value(password))),
59 (Some(_), Some(_)) => bail!("Cannot specify both `password` and `password_file`"),
60 }
61 }
62}
63
64impl From<Option<Password>> for PasswordRaw {
65 fn from(value: Option<Password>) -> Self {
66 match value {
67 Some(Password::File(path)) => PasswordRaw {
68 password_file: Some(path),
69 password: None,
70 },
71 Some(Password::Value(password)) => PasswordRaw {
72 password_file: None,
73 password: Some(password),
74 },
75 None => PasswordRaw {
76 password_file: None,
77 password: None,
78 },
79 }
80 }
81}
82
83#[derive(Clone, Debug)]
88pub enum Key {
89 File(Utf8PathBuf),
90 Value(String),
91}
92
93#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
95struct KeyRaw {
96 #[schemars(with = "Option<String>")]
97 #[serde(skip_serializing_if = "Option::is_none")]
98 key_file: Option<Utf8PathBuf>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 key: Option<String>,
101}
102
103impl TryFrom<KeyRaw> for Key {
104 type Error = anyhow::Error;
105
106 fn try_from(value: KeyRaw) -> Result<Key, Self::Error> {
107 match (value.key, value.key_file) {
108 (None, None) => bail!("Missing `key` or `key_file`"),
109 (None, Some(path)) => Ok(Key::File(path)),
110 (Some(key), None) => Ok(Key::Value(key)),
111 (Some(_), Some(_)) => bail!("Cannot specify both `key` and `key_file`"),
112 }
113 }
114}
115
116impl From<Key> for KeyRaw {
117 fn from(value: Key) -> Self {
118 match value {
119 Key::File(path) => KeyRaw {
120 key_file: Some(path),
121 key: None,
122 },
123 Key::Value(key) => KeyRaw {
124 key_file: None,
125 key: Some(key),
126 },
127 }
128 }
129}
130
131#[serde_as]
133#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
134pub struct KeyConfig {
135 kid: 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 Ok(JsonWebKey::new(private_key)
182 .with_kid(self.kid.clone())
183 .with_use(mas_iana::jose::JsonWebKeyUse::Sig))
184 }
185}
186
187#[derive(Debug, Clone)]
189pub enum Encryption {
190 File(Utf8PathBuf),
191 Value([u8; 32]),
192}
193
194#[serde_as]
196#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
197struct EncryptionRaw {
198 #[schemars(with = "Option<String>")]
200 #[serde(skip_serializing_if = "Option::is_none")]
201 encryption_file: Option<Utf8PathBuf>,
202
203 #[schemars(
205 with = "Option<String>",
206 regex(pattern = r"[0-9a-fA-F]{64}"),
207 example = "example_secret"
208 )]
209 #[serde_as(as = "Option<serde_with::hex::Hex>")]
210 #[serde(skip_serializing_if = "Option::is_none")]
211 encryption: Option<[u8; 32]>,
212}
213
214impl TryFrom<EncryptionRaw> for Encryption {
215 type Error = anyhow::Error;
216
217 fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
218 match (value.encryption, value.encryption_file) {
219 (None, None) => bail!("Missing `encryption` or `encryption_file`"),
220 (None, Some(path)) => Ok(Encryption::File(path)),
221 (Some(encryption), None) => Ok(Encryption::Value(encryption)),
222 (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
223 }
224 }
225}
226
227impl From<Encryption> for EncryptionRaw {
228 fn from(value: Encryption) -> Self {
229 match value {
230 Encryption::File(path) => EncryptionRaw {
231 encryption_file: Some(path),
232 encryption: None,
233 },
234 Encryption::Value(encryption) => EncryptionRaw {
235 encryption_file: None,
236 encryption: Some(encryption),
237 },
238 }
239 }
240}
241
242#[serde_as]
244#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
245pub struct SecretsConfig {
246 #[schemars(with = "EncryptionRaw")]
248 #[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
249 #[serde(flatten)]
250 encryption: Encryption,
251
252 #[serde(default)]
254 keys: Vec<KeyConfig>,
255}
256
257impl SecretsConfig {
258 #[tracing::instrument(name = "secrets.load", skip_all)]
264 pub async fn key_store(&self) -> anyhow::Result<Keystore> {
265 let web_keys = try_join_all(self.keys.iter().map(KeyConfig::json_web_key)).await?;
266
267 Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
268 }
269
270 pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
276 Ok(Encrypter::new(&self.encryption().await?))
277 }
278
279 pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
285 match self.encryption {
287 Encryption::Value(encryption) => Ok(encryption),
288 Encryption::File(ref path) => {
289 let mut bytes = [0; 32];
290 let content = tokio::fs::read(path).await?;
291 hex::decode_to_slice(content, &mut bytes).context(
292 "Content of `encryption_file` must contain hex characters \
293 encoding exactly 32 bytes",
294 )?;
295 Ok(bytes)
296 }
297 }
298 }
299}
300
301impl ConfigurationSection for SecretsConfig {
302 const PATH: Option<&'static str> = Some("secrets");
303}
304
305impl SecretsConfig {
306 #[tracing::instrument(skip_all)]
307 pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
308 where
309 R: Rng + Send,
310 {
311 info!("Generating keys...");
312
313 let span = tracing::info_span!("rsa");
314 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
315 let rsa_key = task::spawn_blocking(move || {
316 let _entered = span.enter();
317 let ret = PrivateKey::generate_rsa(key_rng).unwrap();
318 info!("Done generating RSA key");
319 ret
320 })
321 .await
322 .context("could not join blocking task")?;
323 let rsa_key = KeyConfig {
324 kid: Alphanumeric.sample_string(&mut rng, 10),
325 password: None,
326 key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
327 };
328
329 let span = tracing::info_span!("ec_p256");
330 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
331 let ec_p256_key = task::spawn_blocking(move || {
332 let _entered = span.enter();
333 let ret = PrivateKey::generate_ec_p256(key_rng);
334 info!("Done generating EC P-256 key");
335 ret
336 })
337 .await
338 .context("could not join blocking task")?;
339 let ec_p256_key = KeyConfig {
340 kid: Alphanumeric.sample_string(&mut rng, 10),
341 password: None,
342 key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
343 };
344
345 let span = tracing::info_span!("ec_p384");
346 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
347 let ec_p384_key = task::spawn_blocking(move || {
348 let _entered = span.enter();
349 let ret = PrivateKey::generate_ec_p384(key_rng);
350 info!("Done generating EC P-256 key");
351 ret
352 })
353 .await
354 .context("could not join blocking task")?;
355 let ec_p384_key = KeyConfig {
356 kid: Alphanumeric.sample_string(&mut rng, 10),
357 password: None,
358 key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
359 };
360
361 let span = tracing::info_span!("ec_k256");
362 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
363 let ec_k256_key = task::spawn_blocking(move || {
364 let _entered = span.enter();
365 let ret = PrivateKey::generate_ec_k256(key_rng);
366 info!("Done generating EC secp256k1 key");
367 ret
368 })
369 .await
370 .context("could not join blocking task")?;
371 let ec_k256_key = KeyConfig {
372 kid: Alphanumeric.sample_string(&mut rng, 10),
373 password: None,
374 key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
375 };
376
377 Ok(Self {
378 encryption: Encryption::Value(Standard.sample(&mut rng)),
379 keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
380 })
381 }
382
383 pub(crate) fn test() -> Self {
384 let rsa_key = KeyConfig {
385 kid: "abcdef".to_owned(),
386 password: None,
387 key: Key::Value(
388 indoc::indoc! {r"
389 -----BEGIN PRIVATE KEY-----
390 MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
391 QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
392 scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
393 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
394 vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
395 N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
396 tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
397 Gh7BNzCeN+D6
398 -----END PRIVATE KEY-----
399 "}
400 .to_owned(),
401 ),
402 };
403 let ecdsa_key = KeyConfig {
404 kid: "ghijkl".to_owned(),
405 password: None,
406 key: Key::Value(
407 indoc::indoc! {r"
408 -----BEGIN PRIVATE KEY-----
409 MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
410 NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
411 OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
412 -----END PRIVATE KEY-----
413 "}
414 .to_owned(),
415 ),
416 };
417
418 Self {
419 encryption: Encryption::Value([0xEA; 32]),
420 keys: vec![rsa_key, ecdsa_key],
421 }
422 }
423}