1use {
6 crate::{
7 cli::{certificate_source::CertificateSource, ScopedSigningSettingsValues},
8 error::AppleCodesignError,
9 },
10 figment::{
11 providers::{Env, Format, Serialized, Toml},
12 Figment,
13 },
14 log::debug,
15 serde::{Deserialize, Serialize},
16 std::{
17 collections::BTreeMap,
18 ops::{Deref, DerefMut},
19 path::Path,
20 },
21};
22
23#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
25#[serde(deny_unknown_fields, rename_all = "kebab-case")]
26pub struct Config {
27 #[serde(default)]
29 pub sign: SignConfig,
30
31 #[serde(default)]
32 pub remote_sign: RemoteSignConfig,
33}
34
35#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct SignConfig {
39 #[serde(default)]
41 pub signer: CertificateSource,
42
43 #[serde(default, rename = "path", skip_serializing_if = "BTreeMap::is_empty")]
45 pub paths: BTreeMap<String, ScopedSigningSettingsValues>,
46}
47
48#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
49#[serde(deny_unknown_fields)]
50pub struct RemoteSignConfig {
51 #[serde(default)]
53 pub signer: CertificateSource,
54}
55
56#[derive(Clone)]
58pub struct ConfigBuilder {
59 loader: Figment,
60}
61
62impl Default for ConfigBuilder {
63 fn default() -> Self {
64 Self {
65 loader: Figment::new(),
66 }
67 }
68}
69
70impl Deref for ConfigBuilder {
71 type Target = Figment;
72
73 fn deref(&self) -> &Self::Target {
74 &self.loader
75 }
76}
77
78impl DerefMut for ConfigBuilder {
79 fn deref_mut(&mut self) -> &mut Self::Target {
80 &mut self.loader
81 }
82}
83
84impl ConfigBuilder {
85 pub fn with_user_config_file(mut self) -> Self {
87 if let Some(base) = dirs::config_dir() {
88 let p = base.join("rcodesign").join("rcodesign.toml");
89 debug!("registering user config file: {}", p.display());
90
91 self.loader = self.loader.merge(Toml::file(p).nested());
92 }
93
94 self
95 }
96
97 pub fn with_cwd_config_file(mut self) -> Self {
99 if let Ok(cwd) = std::env::current_dir() {
100 let p = cwd.join("rcodesign.toml");
101 debug!("registering cwd config file: {}", p.display());
102
103 self.loader = self.loader.merge(Toml::file(p).nested());
104 }
105
106 self
107 }
108
109 pub fn with_env_prefix(mut self) -> Self {
114 debug!("registering RCODESIGN_ environment variable config source");
115 let env = Env::prefixed("RCODESIGN_")
116 .split("_")
117 .profile(self.loader.profile().to_string());
118
119 self.loader = self.loader.merge(env);
120 self
121 }
122
123 pub fn toml_file(mut self, path: impl AsRef<Path>) -> Self {
125 let path = path.as_ref();
126 debug!("registering custom config file: {}", path.display());
127 self.loader = self.loader.merge(Toml::file(path).nested());
128 self
129 }
130
131 pub fn toml_string(mut self, data: &str) -> Self {
133 debug!("registering TOML string config data");
134 self.loader = self.loader.merge(Toml::string(data).nested());
135 self
136 }
137
138 pub fn with_config_struct(mut self, config: Config) -> Self {
140 debug!("registering config struct");
141 let serialized = Serialized::defaults(config).profile(self.loader.profile().to_string());
142
143 self.loader = self.loader.merge(serialized);
144 self
145 }
146
147 pub fn profile(mut self, profile: String) -> Self {
149 self.loader = self.loader.select(profile);
150 self
151 }
152
153 pub fn config(self) -> Result<Config, AppleCodesignError> {
155 Ok(self.loader.extract()?)
156 }
157}
158
159#[cfg(test)]
160mod test {
161 use super::*;
162 use {
163 crate::cli::certificate_source::{
164 MacosKeychainSigningKey, P12SigningKey, PemSigningKey, RemoteSigningKey,
165 SmartcardSigningKey, WindowsStoreSigningKey,
166 },
167 std::path::PathBuf,
168 };
169
170 #[test]
171 fn default_config() {
172 let c = ConfigBuilder::default().config().unwrap();
173
174 assert_eq!(c, Config::default());
175 }
176
177 #[test]
178 fn smartcard_signer() {
179 let c = ConfigBuilder::default()
180 .toml_string(
181 r#"
182 [default.sign]
183 signer.smartcard = { slot = "9c" }
184 "#,
185 )
186 .config()
187 .unwrap();
188
189 assert_eq!(
190 c.sign.signer,
191 CertificateSource {
192 smartcard_key: Some(SmartcardSigningKey {
193 slot: Some("9c".into()),
194 pin: None,
195 pin_env: None,
196 }),
197 ..Default::default()
198 }
199 );
200
201 let c = ConfigBuilder::default()
202 .toml_string(
203 r#"
204 [default.sign]
205 signer.smartcard = { slot = "9c", pin = "1234" }
206 "#,
207 )
208 .config()
209 .unwrap();
210 assert_eq!(
211 c.sign.signer,
212 CertificateSource {
213 smartcard_key: Some(SmartcardSigningKey {
214 slot: Some("9c".into()),
215 pin: Some("1234".into()),
216 pin_env: None,
217 }),
218 ..Default::default()
219 }
220 );
221 }
222
223 #[test]
224 fn macos_keychain_signer() {
225 assert_eq!(
226 ConfigBuilder::default()
227 .toml_string(
228 r#"
229 [default.sign]
230 signer.macos_keychain = { sha256_fingerprint = "deadbeef" }
231 "#,
232 )
233 .config()
234 .unwrap()
235 .sign
236 .signer,
237 CertificateSource {
238 macos_keychain_key: Some(MacosKeychainSigningKey {
239 domains: vec![],
240 sha256_fingerprint: Some("deadbeef".into()),
241 }),
242 ..Default::default()
243 }
244 );
245 }
246
247 #[test]
248 fn pem_signer() {
249 assert_eq!(
250 ConfigBuilder::default()
251 .toml_string(
252 r#"
253 [default.sign]
254 signer.pem.files = ["key.pem", "cert.pem"]
255 "#
256 )
257 .config()
258 .unwrap()
259 .sign
260 .signer,
261 CertificateSource {
262 pem_path_key: Some(PemSigningKey {
263 paths: vec![PathBuf::from("key.pem"), PathBuf::from("cert.pem")]
264 }),
265 ..Default::default()
266 }
267 );
268 }
269
270 #[test]
271 fn p12_signer() {
272 assert_eq!(
273 ConfigBuilder::default()
274 .toml_string(
275 r#"
276 [default.sign]
277 signer.p12 = { path = "key.p12", password = "password" }
278 "#
279 )
280 .config()
281 .unwrap()
282 .sign
283 .signer,
284 CertificateSource {
285 p12_key: Some(P12SigningKey {
286 path: Some(PathBuf::from("key.p12")),
287 password: Some("password".into()),
288 password_path: None
289 }),
290 ..Default::default()
291 }
292 );
293 assert_eq!(
294 ConfigBuilder::default()
295 .toml_string(
296 r#"
297 [default.sign]
298 signer.p12 = { path = "key.p12", password_path = "path/to/file" }
299 "#
300 )
301 .config()
302 .unwrap()
303 .sign
304 .signer,
305 CertificateSource {
306 p12_key: Some(P12SigningKey {
307 path: Some(PathBuf::from("key.p12")),
308 password: None,
309 password_path: Some("path/to/file".into()),
310 }),
311 ..Default::default()
312 }
313 );
314 }
315
316 #[test]
317 fn remote_signer() {
318 assert_eq!(
319 ConfigBuilder::default()
320 .toml_string(
321 r#"
322 [default.sign]
323 signer.remote.public_key = "DEADBEEF"
324 "#
325 )
326 .config()
327 .unwrap()
328 .sign
329 .signer,
330 CertificateSource {
331 remote_signing_key: Some(RemoteSigningKey {
332 public_key: Some("DEADBEEF".into()),
333 ..Default::default()
334 }),
335 ..Default::default()
336 }
337 );
338
339 assert_eq!(
340 ConfigBuilder::default()
341 .toml_string(
342 r#"
343 [default.sign]
344 signer.remote.public_key_pem_path = "path/to/cert.pem"
345 "#
346 )
347 .config()
348 .unwrap()
349 .sign
350 .signer,
351 CertificateSource {
352 remote_signing_key: Some(RemoteSigningKey {
353 public_key_pem_path: Some("path/to/cert.pem".into()),
354 ..Default::default()
355 }),
356 ..Default::default()
357 }
358 );
359
360 assert_eq!(
361 ConfigBuilder::default()
362 .toml_string(
363 r#"
364 [default.sign]
365 signer.remote.shared_secret = "SECRET"
366 "#
367 )
368 .config()
369 .unwrap()
370 .sign
371 .signer,
372 CertificateSource {
373 remote_signing_key: Some(RemoteSigningKey {
374 shared_secret: Some("SECRET".into()),
375 ..Default::default()
376 }),
377 ..Default::default()
378 }
379 );
380 }
381
382 #[test]
383 fn windows_store() {
384 assert_eq!(
385 ConfigBuilder::default()
386 .toml_string(
387 r#"
388 [default.sign]
389 signer.windows_store = { stores = ["user"], sha1_fingerprint = "DEADBEEF" }
390 "#
391 )
392 .config()
393 .unwrap()
394 .sign
395 .signer,
396 CertificateSource {
397 windows_store_key: Some(WindowsStoreSigningKey {
398 stores: vec!["user".into()],
399 sha1_fingerprint: Some("DEADBEEF".into()),
400 }),
401 ..Default::default()
402 }
403 );
404 }
405
406 #[test]
407 fn paths_toml() {
408 assert_eq!(
409 ConfigBuilder::default()
410 .toml_string(
411 r#"
412 [default.sign.path."Contents/MacOS/extra-bin"]
413 binary_identifier = "ident"
414 code_requirements_file = "reqs"
415 code_resources_file = "code-resources"
416 code_signature_flags = ["runtime"]
417 digests = ["sha1", "sha256"]
418 entitlements_xml_file = "entitlements.plist"
419 launch_constraints_self_file = "lc-self"
420 launch_constraints_parent_file = "lc-parent"
421 launch_constraints_responsible_file = "lc-responsible"
422 library_constraints_file = "lc-library"
423 runtime_version = "11.0.0"
424 info_plist_file = "Info.plist"
425 "#
426 )
427 .config()
428 .unwrap()
429 .sign
430 .paths,
431 BTreeMap::from_iter([(
432 "Contents/MacOS/extra-bin".into(),
433 ScopedSigningSettingsValues {
434 binary_identifier: Some("ident".into()),
435 code_requirements_file: Some("reqs".into()),
436 code_resources_file: Some("code-resources".into()),
437 code_signature_flags: vec!["runtime".into()],
438 digests: vec!["sha1".into(), "sha256".into()],
439 entitlements_xml_file: Some("entitlements.plist".into()),
440 launch_constraints_self_file: Some("lc-self".into()),
441 launch_constraints_parent_file: Some("lc-parent".into()),
442 launch_constraints_responsible_file: Some("lc-responsible".into()),
443 library_constraints_file: Some("lc-library".into()),
444 runtime_version: Some("11.0.0".into()),
445 info_plist_file: Some("Info.plist".into()),
446 }
447 )])
448 );
449 }
450}