1import { vol } from 'memfs'; 2 3import { getProjectDevelopmentCertificateAsync } from '../../api/getProjectDevelopmentCertificate'; 4import { APISettings } from '../../api/settings'; 5import { getCodeSigningInfoAsync, signManifestString } from '../codesigning'; 6import { mockExpoRootChain, mockSelfSigned } from './fixtures/certificates'; 7 8const asMock = <T extends (...args: any[]) => any>(fn: T): jest.MockedFunction<T> => 9 fn as jest.MockedFunction<T>; 10 11jest.mock('../../log'); 12jest.mock('@expo/code-signing-certificates', () => ({ 13 ...(jest.requireActual( 14 '@expo/code-signing-certificates' 15 ) as typeof import('@expo/code-signing-certificates')), 16 generateKeyPair: jest.fn(() => 17 ( 18 jest.requireActual( 19 '@expo/code-signing-certificates' 20 ) as typeof import('@expo/code-signing-certificates') 21 ).convertKeyPairPEMToKeyPair({ 22 publicKeyPEM: mockExpoRootChain.publicKeyPEM, 23 privateKeyPEM: mockExpoRootChain.privateKeyPEM, 24 }) 25 ), 26})); 27jest.mock('../../api/getProjectDevelopmentCertificate', () => ({ 28 getProjectDevelopmentCertificateAsync: jest.fn(() => mockExpoRootChain.developmentCertificate), 29})); 30jest.mock('../../api/getExpoGoIntermediateCertificate', () => ({ 31 getExpoGoIntermediateCertificateAsync: jest.fn( 32 () => mockExpoRootChain.expoGoIntermediateCertificate 33 ), 34})); 35 36// Mock the CLI global to prevent side-effects in other tests. 37jest.mock('../../api/settings', () => ({ 38 APISettings: { 39 isOffline: true, 40 }, 41})); 42 43beforeEach(() => { 44 vol.reset(); 45}); 46 47describe(getCodeSigningInfoAsync, () => { 48 it('returns null when no expo-expect-signature header is requested', async () => { 49 await expect(getCodeSigningInfoAsync({} as any, null, null)).resolves.toBeNull(); 50 }); 51 52 it('throws when expo-expect-signature header has invalid format', async () => { 53 await expect(getCodeSigningInfoAsync({} as any, 'hello', null)).rejects.toThrowError( 54 'keyid not present in expo-expect-signature header' 55 ); 56 await expect(getCodeSigningInfoAsync({} as any, 'keyid=1', null)).rejects.toThrowError( 57 'Invalid value for keyid in expo-expect-signature header: 1' 58 ); 59 await expect( 60 getCodeSigningInfoAsync({} as any, 'keyid="hello", alg=1', null) 61 ).rejects.toThrowError('Invalid value for alg in expo-expect-signature header'); 62 }); 63 64 describe('expo-root keyid requested', () => { 65 describe('online', () => { 66 beforeEach(() => { 67 APISettings.isOffline = false; 68 }); 69 70 it('normal case gets a development certificate', async () => { 71 const result = await getCodeSigningInfoAsync( 72 { extra: { eas: { projectId: 'testprojectid' } } } as any, 73 'keyid="expo-root", alg="rsa-v1_5-sha256"', 74 undefined 75 ); 76 expect(result).toMatchSnapshot(); 77 }); 78 79 it('requires easProjectId to be configured', async () => { 80 const result = await getCodeSigningInfoAsync( 81 { extra: { eas: {} } } as any, 82 'keyid="expo-root", alg="rsa-v1_5-sha256"', 83 undefined 84 ); 85 expect(result).toBeNull(); 86 }); 87 88 it('falls back to cached when there is a network error', async () => { 89 const result = await getCodeSigningInfoAsync( 90 { extra: { eas: { projectId: 'testprojectid' } } } as any, 91 'keyid="expo-root", alg="rsa-v1_5-sha256"', 92 undefined 93 ); 94 95 asMock(getProjectDevelopmentCertificateAsync).mockImplementationOnce( 96 async (): Promise<string> => { 97 throw Error('wat'); 98 } 99 ); 100 101 const result2 = await getCodeSigningInfoAsync( 102 { extra: { eas: { projectId: 'testprojectid' } } } as any, 103 'keyid="expo-root", alg="rsa-v1_5-sha256"', 104 undefined 105 ); 106 expect(result2).toEqual(result); 107 }); 108 109 it('throws when it tried to falls back to cached when there is a network error but no cached value exists', async () => { 110 asMock(getProjectDevelopmentCertificateAsync).mockImplementationOnce( 111 async (): Promise<string> => { 112 throw Error('wat'); 113 } 114 ); 115 116 await expect( 117 getCodeSigningInfoAsync( 118 { extra: { eas: { projectId: 'testprojectid' } } } as any, 119 'keyid="expo-root", alg="rsa-v1_5-sha256"', 120 undefined 121 ) 122 ).rejects.toThrowError('wat'); 123 }); 124 125 it('falls back to cached when offline', async () => { 126 const result = await getCodeSigningInfoAsync( 127 { extra: { eas: { projectId: 'testprojectid' } } } as any, 128 'keyid="expo-root", alg="rsa-v1_5-sha256"', 129 undefined 130 ); 131 APISettings.isOffline = true; 132 const result2 = await getCodeSigningInfoAsync( 133 { extra: { eas: { projectId: 'testprojectid' } } } as any, 134 'keyid="expo-root", alg="rsa-v1_5-sha256"', 135 undefined 136 ); 137 expect(result2).toEqual(result); 138 APISettings.isOffline = false; 139 }); 140 }); 141 }); 142 143 describe('expo-go keyid requested', () => { 144 it('throws', async () => { 145 await expect( 146 getCodeSigningInfoAsync({} as any, 'keyid="expo-go"', null) 147 ).rejects.toThrowError( 148 'Invalid certificate requested: cannot sign with embedded keyid=expo-go key' 149 ); 150 }); 151 }); 152 153 describe('non expo-root certificate keyid requested', () => { 154 it('normal case gets the configured certificate', async () => { 155 vol.fromJSON({ 156 'certs/cert.pem': mockSelfSigned.certificate, 157 'keys/private-key.pem': mockSelfSigned.privateKey, 158 }); 159 160 const result = await getCodeSigningInfoAsync( 161 { 162 updates: { 163 codeSigningCertificate: 'certs/cert.pem', 164 codeSigningMetadata: { keyid: 'test', alg: 'rsa-v1_5-sha256' }, 165 }, 166 } as any, 167 'keyid="test", alg="rsa-v1_5-sha256"', 168 'keys/private-key.pem' 169 ); 170 expect(result).toMatchSnapshot(); 171 }); 172 173 it('throws when private key path is not supplied', async () => { 174 await expect( 175 getCodeSigningInfoAsync( 176 { 177 updates: { codeSigningCertificate: 'certs/cert.pem' }, 178 } as any, 179 'keyid="test", alg="rsa-v1_5-sha256"', 180 undefined 181 ) 182 ).rejects.toThrowError( 183 'Must specify --private-key-path argument to sign development manifest for requested code signing key' 184 ); 185 }); 186 187 it('throws when it cannot generate the requested keyid due to no code signing configuration in app.json', async () => { 188 await expect( 189 getCodeSigningInfoAsync( 190 { 191 updates: { codeSigningCertificate: 'certs/cert.pem' }, 192 } as any, 193 'keyid="test", alg="rsa-v1_5-sha256"', 194 'keys/private-key.pem' 195 ) 196 ).rejects.toThrowError( 197 'Must specify "codeSigningMetadata" under the "updates" field of your app config file to use EAS code signing' 198 ); 199 }); 200 201 it('throws when it cannot generate the requested keyid due to configured keyid or alg mismatch', async () => { 202 await expect( 203 getCodeSigningInfoAsync( 204 { 205 updates: { 206 codeSigningCertificate: 'certs/cert.pem', 207 codeSigningMetadata: { keyid: 'test2', alg: 'rsa-v1_5-sha256' }, 208 }, 209 } as any, 210 'keyid="test", alg="rsa-v1_5-sha256"', 211 'keys/private-key.pem' 212 ) 213 ).rejects.toThrowError('keyid mismatch: client=test, project=test2'); 214 215 await expect( 216 getCodeSigningInfoAsync( 217 { 218 updates: { 219 codeSigningCertificate: 'certs/cert.pem', 220 codeSigningMetadata: { keyid: 'test', alg: 'fake' }, 221 }, 222 } as any, 223 'keyid="test", alg="fake2"', 224 'keys/private-key.pem' 225 ) 226 ).rejects.toThrowError('"alg" field mismatch (client=fake2, project=fake)'); 227 }); 228 229 it('throws when it cannot load configured code signing info', async () => { 230 await expect( 231 getCodeSigningInfoAsync( 232 { 233 updates: { 234 codeSigningCertificate: 'certs/cert.pem', 235 codeSigningMetadata: { keyid: 'test', alg: 'rsa-v1_5-sha256' }, 236 }, 237 } as any, 238 'keyid="test", alg="rsa-v1_5-sha256"', 239 'keys/private-key.pem' 240 ) 241 ).rejects.toThrowError('Code signing certificate cannot be read from path: certs/cert.pem'); 242 }); 243 }); 244}); 245 246describe(signManifestString, () => { 247 it('generates signature', () => { 248 expect( 249 signManifestString('hello', { 250 certificateChainForResponse: [], 251 certificateForPrivateKey: mockSelfSigned.certificate, 252 privateKey: mockSelfSigned.privateKey, 253 }) 254 ).toMatchSnapshot(); 255 }); 256 it('validates generated signature against certificate', () => { 257 expect(() => 258 signManifestString('hello', { 259 certificateChainForResponse: [], 260 certificateForPrivateKey: '', 261 privateKey: mockSelfSigned.privateKey, 262 }) 263 ).toThrowError('Invalid PEM formatted message.'); 264 }); 265}); 266