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