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}