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