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