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