1use std::borrow::Cow;
8
9use anyhow::{Context, bail};
10use camino::Utf8PathBuf;
11use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
12use mas_keystore::{Encrypter, Keystore, PrivateKey};
13use rand::{
14 Rng, SeedableRng,
15 distributions::{Alphanumeric, DistString, Standard},
16 prelude::Distribution as _,
17};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use serde_with::serde_as;
21use tokio::task;
22use tracing::info;
23
24use super::ConfigurationSection;
25
26fn example_secret() -> &'static str {
27 "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
28}
29
30#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
31pub struct KeyConfig {
32 kid: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
35 password: Option<String>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
38 #[schemars(with = "Option<String>")]
39 password_file: Option<Utf8PathBuf>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
42 key: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
45 #[schemars(with = "Option<String>")]
46 key_file: Option<Utf8PathBuf>,
47}
48
49#[serde_as]
51#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
52pub struct SecretsConfig {
53 #[schemars(
55 with = "String",
56 regex(pattern = r"[0-9a-fA-F]{64}"),
57 example = "example_secret"
58 )]
59 #[serde_as(as = "serde_with::hex::Hex")]
60 pub encryption: [u8; 32],
61
62 #[serde(default)]
64 keys: Vec<KeyConfig>,
65}
66
67impl SecretsConfig {
68 #[tracing::instrument(name = "secrets.load", skip_all, err(Debug))]
74 pub async fn key_store(&self) -> anyhow::Result<Keystore> {
75 let mut keys = Vec::with_capacity(self.keys.len());
76 for item in &self.keys {
77 let password = match (&item.password, &item.password_file) {
78 (None, None) => None,
79 (Some(_), Some(_)) => {
80 bail!("Cannot specify both `password` and `password_file`")
81 }
82 (Some(password), None) => Some(Cow::Borrowed(password)),
83 (None, Some(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)),
84 };
85
86 let key = match (&item.key, &item.key_file) {
88 (None, None) => bail!("Missing `key` or `key_file`"),
89 (Some(_), Some(_)) => bail!("Cannot specify both `key` and `key_file`"),
90 (Some(key), None) => {
91 if let Some(password) = password {
93 PrivateKey::load_encrypted_pem(key, password.as_bytes())?
94 } else {
95 PrivateKey::load_pem(key)?
96 }
97 }
98 (None, Some(path)) => {
99 let key = tokio::fs::read(path).await?;
102 if let Some(password) = password {
103 PrivateKey::load_encrypted(&key, password.as_bytes())?
104 } else {
105 PrivateKey::load(&key)?
106 }
107 }
108 };
109
110 let key = JsonWebKey::new(key)
111 .with_kid(item.kid.clone())
112 .with_use(mas_iana::jose::JsonWebKeyUse::Sig);
113 keys.push(key);
114 }
115
116 let keys = JsonWebKeySet::new(keys);
117 Ok(Keystore::new(keys))
118 }
119
120 #[must_use]
122 pub fn encrypter(&self) -> Encrypter {
123 Encrypter::new(&self.encryption)
124 }
125}
126
127impl ConfigurationSection for SecretsConfig {
128 const PATH: Option<&'static str> = Some("secrets");
129
130 fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
131 for (index, key) in self.keys.iter().enumerate() {
132 let annotate = |mut error: figment::Error| {
133 error.metadata = figment
134 .find_metadata(&format!("{root}.keys", root = Self::PATH.unwrap()))
135 .cloned();
136 error.profile = Some(figment::Profile::Default);
137 error.path = vec![
138 Self::PATH.unwrap().to_owned(),
139 "keys".to_owned(),
140 index.to_string(),
141 ];
142 Err(error)
143 };
144
145 if key.key.is_none() && key.key_file.is_none() {
146 return annotate(figment::Error::from(
147 "Missing `key` or `key_file`".to_owned(),
148 ));
149 }
150
151 if key.key.is_some() && key.key_file.is_some() {
152 return annotate(figment::Error::from(
153 "Cannot specify both `key` and `key_file`".to_owned(),
154 ));
155 }
156
157 if key.password.is_some() && key.password_file.is_some() {
158 return annotate(figment::Error::from(
159 "Cannot specify both `password` and `password_file`".to_owned(),
160 ));
161 }
162 }
163
164 Ok(())
165 }
166}
167
168impl SecretsConfig {
169 #[tracing::instrument(skip_all)]
170 pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
171 where
172 R: Rng + Send,
173 {
174 info!("Generating keys...");
175
176 let span = tracing::info_span!("rsa");
177 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
178 let rsa_key = task::spawn_blocking(move || {
179 let _entered = span.enter();
180 let ret = PrivateKey::generate_rsa(key_rng).unwrap();
181 info!("Done generating RSA key");
182 ret
183 })
184 .await
185 .context("could not join blocking task")?;
186 let rsa_key = KeyConfig {
187 kid: Alphanumeric.sample_string(&mut rng, 10),
188 password: None,
189 password_file: None,
190 key: Some(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
191 key_file: None,
192 };
193
194 let span = tracing::info_span!("ec_p256");
195 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
196 let ec_p256_key = task::spawn_blocking(move || {
197 let _entered = span.enter();
198 let ret = PrivateKey::generate_ec_p256(key_rng);
199 info!("Done generating EC P-256 key");
200 ret
201 })
202 .await
203 .context("could not join blocking task")?;
204 let ec_p256_key = KeyConfig {
205 kid: Alphanumeric.sample_string(&mut rng, 10),
206 password: None,
207 password_file: None,
208 key: Some(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
209 key_file: None,
210 };
211
212 let span = tracing::info_span!("ec_p384");
213 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
214 let ec_p384_key = task::spawn_blocking(move || {
215 let _entered = span.enter();
216 let ret = PrivateKey::generate_ec_p384(key_rng);
217 info!("Done generating EC P-256 key");
218 ret
219 })
220 .await
221 .context("could not join blocking task")?;
222 let ec_p384_key = KeyConfig {
223 kid: Alphanumeric.sample_string(&mut rng, 10),
224 password: None,
225 password_file: None,
226 key: Some(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
227 key_file: None,
228 };
229
230 let span = tracing::info_span!("ec_k256");
231 let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
232 let ec_k256_key = task::spawn_blocking(move || {
233 let _entered = span.enter();
234 let ret = PrivateKey::generate_ec_k256(key_rng);
235 info!("Done generating EC secp256k1 key");
236 ret
237 })
238 .await
239 .context("could not join blocking task")?;
240 let ec_k256_key = KeyConfig {
241 kid: Alphanumeric.sample_string(&mut rng, 10),
242 password: None,
243 password_file: None,
244 key: Some(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
245 key_file: None,
246 };
247
248 Ok(Self {
249 encryption: Standard.sample(&mut rng),
250 keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
251 })
252 }
253
254 pub(crate) fn test() -> Self {
255 let rsa_key = KeyConfig {
256 kid: "abcdef".to_owned(),
257 password: None,
258 password_file: None,
259 key: Some(
260 indoc::indoc! {r"
261 -----BEGIN PRIVATE KEY-----
262 MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
263 QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
264 scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
265 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
266 vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
267 N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
268 tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
269 Gh7BNzCeN+D6
270 -----END PRIVATE KEY-----
271 "}
272 .to_owned(),
273 ),
274 key_file: None,
275 };
276 let ecdsa_key = KeyConfig {
277 kid: "ghijkl".to_owned(),
278 password: None,
279 password_file: None,
280 key: Some(
281 indoc::indoc! {r"
282 -----BEGIN PRIVATE KEY-----
283 MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
284 NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
285 OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
286 -----END PRIVATE KEY-----
287 "}
288 .to_owned(),
289 ),
290 key_file: None,
291 };
292
293 Self {
294 encryption: [0xEA; 32],
295 keys: vec![rsa_key, ecdsa_key],
296 }
297 }
298}