1e377ff85SWill Schurmanimport { vol } from 'memfs'; 2e377ff85SWill Schurman 3*8a424bebSJames Ideimport { mockExpoRootChain, mockSelfSigned } from './fixtures/certificates'; 4fa5bc561SWill Schurmanimport { asMock } from '../../__tests__/asMock'; 56b02c0ccSWill Schurmanimport { getProjectDevelopmentCertificateAsync } from '../../api/getProjectDevelopmentCertificate'; 69fe3dc72SWill Schurmanimport { getUserAsync } from '../../api/user/user'; 7e377ff85SWill Schurmanimport { getCodeSigningInfoAsync, signManifestString } from '../codesigning'; 8e377ff85SWill Schurman 99fe3dc72SWill Schurmanjest.mock('../../api/user/user'); 109fe3dc72SWill Schurmanjest.mock('../../api/graphql/queries/AppQuery', () => ({ 119fe3dc72SWill Schurman AppQuery: { 129fe3dc72SWill Schurman byIdAsync: jest.fn(async () => ({ 139fe3dc72SWill Schurman id: 'blah', 149fe3dc72SWill Schurman scopeKey: 'scope-key', 159fe3dc72SWill Schurman ownerAccount: { 169fe3dc72SWill Schurman id: 'blah-account', 179fe3dc72SWill Schurman }, 189fe3dc72SWill Schurman })), 199fe3dc72SWill Schurman }, 209fe3dc72SWill Schurman})); 214c50faceSEvan Baconjest.mock('../../log'); 22e377ff85SWill Schurmanjest.mock('@expo/code-signing-certificates', () => ({ 23e377ff85SWill Schurman ...(jest.requireActual( 24e377ff85SWill Schurman '@expo/code-signing-certificates' 25e377ff85SWill Schurman ) as typeof import('@expo/code-signing-certificates')), 26e377ff85SWill Schurman generateKeyPair: jest.fn(() => 27e377ff85SWill Schurman ( 28e377ff85SWill Schurman jest.requireActual( 29e377ff85SWill Schurman '@expo/code-signing-certificates' 30e377ff85SWill Schurman ) as typeof import('@expo/code-signing-certificates') 31e377ff85SWill Schurman ).convertKeyPairPEMToKeyPair({ 32e377ff85SWill Schurman publicKeyPEM: mockExpoRootChain.publicKeyPEM, 33e377ff85SWill Schurman privateKeyPEM: mockExpoRootChain.privateKeyPEM, 34e377ff85SWill Schurman }) 35e377ff85SWill Schurman ), 36e377ff85SWill Schurman})); 37e377ff85SWill Schurmanjest.mock('../../api/getProjectDevelopmentCertificate', () => ({ 38e377ff85SWill Schurman getProjectDevelopmentCertificateAsync: jest.fn(() => mockExpoRootChain.developmentCertificate), 39e377ff85SWill Schurman})); 40e377ff85SWill Schurmanjest.mock('../../api/getExpoGoIntermediateCertificate', () => ({ 41e377ff85SWill Schurman getExpoGoIntermediateCertificateAsync: jest.fn( 42e377ff85SWill Schurman () => mockExpoRootChain.expoGoIntermediateCertificate 43e377ff85SWill Schurman ), 44e377ff85SWill Schurman})); 45e377ff85SWill Schurman 46e377ff85SWill SchurmanbeforeEach(() => { 47e377ff85SWill Schurman vol.reset(); 489fe3dc72SWill Schurman 499fe3dc72SWill Schurman asMock(getUserAsync).mockImplementation(async () => ({ 509fe3dc72SWill Schurman __typename: 'User', 519fe3dc72SWill Schurman id: 'userwat', 529fe3dc72SWill Schurman username: 'wat', 539fe3dc72SWill Schurman primaryAccount: { id: 'blah-account' }, 549fe3dc72SWill Schurman accounts: [], 559fe3dc72SWill Schurman })); 56e377ff85SWill Schurman}); 57e377ff85SWill Schurman 58e377ff85SWill Schurmandescribe(getCodeSigningInfoAsync, () => { 59e32ccf9fSEvan Bacon beforeEach(() => { 60e32ccf9fSEvan Bacon delete process.env.EXPO_OFFLINE; 61e32ccf9fSEvan Bacon }); 62e377ff85SWill Schurman it('returns null when no expo-expect-signature header is requested', async () => { 63c14835f6SWill Schurman await expect(getCodeSigningInfoAsync({} as any, null, undefined)).resolves.toBeNull(); 64e377ff85SWill Schurman }); 65e377ff85SWill Schurman 66e377ff85SWill Schurman it('throws when expo-expect-signature header has invalid format', async () => { 67c14835f6SWill Schurman await expect(getCodeSigningInfoAsync({} as any, 'hello', undefined)).rejects.toThrowError( 68e377ff85SWill Schurman 'keyid not present in expo-expect-signature header' 69e377ff85SWill Schurman ); 70c14835f6SWill Schurman await expect(getCodeSigningInfoAsync({} as any, 'keyid=1', undefined)).rejects.toThrowError( 71e377ff85SWill Schurman 'Invalid value for keyid in expo-expect-signature header: 1' 72e377ff85SWill Schurman ); 73e377ff85SWill Schurman await expect( 74c14835f6SWill Schurman getCodeSigningInfoAsync({} as any, 'keyid="hello", alg=1', undefined) 75e377ff85SWill Schurman ).rejects.toThrowError('Invalid value for alg in expo-expect-signature header'); 76e377ff85SWill Schurman }); 77e377ff85SWill Schurman 78e377ff85SWill Schurman describe('expo-root keyid requested', () => { 79e377ff85SWill Schurman describe('online', () => { 80e377ff85SWill Schurman beforeEach(() => { 81e32ccf9fSEvan Bacon delete process.env.EXPO_OFFLINE; 82e32ccf9fSEvan Bacon }); 83e32ccf9fSEvan Bacon afterAll(() => { 84e32ccf9fSEvan Bacon delete process.env.EXPO_OFFLINE; 85e377ff85SWill Schurman }); 86e377ff85SWill Schurman 87e377ff85SWill Schurman it('normal case gets a development certificate', async () => { 88e377ff85SWill Schurman const result = await getCodeSigningInfoAsync( 89e377ff85SWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 90e377ff85SWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 91e377ff85SWill Schurman undefined 92e377ff85SWill Schurman ); 93e377ff85SWill Schurman expect(result).toMatchSnapshot(); 94e377ff85SWill Schurman }); 95e377ff85SWill Schurman 96e377ff85SWill Schurman it('requires easProjectId to be configured', async () => { 97e377ff85SWill Schurman const result = await getCodeSigningInfoAsync( 98e377ff85SWill Schurman { extra: { eas: {} } } as any, 99e377ff85SWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 100e377ff85SWill Schurman undefined 101e377ff85SWill Schurman ); 102e377ff85SWill Schurman expect(result).toBeNull(); 103e377ff85SWill Schurman }); 104e377ff85SWill Schurman 1056b02c0ccSWill Schurman it('falls back to cached when there is a network error', async () => { 1066b02c0ccSWill Schurman const result = await getCodeSigningInfoAsync( 1076b02c0ccSWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 1086b02c0ccSWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 1096b02c0ccSWill Schurman undefined 1106b02c0ccSWill Schurman ); 1116b02c0ccSWill Schurman 1126b02c0ccSWill Schurman asMock(getProjectDevelopmentCertificateAsync).mockImplementationOnce( 1136b02c0ccSWill Schurman async (): Promise<string> => { 1146b02c0ccSWill Schurman throw Error('wat'); 1156b02c0ccSWill Schurman } 1166b02c0ccSWill Schurman ); 1176b02c0ccSWill Schurman 1186b02c0ccSWill Schurman const result2 = await getCodeSigningInfoAsync( 1196b02c0ccSWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 1206b02c0ccSWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 1216b02c0ccSWill Schurman undefined 1226b02c0ccSWill Schurman ); 1236b02c0ccSWill Schurman expect(result2).toEqual(result); 1246b02c0ccSWill Schurman }); 1256b02c0ccSWill Schurman 1266b02c0ccSWill Schurman it('throws when it tried to falls back to cached when there is a network error but no cached value exists', async () => { 1276b02c0ccSWill Schurman asMock(getProjectDevelopmentCertificateAsync).mockImplementationOnce( 1286b02c0ccSWill Schurman async (): Promise<string> => { 1296b02c0ccSWill Schurman throw Error('wat'); 1306b02c0ccSWill Schurman } 1316b02c0ccSWill Schurman ); 1326b02c0ccSWill Schurman 1336b02c0ccSWill Schurman await expect( 1346b02c0ccSWill Schurman getCodeSigningInfoAsync( 1356b02c0ccSWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 1366b02c0ccSWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 1376b02c0ccSWill Schurman undefined 1386b02c0ccSWill Schurman ) 1396b02c0ccSWill Schurman ).rejects.toThrowError('wat'); 1406b02c0ccSWill Schurman }); 1416b02c0ccSWill Schurman 142e377ff85SWill Schurman it('falls back to cached when offline', async () => { 143e377ff85SWill Schurman const result = await getCodeSigningInfoAsync( 144e377ff85SWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 145e377ff85SWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 146e377ff85SWill Schurman undefined 147e377ff85SWill Schurman ); 148e32ccf9fSEvan Bacon process.env.EXPO_OFFLINE = '1'; 149e377ff85SWill Schurman const result2 = await getCodeSigningInfoAsync( 150e377ff85SWill Schurman { extra: { eas: { projectId: 'testprojectid' } } } as any, 151e377ff85SWill Schurman 'keyid="expo-root", alg="rsa-v1_5-sha256"', 152e377ff85SWill Schurman undefined 153e377ff85SWill Schurman ); 154e377ff85SWill Schurman expect(result2).toEqual(result); 155e377ff85SWill Schurman }); 156e377ff85SWill Schurman }); 157e377ff85SWill Schurman }); 158e377ff85SWill Schurman 159e377ff85SWill Schurman describe('expo-go keyid requested', () => { 160e377ff85SWill Schurman it('throws', async () => { 161e377ff85SWill Schurman await expect( 162c14835f6SWill Schurman getCodeSigningInfoAsync({} as any, 'keyid="expo-go"', undefined) 163e377ff85SWill Schurman ).rejects.toThrowError( 164e377ff85SWill Schurman 'Invalid certificate requested: cannot sign with embedded keyid=expo-go key' 165e377ff85SWill Schurman ); 166e377ff85SWill Schurman }); 167e377ff85SWill Schurman }); 168e377ff85SWill Schurman 169e377ff85SWill Schurman describe('non expo-root certificate keyid requested', () => { 170e377ff85SWill Schurman it('normal case gets the configured certificate', async () => { 171e377ff85SWill Schurman vol.fromJSON({ 17272002074SWill Schurman 'certs/cert.pem': mockSelfSigned.certificate, 173e377ff85SWill Schurman 'keys/private-key.pem': mockSelfSigned.privateKey, 174e377ff85SWill Schurman }); 175e377ff85SWill Schurman 176e377ff85SWill Schurman const result = await getCodeSigningInfoAsync( 177e377ff85SWill Schurman { 178e377ff85SWill Schurman updates: { 17972002074SWill Schurman codeSigningCertificate: 'certs/cert.pem', 180e377ff85SWill Schurman codeSigningMetadata: { keyid: 'test', alg: 'rsa-v1_5-sha256' }, 181e377ff85SWill Schurman }, 182e377ff85SWill Schurman } as any, 183e377ff85SWill Schurman 'keyid="test", alg="rsa-v1_5-sha256"', 18472002074SWill Schurman 'keys/private-key.pem' 185e377ff85SWill Schurman ); 186e377ff85SWill Schurman expect(result).toMatchSnapshot(); 187e377ff85SWill Schurman }); 188e377ff85SWill Schurman 18972002074SWill Schurman it('throws when private key path is not supplied', async () => { 19072002074SWill Schurman await expect( 19172002074SWill Schurman getCodeSigningInfoAsync( 19272002074SWill Schurman { 19372002074SWill Schurman updates: { codeSigningCertificate: 'certs/cert.pem' }, 19472002074SWill Schurman } as any, 19572002074SWill Schurman 'keyid="test", alg="rsa-v1_5-sha256"', 19672002074SWill Schurman undefined 19772002074SWill Schurman ) 19872002074SWill Schurman ).rejects.toThrowError( 19972002074SWill Schurman 'Must specify --private-key-path argument to sign development manifest for requested code signing key' 20072002074SWill Schurman ); 20172002074SWill Schurman }); 20272002074SWill Schurman 203e377ff85SWill Schurman it('throws when it cannot generate the requested keyid due to no code signing configuration in app.json', async () => { 204e377ff85SWill Schurman await expect( 205e377ff85SWill Schurman getCodeSigningInfoAsync( 206e377ff85SWill Schurman { 20772002074SWill Schurman updates: { codeSigningCertificate: 'certs/cert.pem' }, 208e377ff85SWill Schurman } as any, 209e377ff85SWill Schurman 'keyid="test", alg="rsa-v1_5-sha256"', 21072002074SWill Schurman 'keys/private-key.pem' 211e377ff85SWill Schurman ) 212e377ff85SWill Schurman ).rejects.toThrowError( 213e377ff85SWill Schurman 'Must specify "codeSigningMetadata" under the "updates" field of your app config file to use EAS code signing' 214e377ff85SWill Schurman ); 215e377ff85SWill Schurman }); 216e377ff85SWill Schurman 217e377ff85SWill Schurman it('throws when it cannot generate the requested keyid due to configured keyid or alg mismatch', async () => { 218e377ff85SWill Schurman await expect( 219e377ff85SWill Schurman getCodeSigningInfoAsync( 220e377ff85SWill Schurman { 221e377ff85SWill Schurman updates: { 22272002074SWill Schurman codeSigningCertificate: 'certs/cert.pem', 223e377ff85SWill Schurman codeSigningMetadata: { keyid: 'test2', alg: 'rsa-v1_5-sha256' }, 224e377ff85SWill Schurman }, 225e377ff85SWill Schurman } as any, 226e377ff85SWill Schurman 'keyid="test", alg="rsa-v1_5-sha256"', 22772002074SWill Schurman 'keys/private-key.pem' 228e377ff85SWill Schurman ) 229e377ff85SWill Schurman ).rejects.toThrowError('keyid mismatch: client=test, project=test2'); 230e377ff85SWill Schurman 231e377ff85SWill Schurman await expect( 232e377ff85SWill Schurman getCodeSigningInfoAsync( 233e377ff85SWill Schurman { 234e377ff85SWill Schurman updates: { 23572002074SWill Schurman codeSigningCertificate: 'certs/cert.pem', 236e377ff85SWill Schurman codeSigningMetadata: { keyid: 'test', alg: 'fake' }, 237e377ff85SWill Schurman }, 238e377ff85SWill Schurman } as any, 239e377ff85SWill Schurman 'keyid="test", alg="fake2"', 24072002074SWill Schurman 'keys/private-key.pem' 241e377ff85SWill Schurman ) 242e377ff85SWill Schurman ).rejects.toThrowError('"alg" field mismatch (client=fake2, project=fake)'); 243e377ff85SWill Schurman }); 244e377ff85SWill Schurman 245e377ff85SWill Schurman it('throws when it cannot load configured code signing info', async () => { 246e377ff85SWill Schurman await expect( 247e377ff85SWill Schurman getCodeSigningInfoAsync( 248e377ff85SWill Schurman { 249e377ff85SWill Schurman updates: { 25072002074SWill Schurman codeSigningCertificate: 'certs/cert.pem', 251e377ff85SWill Schurman codeSigningMetadata: { keyid: 'test', alg: 'rsa-v1_5-sha256' }, 252e377ff85SWill Schurman }, 253e377ff85SWill Schurman } as any, 254e377ff85SWill Schurman 'keyid="test", alg="rsa-v1_5-sha256"', 25572002074SWill Schurman 'keys/private-key.pem' 256e377ff85SWill Schurman ) 25772002074SWill Schurman ).rejects.toThrowError('Code signing certificate cannot be read from path: certs/cert.pem'); 258e377ff85SWill Schurman }); 259e377ff85SWill Schurman }); 260e377ff85SWill Schurman}); 261e377ff85SWill Schurman 262e377ff85SWill Schurmandescribe(signManifestString, () => { 263e32ccf9fSEvan Bacon beforeEach(() => { 264e32ccf9fSEvan Bacon delete process.env.EXPO_OFFLINE; 265e32ccf9fSEvan Bacon }); 266e377ff85SWill Schurman it('generates signature', () => { 267e377ff85SWill Schurman expect( 268e377ff85SWill Schurman signManifestString('hello', { 269c14835f6SWill Schurman keyId: 'test', 270e377ff85SWill Schurman certificateChainForResponse: [], 271e377ff85SWill Schurman certificateForPrivateKey: mockSelfSigned.certificate, 272e377ff85SWill Schurman privateKey: mockSelfSigned.privateKey, 2739fe3dc72SWill Schurman scopeKey: null, 274e377ff85SWill Schurman }) 275e377ff85SWill Schurman ).toMatchSnapshot(); 276e377ff85SWill Schurman }); 277e377ff85SWill Schurman it('validates generated signature against certificate', () => { 278e377ff85SWill Schurman expect(() => 279e377ff85SWill Schurman signManifestString('hello', { 280c14835f6SWill Schurman keyId: 'test', 281e377ff85SWill Schurman certificateChainForResponse: [], 282e377ff85SWill Schurman certificateForPrivateKey: '', 283e377ff85SWill Schurman privateKey: mockSelfSigned.privateKey, 2849fe3dc72SWill Schurman scopeKey: null, 285e377ff85SWill Schurman }) 286e377ff85SWill Schurman ).toThrowError('Invalid PEM formatted message.'); 287e377ff85SWill Schurman }); 288e377ff85SWill Schurman}); 289