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