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