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