apple_clis/security/
find_certificate.rs

1use crate::prelude::*;
2
3use openssl::x509::X509;
4use openssl::{error::ErrorStack as OpenSslError, nid::Nid, x509::X509NameRef};
5use serde::Serialize;
6use thiserror::Error;
7
8use super::SecurityCLIInstance;
9
10impl SecurityCLIInstance {
11	const DEVELOPER_NAME_SCHEMAS: [&'static str; 2] = ["Developer:", "Development:"];
12
13	fn get_pem_list(&self, name_substr: &str) -> bossy::Result<bossy::Output> {
14		self
15			.bossy_command()
16			.with_args(["find-certificate", "-p", "-a", "-c", name_substr])
17			.run_and_wait_for_output()
18	}
19
20	fn get_developer_pem_list(&self) -> bossy::Result<Vec<bossy::Output>> {
21		Self::DEVELOPER_NAME_SCHEMAS
22			.iter()
23			.map(|name| self.get_pem_list(name))
24			.collect()
25	}
26
27	pub fn get_developer_pems(&self) -> Result<Vec<X509>> {
28		let certs = self
29			.get_developer_pem_list()?
30			.into_iter()
31			.map(|output| X509::stack_from_pem(output.stdout()).map_err(Error::X509ParseFailed))
32			.collect::<Result<Vec<_>>>()?;
33		let certs = certs.into_iter().flatten();
34		Ok(certs.collect())
35	}
36
37	#[instrument(skip_all, ret)]
38	pub fn get_developer_certs(&self) -> Result<Vec<Certificate>> {
39		Ok(
40			self
41				.get_developer_pems()?
42				.into_iter()
43				.filter_map(|cert| Certificate::try_from_x509(cert).ok())
44				.collect(),
45		)
46	}
47}
48
49#[derive(Debug, Serialize)]
50pub struct Certificate {
51	/// e.g. "Apple Development: johnsmith@hotmail.com (UIOH89JLHGF)"
52	pub common_name: String,
53	// e.g. "John Smith"
54	pub organization_name: Option<String>,
55}
56
57#[derive(Debug, Error)]
58pub enum X509FieldError {
59	#[error("Missing X509 field {name:?} ({id:?})")]
60	FieldMissing { name: &'static str, id: Nid },
61
62	#[error("Field contained invalid UTF-8: {0}")]
63	FieldNotValidUtf8(#[source] OpenSslError),
64}
65
66#[tracing::instrument(level = "trace", skip(subject_name, field_name, field_nid))]
67fn get_x509_field(
68	subject_name: &X509NameRef,
69	field_name: &'static str,
70	field_nid: Nid,
71) -> std::result::Result<String, X509FieldError> {
72	subject_name
73		.entries_by_nid(field_nid)
74		.next()
75		.ok_or(X509FieldError::FieldMissing {
76			name: field_name,
77			id: field_nid,
78		})?
79		.data()
80		.as_utf8()
81		.map_err(X509FieldError::FieldNotValidUtf8)
82		.map(|s| s.to_string())
83}
84
85#[derive(Debug, thiserror::Error)]
86pub enum FromX509Error {
87	#[error("Common Name missing in cert: {0}")]
88	CommonNameMissing(#[from] X509FieldError),
89}
90
91impl Certificate {
92	#[tracing::instrument(level = "trace", skip(cert))]
93	pub fn try_from_x509(cert: X509) -> std::result::Result<Self, FromX509Error> {
94		let subject = cert.subject_name();
95		let common_name = get_x509_field(subject, "Common Name", Nid::COMMONNAME)?;
96		let organization_name = get_x509_field(subject, "Organization", Nid::ORGANIZATIONNAME).ok();
97		Ok(Certificate {
98			common_name,
99			organization_name,
100		})
101	}
102}