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