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