apple_codesign/cli/
config.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use {
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/// Configuration file profile definition.
24#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
25#[serde(deny_unknown_fields, rename_all = "kebab-case")]
26pub struct Config {
27    /// Configuration for the sign command.
28    #[serde(default)]
29    pub sign: SignConfig,
30
31    #[serde(default)]
32    pub remote_sign: RemoteSignConfig,
33}
34
35/// Configuration for the sign command.
36#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct SignConfig {
39    /// Defines a source for the cryptographic signing key.
40    #[serde(default)]
41    pub signer: CertificateSource,
42
43    /// Keys are scope paths. Values are per-path configs.
44    #[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    /// Defines a source for the cryptographic signing key.
52    #[serde(default)]
53    pub signer: CertificateSource,
54}
55
56/// Used to instantiate [Config] instances.
57#[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    /// Add the $XDG_CONFIG/rcodesign/rcodesign.toml user config file if it exists.
86    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    /// Merge a config file from `pwd`/rcodesign.toml.
98    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    /// Merge with environment variables.
110    ///
111    /// Must be called after [profile()] to ensure environment variables are
112    /// mapped to the current profile.
113    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    /// Add a TOML config file to this instance.
124    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    /// Add a TOML string config to this instance.
132    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    /// Merge a [Config] struct into this builder
139    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    /// Load the named profile instead of the `[default]` profile.
148    pub fn profile(mut self, profile: String) -> Self {
149        self.loader = self.loader.select(profile);
150        self
151    }
152
153    /// Obtain a config profile.
154    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}