1*c4ef02aeSEvan Baconimport spawnAsync from '@expo/spawn-async';
2*c4ef02aeSEvan Baconimport forge from 'node-forge';
3*c4ef02aeSEvan Bacon
4*c4ef02aeSEvan Baconimport { SecurityBinPrerequisite } from '../../../start/doctor/SecurityBinPrerequisite';
5*c4ef02aeSEvan Baconimport { CommandError } from '../../../utils/errors';
6*c4ef02aeSEvan Bacon
7*c4ef02aeSEvan Baconexport type CertificateSigningInfo = {
8*c4ef02aeSEvan Bacon  /**
9*c4ef02aeSEvan Bacon   * @example 'AA00AABB0A'
10*c4ef02aeSEvan Bacon   */
11*c4ef02aeSEvan Bacon  signingCertificateId: string;
12*c4ef02aeSEvan Bacon  /**
13*c4ef02aeSEvan Bacon   * @example 'Apple Development: Evan Bacon (AA00AABB0A)'
14*c4ef02aeSEvan Bacon   */
15*c4ef02aeSEvan Bacon  codeSigningInfo?: string;
16*c4ef02aeSEvan Bacon  /**
17*c4ef02aeSEvan Bacon   * @example '650 Industries, Inc.'
18*c4ef02aeSEvan Bacon   */
19*c4ef02aeSEvan Bacon  appleTeamName?: string;
20*c4ef02aeSEvan Bacon  /**
21*c4ef02aeSEvan Bacon   * @example 'A1BCDEF234'
22*c4ef02aeSEvan Bacon   */
23*c4ef02aeSEvan Bacon  appleTeamId?: string;
24*c4ef02aeSEvan Bacon};
25*c4ef02aeSEvan Bacon
26*c4ef02aeSEvan Baconexport async function getSecurityPemAsync(id: string) {
27*c4ef02aeSEvan Bacon  const pem = (await spawnAsync('security', ['find-certificate', '-c', id, '-p'])).stdout?.trim?.();
28*c4ef02aeSEvan Bacon  if (!pem) {
29*c4ef02aeSEvan Bacon    throw new CommandError(`Failed to get PEM certificate for ID "${id}" using the 'security' bin`);
30*c4ef02aeSEvan Bacon  }
31*c4ef02aeSEvan Bacon  return pem;
32*c4ef02aeSEvan Bacon}
33*c4ef02aeSEvan Bacon
34*c4ef02aeSEvan Baconexport async function getCertificateForSigningIdAsync(id: string): Promise<forge.pki.Certificate> {
35*c4ef02aeSEvan Bacon  const pem = await getSecurityPemAsync(id);
36*c4ef02aeSEvan Bacon  return forge.pki.certificateFromPem(pem);
37*c4ef02aeSEvan Bacon}
38*c4ef02aeSEvan Bacon
39*c4ef02aeSEvan Bacon/**
40*c4ef02aeSEvan Bacon * Get the signing identities from the security bin. Return a list of parsed values with duplicates removed.
41*c4ef02aeSEvan Bacon * @returns A list like ['Apple Development: [email protected] (BB00AABB0A)', 'Apple Developer: Evan Bacon (AA00AABB0A)']
42*c4ef02aeSEvan Bacon */
43*c4ef02aeSEvan Baconexport async function findIdentitiesAsync(): Promise<string[]> {
44*c4ef02aeSEvan Bacon  await SecurityBinPrerequisite.instance.assertAsync();
45*c4ef02aeSEvan Bacon
46*c4ef02aeSEvan Bacon  const results = (
47*c4ef02aeSEvan Bacon    await spawnAsync('security', ['find-identity', '-p', 'codesigning', '-v'])
48*c4ef02aeSEvan Bacon  ).stdout.trim?.();
49*c4ef02aeSEvan Bacon  // Returns a string like:
50*c4ef02aeSEvan Bacon  // 1) 12222234253761286351826735HGKDHAJGF45283 "Apple Development: Evan Bacon (AA00AABB0A)" (CSSMERR_TP_CERT_REVOKED)
51*c4ef02aeSEvan Bacon  // 2) 12312234253761286351826735HGKDHAJGF45283 "Apple Development: [email protected] (BB00AABB0A)"
52*c4ef02aeSEvan Bacon  // 3) 12442234253761286351826735HGKDHAJGF45283 "iPhone Distribution: Evan Bacon (CC00AABB0B)" (CSSMERR_TP_CERT_REVOKED)
53*c4ef02aeSEvan Bacon  // 4) 15672234253761286351826735HGKDHAJGF45283 "Apple Development: Evan Bacon (AA00AABB0A)"
54*c4ef02aeSEvan Bacon  //  4 valid identities found
55*c4ef02aeSEvan Bacon
56*c4ef02aeSEvan Bacon  const parsed = results
57*c4ef02aeSEvan Bacon    .split('\n')
58*c4ef02aeSEvan Bacon    .map((line) => extractCodeSigningInfo(line))
59*c4ef02aeSEvan Bacon    .filter(Boolean) as string[];
60*c4ef02aeSEvan Bacon
61*c4ef02aeSEvan Bacon  // Remove duplicates
62*c4ef02aeSEvan Bacon  return [...new Set(parsed)];
63*c4ef02aeSEvan Bacon}
64*c4ef02aeSEvan Bacon
65*c4ef02aeSEvan Bacon/**
66*c4ef02aeSEvan Bacon * @param value '  2) 12312234253761286351826735HGKDHAJGF45283 "Apple Development: [email protected] (BB00AABB0A)"'
67*c4ef02aeSEvan Bacon * @returns 'Apple Development: Evan Bacon (PH75MDXG4H)'
68*c4ef02aeSEvan Bacon */
69*c4ef02aeSEvan Baconexport function extractCodeSigningInfo(value: string): string | null {
70*c4ef02aeSEvan Bacon  return value.match(/^\s*\d+\).+"(.+Develop(ment|er).+)"$/)?.[1] ?? null;
71*c4ef02aeSEvan Bacon}
72*c4ef02aeSEvan Bacon
73*c4ef02aeSEvan Baconexport async function resolveIdentitiesAsync(
74*c4ef02aeSEvan Bacon  identities: string[]
75*c4ef02aeSEvan Bacon): Promise<CertificateSigningInfo[]> {
76*c4ef02aeSEvan Bacon  const values = identities.map(extractSigningId).filter(Boolean) as string[];
77*c4ef02aeSEvan Bacon  return Promise.all(values.map(resolveCertificateSigningInfoAsync));
78*c4ef02aeSEvan Bacon}
79*c4ef02aeSEvan Bacon
80*c4ef02aeSEvan Bacon/**
81*c4ef02aeSEvan Bacon * @param signingCertificateId 'AA00AABB0A'
82*c4ef02aeSEvan Bacon */
83*c4ef02aeSEvan Baconexport async function resolveCertificateSigningInfoAsync(
84*c4ef02aeSEvan Bacon  signingCertificateId: string
85*c4ef02aeSEvan Bacon): Promise<CertificateSigningInfo> {
86*c4ef02aeSEvan Bacon  const certificate = await getCertificateForSigningIdAsync(signingCertificateId);
87*c4ef02aeSEvan Bacon  return {
88*c4ef02aeSEvan Bacon    signingCertificateId,
89*c4ef02aeSEvan Bacon    codeSigningInfo: certificate.subject.getField('CN')?.value,
90*c4ef02aeSEvan Bacon    appleTeamName: certificate.subject.getField('O')?.value,
91*c4ef02aeSEvan Bacon    appleTeamId: certificate.subject.getField('OU')?.value,
92*c4ef02aeSEvan Bacon  };
93*c4ef02aeSEvan Bacon}
94*c4ef02aeSEvan Bacon
95*c4ef02aeSEvan Bacon/**
96*c4ef02aeSEvan Bacon * @param codeSigningInfo 'Apple Development: Evan Bacon (AA00AABB0A)'
97*c4ef02aeSEvan Bacon * @returns 'AA00AABB0A'
98*c4ef02aeSEvan Bacon */
99*c4ef02aeSEvan Baconexport function extractSigningId(codeSigningInfo: string): string | null {
100*c4ef02aeSEvan Bacon  return codeSigningInfo.match(/.*\(([a-zA-Z0-9]+)\)/)?.[1] ?? null;
101*c4ef02aeSEvan Bacon}
102