1e377ff85SWill Schurmanimport { 2e377ff85SWill Schurman convertCertificatePEMToCertificate, 3e377ff85SWill Schurman convertKeyPairToPEM, 4e377ff85SWill Schurman convertCSRToCSRPEM, 5e377ff85SWill Schurman generateKeyPair, 6e377ff85SWill Schurman generateCSR, 7e377ff85SWill Schurman convertPrivateKeyPEMToPrivateKey, 8e377ff85SWill Schurman validateSelfSignedCertificate, 940f6c8a5SWill Schurman signBufferRSASHA256AndVerify, 10e377ff85SWill Schurman} from '@expo/code-signing-certificates'; 11e377ff85SWill Schurmanimport { ExpoConfig } from '@expo/config'; 12e377ff85SWill Schurmanimport { getExpoHomeDirectory } from '@expo/config/build/getUserState'; 13e377ff85SWill Schurmanimport JsonFile, { JSONObject } from '@expo/json-file'; 14e377ff85SWill Schurmanimport { promises as fs } from 'fs'; 15e377ff85SWill Schurmanimport { pki as PKI } from 'node-forge'; 16e377ff85SWill Schurmanimport path from 'path'; 17e377ff85SWill Schurmanimport { Dictionary, parseDictionary } from 'structured-headers'; 18e377ff85SWill Schurman 198a424bebSJames Ideimport { env } from './env'; 208a424bebSJames Ideimport { CommandError } from './errors'; 21e377ff85SWill Schurmanimport { getExpoGoIntermediateCertificateAsync } from '../api/getExpoGoIntermediateCertificate'; 22e377ff85SWill Schurmanimport { getProjectDevelopmentCertificateAsync } from '../api/getProjectDevelopmentCertificate'; 239fe3dc72SWill Schurmanimport { AppQuery } from '../api/graphql/queries/AppQuery'; 249fe3dc72SWill Schurmanimport { ensureLoggedInAsync } from '../api/user/actions'; 259fe3dc72SWill Schurmanimport { Actor } from '../api/user/user'; 269fe3dc72SWill Schurmanimport { AppByIdQuery, Permission } from '../graphql/generated'; 27e377ff85SWill Schurmanimport * as Log from '../log'; 281ae005aaSWill Schurmanimport { learnMore } from '../utils/link'; 29e377ff85SWill Schurman 30ff404fe3SEvan Baconconst debug = require('debug')('expo:codesigning') as typeof console.log; 31ff404fe3SEvan Bacon 32e377ff85SWill Schurmanexport type CodeSigningInfo = { 33c14835f6SWill Schurman keyId: string; 34e377ff85SWill Schurman privateKey: string; 35e377ff85SWill Schurman certificateForPrivateKey: string; 36e377ff85SWill Schurman /** 37e377ff85SWill Schurman * Chain of certificates to serve in the manifest multipart body "certificate_chain" part. 38e377ff85SWill Schurman * The leaf certificate must be the 0th element of the array, followed by any intermediate certificates 39e377ff85SWill Schurman * necessary to evaluate the chain of trust ending in the implicitly trusted root certificate embedded in 40e377ff85SWill Schurman * the client. 41e377ff85SWill Schurman * 42e377ff85SWill Schurman * An empty array indicates that there is no need to serve the certificate chain in the multipart response. 43e377ff85SWill Schurman */ 44e377ff85SWill Schurman certificateChainForResponse: string[]; 459fe3dc72SWill Schurman /** 469fe3dc72SWill Schurman * Scope key cached for the project when certificate is development Expo Go code signing. 479fe3dc72SWill Schurman * For project-specific code signing (keyId == the project's generated keyId) this is undefined. 489fe3dc72SWill Schurman */ 499fe3dc72SWill Schurman scopeKey: string | null; 50e377ff85SWill Schurman}; 51e377ff85SWill Schurman 52e377ff85SWill Schurmantype StoredDevelopmentExpoRootCodeSigningInfo = { 53e377ff85SWill Schurman easProjectId: string | null; 549fe3dc72SWill Schurman scopeKey: string | null; 55e377ff85SWill Schurman privateKey: string | null; 56e377ff85SWill Schurman certificateChain: string[] | null; 57e377ff85SWill Schurman}; 589fe3dc72SWill Schurmanconst DEVELOPMENT_CODE_SIGNING_SETTINGS_FILE_NAME = 'development-code-signing-settings-2.json'; 59e377ff85SWill Schurman 60e377ff85SWill Schurmanexport function getDevelopmentCodeSigningDirectory(): string { 61e377ff85SWill Schurman return path.join(getExpoHomeDirectory(), 'codesigning'); 62e377ff85SWill Schurman} 63e377ff85SWill Schurman 64e377ff85SWill Schurmanfunction getProjectDevelopmentCodeSigningInfoFile<T extends JSONObject>(defaults: T) { 65e377ff85SWill Schurman function getFile(easProjectId: string): JsonFile<T> { 66e377ff85SWill Schurman const filePath = path.join( 67e377ff85SWill Schurman getDevelopmentCodeSigningDirectory(), 68e377ff85SWill Schurman easProjectId, 69e377ff85SWill Schurman DEVELOPMENT_CODE_SIGNING_SETTINGS_FILE_NAME 70e377ff85SWill Schurman ); 71e377ff85SWill Schurman return new JsonFile<T>(filePath); 72e377ff85SWill Schurman } 73e377ff85SWill Schurman 74e377ff85SWill Schurman async function readAsync(easProjectId: string): Promise<T> { 75e377ff85SWill Schurman let projectSettings; 76e377ff85SWill Schurman try { 77e377ff85SWill Schurman projectSettings = await getFile(easProjectId).readAsync(); 78e377ff85SWill Schurman } catch { 79e377ff85SWill Schurman projectSettings = await getFile(easProjectId).writeAsync(defaults, { ensureDir: true }); 80e377ff85SWill Schurman } 81e377ff85SWill Schurman // Set defaults for any missing fields 82e377ff85SWill Schurman return { ...defaults, ...projectSettings }; 83e377ff85SWill Schurman } 84e377ff85SWill Schurman 85e377ff85SWill Schurman async function setAsync(easProjectId: string, json: Partial<T>): Promise<T> { 86e377ff85SWill Schurman try { 87e377ff85SWill Schurman return await getFile(easProjectId).mergeAsync(json, { 88e377ff85SWill Schurman cantReadFileDefault: defaults, 89e377ff85SWill Schurman }); 90e377ff85SWill Schurman } catch { 91e377ff85SWill Schurman return await getFile(easProjectId).writeAsync( 92e377ff85SWill Schurman { 93e377ff85SWill Schurman ...defaults, 94e377ff85SWill Schurman ...json, 95e377ff85SWill Schurman }, 96e377ff85SWill Schurman { ensureDir: true } 97e377ff85SWill Schurman ); 98e377ff85SWill Schurman } 99e377ff85SWill Schurman } 100e377ff85SWill Schurman 101e377ff85SWill Schurman return { 102e377ff85SWill Schurman getFile, 103e377ff85SWill Schurman readAsync, 104e377ff85SWill Schurman setAsync, 105e377ff85SWill Schurman }; 106e377ff85SWill Schurman} 107e377ff85SWill Schurman 108e377ff85SWill Schurmanexport const DevelopmentCodeSigningInfoFile = 109e377ff85SWill Schurman getProjectDevelopmentCodeSigningInfoFile<StoredDevelopmentExpoRootCodeSigningInfo>({ 110e377ff85SWill Schurman easProjectId: null, 1119fe3dc72SWill Schurman scopeKey: null, 112e377ff85SWill Schurman privateKey: null, 113e377ff85SWill Schurman certificateChain: null, 114e377ff85SWill Schurman }); 115e377ff85SWill Schurman 116e377ff85SWill Schurman/** 117e377ff85SWill Schurman * Get info necessary to generate a response `expo-signature` header given a project and incoming request `expo-expect-signature` header. 118e377ff85SWill Schurman * This only knows how to serve two code signing keyids: 119e377ff85SWill Schurman * - `expo-root` indicates that it should use a development certificate in the `expo-root` chain. See {@link getExpoRootDevelopmentCodeSigningInfoAsync} 120e377ff85SWill Schurman * - <developer's expo-updates keyid> indicates that it should sign with the configured certificate. See {@link getProjectCodeSigningCertificateAsync} 121e377ff85SWill Schurman */ 122e377ff85SWill Schurmanexport async function getCodeSigningInfoAsync( 123e377ff85SWill Schurman exp: ExpoConfig, 124e377ff85SWill Schurman expectSignatureHeader: string | null, 125e377ff85SWill Schurman privateKeyPath: string | undefined 126e377ff85SWill Schurman): Promise<CodeSigningInfo | null> { 127e377ff85SWill Schurman if (!expectSignatureHeader) { 128e377ff85SWill Schurman return null; 129e377ff85SWill Schurman } 130e377ff85SWill Schurman 131e377ff85SWill Schurman let parsedExpectSignature: Dictionary; 132e377ff85SWill Schurman try { 133e377ff85SWill Schurman parsedExpectSignature = parseDictionary(expectSignatureHeader); 134e377ff85SWill Schurman } catch { 135e377ff85SWill Schurman throw new CommandError('Invalid value for expo-expect-signature header'); 136e377ff85SWill Schurman } 137e377ff85SWill Schurman 138e377ff85SWill Schurman const expectedKeyIdOuter = parsedExpectSignature.get('keyid'); 139e377ff85SWill Schurman if (!expectedKeyIdOuter) { 140e377ff85SWill Schurman throw new CommandError('keyid not present in expo-expect-signature header'); 141e377ff85SWill Schurman } 142e377ff85SWill Schurman 143e377ff85SWill Schurman const expectedKeyId = expectedKeyIdOuter[0]; 144e377ff85SWill Schurman if (typeof expectedKeyId !== 'string') { 145e377ff85SWill Schurman throw new CommandError( 146e377ff85SWill Schurman `Invalid value for keyid in expo-expect-signature header: ${expectedKeyId}` 147e377ff85SWill Schurman ); 148e377ff85SWill Schurman } 149e377ff85SWill Schurman 150e377ff85SWill Schurman let expectedAlg: string | null = null; 151e377ff85SWill Schurman const expectedAlgOuter = parsedExpectSignature.get('alg'); 152e377ff85SWill Schurman if (expectedAlgOuter) { 153e377ff85SWill Schurman const expectedAlgTemp = expectedAlgOuter[0]; 154e377ff85SWill Schurman if (typeof expectedAlgTemp !== 'string') { 155e377ff85SWill Schurman throw new CommandError('Invalid value for alg in expo-expect-signature header'); 156e377ff85SWill Schurman } 157e377ff85SWill Schurman expectedAlg = expectedAlgTemp; 158e377ff85SWill Schurman } 159e377ff85SWill Schurman 160e377ff85SWill Schurman if (expectedKeyId === 'expo-root') { 161e377ff85SWill Schurman return await getExpoRootDevelopmentCodeSigningInfoAsync(exp); 162e377ff85SWill Schurman } else if (expectedKeyId === 'expo-go') { 163e377ff85SWill Schurman throw new CommandError( 164e377ff85SWill Schurman 'Invalid certificate requested: cannot sign with embedded keyid=expo-go key' 165e377ff85SWill Schurman ); 166e377ff85SWill Schurman } else { 167e377ff85SWill Schurman return await getProjectCodeSigningCertificateAsync( 168e377ff85SWill Schurman exp, 169e377ff85SWill Schurman privateKeyPath, 170e377ff85SWill Schurman expectedKeyId, 171e377ff85SWill Schurman expectedAlg 172e377ff85SWill Schurman ); 173e377ff85SWill Schurman } 174e377ff85SWill Schurman} 175e377ff85SWill Schurman 176e377ff85SWill Schurman/** 177e377ff85SWill Schurman * Get a development code signing certificate for the expo-root -> expo-go -> (development certificate) certificate chain. 178e377ff85SWill Schurman * This requires the user be logged in and online, otherwise try to use the cached development certificate. 179e377ff85SWill Schurman */ 180e377ff85SWill Schurmanasync function getExpoRootDevelopmentCodeSigningInfoAsync( 181e377ff85SWill Schurman exp: ExpoConfig 182e377ff85SWill Schurman): Promise<CodeSigningInfo | null> { 183e377ff85SWill Schurman const easProjectId = exp.extra?.eas?.projectId; 184e377ff85SWill Schurman // can't check for scope key validity since scope key is derived on the server from projectId and we may be offline. 185e377ff85SWill Schurman // we rely upon the client certificate check to validate the scope key 186e377ff85SWill Schurman if (!easProjectId) { 187ff404fe3SEvan Bacon debug( 188ff404fe3SEvan Bacon `WARN: Expo Application Services (EAS) is not configured for your project. Configuring EAS enables a more secure development experience amongst many other benefits. ${learnMore( 1891ae005aaSWill Schurman 'https://docs.expo.dev/eas/' 1901ae005aaSWill Schurman )}` 1911ae005aaSWill Schurman ); 192e377ff85SWill Schurman return null; 193e377ff85SWill Schurman } 194e377ff85SWill Schurman 195*05863844SEvan Bacon const developmentCodeSigningInfoFromFile = 196*05863844SEvan Bacon await DevelopmentCodeSigningInfoFile.readAsync(easProjectId); 197e377ff85SWill Schurman const validatedCodeSigningInfo = validateStoredDevelopmentExpoRootCertificateCodeSigningInfo( 198e377ff85SWill Schurman developmentCodeSigningInfoFromFile, 199e377ff85SWill Schurman easProjectId 200e377ff85SWill Schurman ); 2016b02c0ccSWill Schurman 2026b02c0ccSWill Schurman // 1. If online, ensure logged in, generate key pair and CSR, fetch and cache certificate chain for projectId 2036b02c0ccSWill Schurman // (overwriting existing dev cert in case projectId changed or it has expired) 204e32ccf9fSEvan Bacon if (!env.EXPO_OFFLINE) { 2056b02c0ccSWill Schurman try { 2066b02c0ccSWill Schurman return await fetchAndCacheNewDevelopmentCodeSigningInfoAsync(easProjectId); 207e32ccf9fSEvan Bacon } catch (e: any) { 2086b02c0ccSWill Schurman if (validatedCodeSigningInfo) { 2096b02c0ccSWill Schurman Log.warn( 2106b02c0ccSWill Schurman 'There was an error fetching the Expo development certificate, falling back to cached certificate' 2116b02c0ccSWill Schurman ); 2126b02c0ccSWill Schurman return validatedCodeSigningInfo; 2136b02c0ccSWill Schurman } else { 2149fe3dc72SWill Schurman // need to return null here and say a message 2156b02c0ccSWill Schurman throw e; 2166b02c0ccSWill Schurman } 2176b02c0ccSWill Schurman } 2186b02c0ccSWill Schurman } 2196b02c0ccSWill Schurman 2206b02c0ccSWill Schurman // 2. check for cached cert/private key matching projectId and scopeKey of project, if found and valid return private key and cert chain including expo-go cert 221e377ff85SWill Schurman if (validatedCodeSigningInfo) { 222e377ff85SWill Schurman return validatedCodeSigningInfo; 223e377ff85SWill Schurman } 224e377ff85SWill Schurman 225e377ff85SWill Schurman // 3. if offline, return null 226e377ff85SWill Schurman Log.warn('Offline and no cached development certificate found, unable to sign manifest'); 227e377ff85SWill Schurman return null; 228e377ff85SWill Schurman} 229e377ff85SWill Schurman 230e377ff85SWill Schurman/** 231e377ff85SWill Schurman * Get the certificate configured for expo-updates for this project. 232e377ff85SWill Schurman */ 233e377ff85SWill Schurmanasync function getProjectCodeSigningCertificateAsync( 234e377ff85SWill Schurman exp: ExpoConfig, 235e377ff85SWill Schurman privateKeyPath: string | undefined, 236e377ff85SWill Schurman expectedKeyId: string, 237e377ff85SWill Schurman expectedAlg: string | null 238e377ff85SWill Schurman): Promise<CodeSigningInfo | null> { 239e377ff85SWill Schurman const codeSigningCertificatePath = exp.updates?.codeSigningCertificate; 240e377ff85SWill Schurman if (!codeSigningCertificatePath) { 241e377ff85SWill Schurman return null; 242e377ff85SWill Schurman } 243e377ff85SWill Schurman 244e377ff85SWill Schurman if (!privateKeyPath) { 24572002074SWill Schurman throw new CommandError( 24672002074SWill Schurman 'Must specify --private-key-path argument to sign development manifest for requested code signing key' 24772002074SWill Schurman ); 248e377ff85SWill Schurman } 249e377ff85SWill Schurman 250e377ff85SWill Schurman const codeSigningMetadata = exp.updates?.codeSigningMetadata; 251e377ff85SWill Schurman if (!codeSigningMetadata) { 252e377ff85SWill Schurman throw new CommandError( 253e377ff85SWill Schurman 'Must specify "codeSigningMetadata" under the "updates" field of your app config file to use EAS code signing' 254e377ff85SWill Schurman ); 255e377ff85SWill Schurman } 256e377ff85SWill Schurman 257e377ff85SWill Schurman const { alg, keyid } = codeSigningMetadata; 258e377ff85SWill Schurman if (!alg || !keyid) { 259e377ff85SWill Schurman throw new CommandError( 260e377ff85SWill Schurman 'Must specify "keyid" and "alg" in the "codeSigningMetadata" field under the "updates" field of your app config file to use EAS code signing' 261e377ff85SWill Schurman ); 262e377ff85SWill Schurman } 263e377ff85SWill Schurman 264e377ff85SWill Schurman if (expectedKeyId !== keyid) { 265e377ff85SWill Schurman throw new CommandError(`keyid mismatch: client=${expectedKeyId}, project=${keyid}`); 266e377ff85SWill Schurman } 267e377ff85SWill Schurman 268e377ff85SWill Schurman if (expectedAlg && expectedAlg !== alg) { 269e377ff85SWill Schurman throw new CommandError(`"alg" field mismatch (client=${expectedAlg}, project=${alg})`); 270e377ff85SWill Schurman } 271e377ff85SWill Schurman 272e377ff85SWill Schurman const { privateKeyPEM, certificatePEM } = 273e377ff85SWill Schurman await getProjectPrivateKeyAndCertificateFromFilePathsAsync({ 274e377ff85SWill Schurman codeSigningCertificatePath, 275e377ff85SWill Schurman privateKeyPath, 276e377ff85SWill Schurman }); 277e377ff85SWill Schurman 278e377ff85SWill Schurman return { 279c14835f6SWill Schurman keyId: keyid, 280e377ff85SWill Schurman privateKey: privateKeyPEM, 281e377ff85SWill Schurman certificateForPrivateKey: certificatePEM, 282e377ff85SWill Schurman certificateChainForResponse: [], 2839fe3dc72SWill Schurman scopeKey: null, 284e377ff85SWill Schurman }; 285e377ff85SWill Schurman} 286e377ff85SWill Schurman 287e377ff85SWill Schurmanasync function readFileWithErrorAsync(path: string, errorMessage: string): Promise<string> { 288e377ff85SWill Schurman try { 289e377ff85SWill Schurman return await fs.readFile(path, 'utf8'); 290e377ff85SWill Schurman } catch { 291e377ff85SWill Schurman throw new CommandError(errorMessage); 292e377ff85SWill Schurman } 293e377ff85SWill Schurman} 294e377ff85SWill Schurman 295e377ff85SWill Schurmanasync function getProjectPrivateKeyAndCertificateFromFilePathsAsync({ 296e377ff85SWill Schurman codeSigningCertificatePath, 297e377ff85SWill Schurman privateKeyPath, 298e377ff85SWill Schurman}: { 299e377ff85SWill Schurman codeSigningCertificatePath: string; 300e377ff85SWill Schurman privateKeyPath: string; 301e377ff85SWill Schurman}): Promise<{ privateKeyPEM: string; certificatePEM: string }> { 302e377ff85SWill Schurman const [codeSigningCertificatePEM, privateKeyPEM] = await Promise.all([ 303e377ff85SWill Schurman readFileWithErrorAsync( 304e377ff85SWill Schurman codeSigningCertificatePath, 305e377ff85SWill Schurman `Code signing certificate cannot be read from path: ${codeSigningCertificatePath}` 306e377ff85SWill Schurman ), 307e377ff85SWill Schurman readFileWithErrorAsync( 308e377ff85SWill Schurman privateKeyPath, 309e377ff85SWill Schurman `Code signing private key cannot be read from path: ${privateKeyPath}` 310e377ff85SWill Schurman ), 311e377ff85SWill Schurman ]); 312e377ff85SWill Schurman 313e377ff85SWill Schurman const privateKey = convertPrivateKeyPEMToPrivateKey(privateKeyPEM); 314e377ff85SWill Schurman const certificate = convertCertificatePEMToCertificate(codeSigningCertificatePEM); 315e377ff85SWill Schurman validateSelfSignedCertificate(certificate, { 316e377ff85SWill Schurman publicKey: certificate.publicKey as PKI.rsa.PublicKey, 317e377ff85SWill Schurman privateKey, 318e377ff85SWill Schurman }); 319e377ff85SWill Schurman 320e377ff85SWill Schurman return { privateKeyPEM, certificatePEM: codeSigningCertificatePEM }; 321e377ff85SWill Schurman} 322e377ff85SWill Schurman 323e377ff85SWill Schurman/** 324e377ff85SWill Schurman * Validate that the cached code signing info is still valid for the current project and 325e377ff85SWill Schurman * that it hasn't expired. If invalid, return null. 326e377ff85SWill Schurman */ 327e377ff85SWill Schurmanfunction validateStoredDevelopmentExpoRootCertificateCodeSigningInfo( 328e377ff85SWill Schurman codeSigningInfo: StoredDevelopmentExpoRootCodeSigningInfo, 329e377ff85SWill Schurman easProjectId: string 330e377ff85SWill Schurman): CodeSigningInfo | null { 331e377ff85SWill Schurman if (codeSigningInfo.easProjectId !== easProjectId) { 332e377ff85SWill Schurman return null; 333e377ff85SWill Schurman } 334e377ff85SWill Schurman 3359fe3dc72SWill Schurman const { 3369fe3dc72SWill Schurman privateKey: privateKeyPEM, 3379fe3dc72SWill Schurman certificateChain: certificatePEMs, 3389fe3dc72SWill Schurman scopeKey, 3399fe3dc72SWill Schurman } = codeSigningInfo; 340e377ff85SWill Schurman if (!privateKeyPEM || !certificatePEMs) { 341e377ff85SWill Schurman return null; 342e377ff85SWill Schurman } 343e377ff85SWill Schurman 344e377ff85SWill Schurman const certificateChain = certificatePEMs.map((certificatePEM) => 345e377ff85SWill Schurman convertCertificatePEMToCertificate(certificatePEM) 346e377ff85SWill Schurman ); 347e377ff85SWill Schurman 348e377ff85SWill Schurman // TODO(wschurman): maybe move to @expo/code-signing-certificates 349e377ff85SWill Schurman const leafCertificate = certificateChain[0]; 350e377ff85SWill Schurman const now = new Date(); 351e377ff85SWill Schurman if (leafCertificate.validity.notBefore > now || leafCertificate.validity.notAfter < now) { 352e377ff85SWill Schurman return null; 353e377ff85SWill Schurman } 354e377ff85SWill Schurman 3559fe3dc72SWill Schurman // TODO(wschurman): maybe do more validation, like validation of projectID and scopeKey within eas certificate extension 356e377ff85SWill Schurman 357e377ff85SWill Schurman return { 358c14835f6SWill Schurman keyId: 'expo-go', 359e377ff85SWill Schurman certificateChainForResponse: certificatePEMs, 360e377ff85SWill Schurman certificateForPrivateKey: certificatePEMs[0], 361e377ff85SWill Schurman privateKey: privateKeyPEM, 3629fe3dc72SWill Schurman scopeKey, 363e377ff85SWill Schurman }; 364e377ff85SWill Schurman} 365e377ff85SWill Schurman 3669fe3dc72SWill Schurmanfunction actorCanGetProjectDevelopmentCertificate(actor: Actor, app: AppByIdQuery['app']['byId']) { 3679fe3dc72SWill Schurman const owningAccountId = app.ownerAccount.id; 3689fe3dc72SWill Schurman 3699fe3dc72SWill Schurman const owningAccountIsActorPrimaryAccount = 3709fe3dc72SWill Schurman actor.__typename === 'User' || actor.__typename === 'SSOUser' 3719fe3dc72SWill Schurman ? actor.primaryAccount.id === owningAccountId 3729fe3dc72SWill Schurman : false; 3739fe3dc72SWill Schurman const userHasPublishPermissionForOwningAccount = !!actor.accounts 3749fe3dc72SWill Schurman .find((account) => account.id === owningAccountId) 3759fe3dc72SWill Schurman ?.users?.find((userPermission) => userPermission.actor.id === actor.id) 3769fe3dc72SWill Schurman ?.permissions?.includes(Permission.Publish); 3779fe3dc72SWill Schurman return owningAccountIsActorPrimaryAccount || userHasPublishPermissionForOwningAccount; 3789fe3dc72SWill Schurman} 3799fe3dc72SWill Schurman 380e377ff85SWill Schurmanasync function fetchAndCacheNewDevelopmentCodeSigningInfoAsync( 381e377ff85SWill Schurman easProjectId: string 3829fe3dc72SWill Schurman): Promise<CodeSigningInfo | null> { 3839fe3dc72SWill Schurman const actor = await ensureLoggedInAsync(); 3849fe3dc72SWill Schurman const app = await AppQuery.byIdAsync(easProjectId); 3859fe3dc72SWill Schurman if (!actorCanGetProjectDevelopmentCertificate(actor, app)) { 3869fe3dc72SWill Schurman return null; 3879fe3dc72SWill Schurman } 3889fe3dc72SWill Schurman 389e377ff85SWill Schurman const keyPair = generateKeyPair(); 390e377ff85SWill Schurman const keyPairPEM = convertKeyPairToPEM(keyPair); 391e377ff85SWill Schurman const csr = generateCSR(keyPair, `Development Certificate for ${easProjectId}`); 392e377ff85SWill Schurman const csrPEM = convertCSRToCSRPEM(csr); 393e377ff85SWill Schurman const [developmentSigningCertificate, expoGoIntermediateCertificate] = await Promise.all([ 394e377ff85SWill Schurman getProjectDevelopmentCertificateAsync(easProjectId, csrPEM), 395e377ff85SWill Schurman getExpoGoIntermediateCertificateAsync(easProjectId), 396e377ff85SWill Schurman ]); 397e377ff85SWill Schurman 398e377ff85SWill Schurman await DevelopmentCodeSigningInfoFile.setAsync(easProjectId, { 399e377ff85SWill Schurman easProjectId, 4009fe3dc72SWill Schurman scopeKey: app.scopeKey, 401e377ff85SWill Schurman privateKey: keyPairPEM.privateKeyPEM, 402e377ff85SWill Schurman certificateChain: [developmentSigningCertificate, expoGoIntermediateCertificate], 403e377ff85SWill Schurman }); 404e377ff85SWill Schurman 405e377ff85SWill Schurman return { 406c14835f6SWill Schurman keyId: 'expo-go', 407e377ff85SWill Schurman certificateChainForResponse: [developmentSigningCertificate, expoGoIntermediateCertificate], 408e377ff85SWill Schurman certificateForPrivateKey: developmentSigningCertificate, 409e377ff85SWill Schurman privateKey: keyPairPEM.privateKeyPEM, 4109fe3dc72SWill Schurman scopeKey: app.scopeKey, 411e377ff85SWill Schurman }; 412e377ff85SWill Schurman} 413e377ff85SWill Schurman/** 414e377ff85SWill Schurman * Generate the `expo-signature` header for a manifest and code signing info. 415e377ff85SWill Schurman */ 416e377ff85SWill Schurmanexport function signManifestString( 417e377ff85SWill Schurman stringifiedManifest: string, 418e377ff85SWill Schurman codeSigningInfo: CodeSigningInfo 419e377ff85SWill Schurman): string { 420e377ff85SWill Schurman const privateKey = convertPrivateKeyPEMToPrivateKey(codeSigningInfo.privateKey); 421e377ff85SWill Schurman const certificate = convertCertificatePEMToCertificate(codeSigningInfo.certificateForPrivateKey); 42240f6c8a5SWill Schurman return signBufferRSASHA256AndVerify( 42340f6c8a5SWill Schurman privateKey, 42440f6c8a5SWill Schurman certificate, 42540f6c8a5SWill Schurman Buffer.from(stringifiedManifest, 'utf8') 42640f6c8a5SWill Schurman ); 427e377ff85SWill Schurman} 428