Skip to main content

cargo_codesign/
init.rs

1use crate::config::{LinuxMethod, MacosAuth};
2use std::fmt::Write;
3
4#[allow(clippy::struct_excessive_bools)]
5pub struct InitSelections {
6    pub macos: bool,
7    pub macos_auth: Option<MacosAuth>,
8    pub windows: bool,
9    pub linux: bool,
10    pub linux_method: Option<LinuxMethod>,
11    pub update: bool,
12}
13
14pub fn generate_sign_toml(selections: &InitSelections) -> String {
15    let mut out = String::new();
16
17    if selections.macos {
18        let auth = selections
19            .macos_auth
20            .as_ref()
21            .unwrap_or(&MacosAuth::AppleId);
22        writeln!(out, "[macos]").unwrap();
23        writeln!(out, "identity = \"Developer ID Application\"").unwrap();
24        writeln!(out, "entitlements = \"entitlements.plist\"").unwrap();
25        match auth {
26            MacosAuth::ApiKey => {
27                writeln!(out, "auth = \"api-key\"").unwrap();
28                writeln!(out).unwrap();
29                writeln!(out, "[macos.env]").unwrap();
30                writeln!(out, "certificate = \"MACOS_CERTIFICATE\"").unwrap();
31                writeln!(out, "certificate-password = \"MACOS_CERTIFICATE_PASSWORD\"").unwrap();
32                writeln!(out, "notarization-key = \"APPLE_NOTARIZATION_KEY\"").unwrap();
33                writeln!(out, "notarization-key-id = \"APPLE_NOTARIZATION_KEY_ID\"").unwrap();
34                writeln!(
35                    out,
36                    "notarization-issuer = \"APPLE_NOTARIZATION_ISSUER_ID\""
37                )
38                .unwrap();
39            }
40            MacosAuth::AppleId => {
41                writeln!(out, "auth = \"apple-id\"").unwrap();
42                writeln!(out).unwrap();
43                writeln!(out, "[macos.env]").unwrap();
44                writeln!(out, "apple-id = \"APPLE_ID\"").unwrap();
45                writeln!(out, "team-id = \"APPLE_TEAM_ID\"").unwrap();
46                writeln!(out, "app-password = \"APPLE_APP_PASSWORD\"").unwrap();
47            }
48        }
49        writeln!(out).unwrap();
50    }
51
52    if selections.windows {
53        writeln!(out, "[windows]").unwrap();
54        writeln!(
55            out,
56            "timestamp-server = \"http://timestamp.acs.microsoft.com\""
57        )
58        .unwrap();
59        writeln!(out).unwrap();
60        writeln!(out, "[windows.env]").unwrap();
61        writeln!(out, "tenant-id = \"AZURE_TENANT_ID\"").unwrap();
62        writeln!(out, "client-id = \"AZURE_CLIENT_ID\"").unwrap();
63        writeln!(out, "client-secret = \"AZURE_CLIENT_SECRET\"").unwrap();
64        writeln!(out, "endpoint = \"AZURE_SIGNING_ENDPOINT\"").unwrap();
65        writeln!(out, "account-name = \"AZURE_SIGNING_ACCOUNT_NAME\"").unwrap();
66        writeln!(out, "cert-profile = \"AZURE_SIGNING_CERT_PROFILE\"").unwrap();
67        writeln!(out).unwrap();
68    }
69
70    if selections.linux {
71        let method = selections
72            .linux_method
73            .as_ref()
74            .unwrap_or(&LinuxMethod::Cosign);
75        writeln!(out, "[linux]").unwrap();
76        match method {
77            LinuxMethod::Cosign => {
78                writeln!(out, "method = \"cosign\"").unwrap();
79                writeln!(out).unwrap();
80                writeln!(out, "[linux.env]").unwrap();
81                writeln!(out, "key = \"COSIGN_PRIVATE_KEY\"").unwrap();
82            }
83            LinuxMethod::Minisign => {
84                writeln!(out, "method = \"minisign\"").unwrap();
85                writeln!(out).unwrap();
86                writeln!(out, "[linux.env]").unwrap();
87                writeln!(out, "key = \"MINISIGN_PRIVATE_KEY\"").unwrap();
88            }
89            LinuxMethod::Gpg => {
90                writeln!(out, "method = \"gpg\"").unwrap();
91                writeln!(out).unwrap();
92                writeln!(out, "[linux.env]").unwrap();
93                writeln!(out, "key = \"GPG_PRIVATE_KEY\"").unwrap();
94            }
95        }
96        writeln!(out).unwrap();
97    }
98
99    if selections.update {
100        writeln!(out, "[update]").unwrap();
101        writeln!(out, "public-key = \"update-signing.pub\"").unwrap();
102        writeln!(out).unwrap();
103        writeln!(out, "[update.env]").unwrap();
104        writeln!(out, "signing-key = \"UPDATE_SIGNING_KEY\"").unwrap();
105        writeln!(out).unwrap();
106    }
107
108    out.trim_end().to_string() + "\n"
109}
110
111const BOOK_BASE_URL: &str = "https://sassman.github.io/cargo-codesign-rs";
112
113pub struct CredentialCheck {
114    pub env_var: String,
115    pub description: String,
116    pub is_set: bool,
117    pub help_url: String,
118}
119
120pub fn check_credentials(selections: &InitSelections) -> Vec<CredentialCheck> {
121    let mut checks = Vec::new();
122
123    if selections.macos {
124        let auth = selections
125            .macos_auth
126            .as_ref()
127            .unwrap_or(&MacosAuth::AppleId);
128        check_macos_creds(&mut checks, auth);
129    }
130
131    if selections.windows {
132        check_windows_creds(&mut checks);
133    }
134
135    if selections.linux {
136        let method = selections.linux_method.unwrap_or(LinuxMethod::Cosign);
137        check_linux_creds(&mut checks, method);
138    }
139
140    if selections.update {
141        check_cred(
142            &mut checks,
143            "UPDATE_SIGNING_KEY",
144            "ed25519 private key for update signing (run `cargo codesign keygen`)",
145            "update-signing/keygen.html",
146        );
147    }
148
149    checks
150}
151
152fn check_macos_creds(checks: &mut Vec<CredentialCheck>, auth: &MacosAuth) {
153    match auth {
154        MacosAuth::ApiKey => {
155            check_cred(
156                checks,
157                "MACOS_CERTIFICATE",
158                "base64-encoded .p12 Developer ID certificate",
159                "macos/credentials.html",
160            );
161            check_cred(
162                checks,
163                "MACOS_CERTIFICATE_PASSWORD",
164                "password for the .p12 certificate",
165                "macos/credentials.html",
166            );
167            check_cred(
168                checks,
169                "APPLE_NOTARIZATION_KEY",
170                "base64-encoded App Store Connect API key (.p8)",
171                "macos/auth-modes.html",
172            );
173            check_cred(
174                checks,
175                "APPLE_NOTARIZATION_KEY_ID",
176                "API key ID from App Store Connect > Keys",
177                "macos/auth-modes.html",
178            );
179            check_cred(
180                checks,
181                "APPLE_NOTARIZATION_ISSUER_ID",
182                "Issuer ID from App Store Connect > Keys",
183                "macos/auth-modes.html",
184            );
185        }
186        MacosAuth::AppleId => {
187            check_cred(
188                checks,
189                "APPLE_ID",
190                "your Apple ID email address",
191                "macos/credentials.html",
192            );
193            check_cred(
194                checks,
195                "APPLE_TEAM_ID",
196                "Team ID from App Store Connect > Membership",
197                "macos/credentials.html",
198            );
199            check_cred(
200                checks,
201                "APPLE_APP_PASSWORD",
202                "app-specific password for notarization",
203                "macos/auth-modes.html",
204            );
205        }
206    }
207}
208
209fn check_windows_creds(checks: &mut Vec<CredentialCheck>) {
210    check_cred(
211        checks,
212        "AZURE_TENANT_ID",
213        "Azure AD tenant ID",
214        "windows/credentials.html",
215    );
216    check_cred(
217        checks,
218        "AZURE_CLIENT_ID",
219        "Azure AD application (client) ID",
220        "windows/credentials.html",
221    );
222    check_cred(
223        checks,
224        "AZURE_CLIENT_SECRET",
225        "Azure AD client secret",
226        "windows/credentials.html",
227    );
228    check_cred(
229        checks,
230        "AZURE_SIGNING_ENDPOINT",
231        "Azure Trusted Signing endpoint URL",
232        "windows/credentials.html",
233    );
234    check_cred(
235        checks,
236        "AZURE_SIGNING_ACCOUNT_NAME",
237        "Trusted Signing account name",
238        "windows/credentials.html",
239    );
240    check_cred(
241        checks,
242        "AZURE_SIGNING_CERT_PROFILE",
243        "certificate profile name",
244        "windows/credentials.html",
245    );
246}
247
248fn check_linux_creds(checks: &mut Vec<CredentialCheck>, method: LinuxMethod) {
249    match method {
250        LinuxMethod::Cosign => {
251            check_cred(
252                checks,
253                "COSIGN_PRIVATE_KEY",
254                "cosign private key (or use keyless OIDC in CI)",
255                "linux/credentials.html",
256            );
257        }
258        LinuxMethod::Minisign => {
259            check_cred(
260                checks,
261                "MINISIGN_PRIVATE_KEY",
262                "minisign private key",
263                "linux/credentials.html",
264            );
265        }
266        LinuxMethod::Gpg => {
267            check_cred(
268                checks,
269                "GPG_PRIVATE_KEY",
270                "GPG private key (armor-encoded)",
271                "linux/credentials.html",
272            );
273        }
274    }
275}
276
277fn check_cred(checks: &mut Vec<CredentialCheck>, env_var: &str, description: &str, path: &str) {
278    let is_set = std::env::var(env_var).is_ok_and(|v| !v.is_empty());
279    checks.push(CredentialCheck {
280        env_var: env_var.to_string(),
281        description: description.to_string(),
282        is_set,
283        help_url: format!("{BOOK_BASE_URL}/{path}"),
284    });
285}
286
287pub fn print_credential_report(checks: &[CredentialCheck]) {
288    for check in checks {
289        if check.is_set {
290            eprintln!("  \u{2713} {:<35} set", check.env_var);
291        } else {
292            eprintln!("  \u{2717} {:<35} {}", check.env_var, check.description);
293        }
294    }
295
296    let missing_urls: Vec<&str> = checks
297        .iter()
298        .filter(|c| !c.is_set)
299        .map(|c| c.help_url.as_str())
300        .collect::<std::collections::BTreeSet<_>>()
301        .into_iter()
302        .collect();
303
304    if !missing_urls.is_empty() {
305        eprintln!();
306        eprintln!("How to obtain missing credentials:");
307        for url in &missing_urls {
308            eprintln!("  \u{2192} {url}");
309        }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn credential_check_detects_set_env_var() {
319        std::env::set_var("CARGO_CODESIGN_TEST_INIT_1", "value");
320        let mut checks = Vec::new();
321        check_cred(
322            &mut checks,
323            "CARGO_CODESIGN_TEST_INIT_1",
324            "test",
325            "test.html",
326        );
327        assert!(checks[0].is_set);
328        std::env::remove_var("CARGO_CODESIGN_TEST_INIT_1");
329    }
330
331    #[test]
332    fn credential_check_detects_missing_env_var() {
333        std::env::remove_var("CARGO_CODESIGN_NONEXISTENT_VAR_XYZ");
334        let mut checks = Vec::new();
335        check_cred(
336            &mut checks,
337            "CARGO_CODESIGN_NONEXISTENT_VAR_XYZ",
338            "test",
339            "test.html",
340        );
341        assert!(!checks[0].is_set);
342    }
343
344    #[test]
345    fn credential_check_macos_apple_id_returns_three() {
346        let selections = InitSelections {
347            macos: true,
348            macos_auth: Some(MacosAuth::AppleId),
349            windows: false,
350            linux: false,
351            linux_method: None,
352            update: false,
353        };
354        let checks = check_credentials(&selections);
355        assert_eq!(checks.len(), 3);
356    }
357
358    #[test]
359    fn credential_check_macos_api_key_returns_five() {
360        let selections = InitSelections {
361            macos: true,
362            macos_auth: Some(MacosAuth::ApiKey),
363            windows: false,
364            linux: false,
365            linux_method: None,
366            update: false,
367        };
368        let checks = check_credentials(&selections);
369        assert_eq!(checks.len(), 5);
370    }
371
372    #[test]
373    fn credential_check_windows_returns_six() {
374        let selections = InitSelections {
375            macos: false,
376            macos_auth: None,
377            windows: true,
378            linux: false,
379            linux_method: None,
380            update: false,
381        };
382        let checks = check_credentials(&selections);
383        assert_eq!(checks.len(), 6);
384    }
385
386    #[test]
387    fn help_url_contains_book_base() {
388        let selections = InitSelections {
389            macos: true,
390            macos_auth: Some(MacosAuth::AppleId),
391            windows: false,
392            linux: false,
393            linux_method: None,
394            update: false,
395        };
396        let checks = check_credentials(&selections);
397        assert!(checks.iter().all(|c| c.help_url.starts_with(BOOK_BASE_URL)));
398    }
399}