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