서론

    졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.
    이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.

    본론

    React Native의 경우, 여러가지 설정이 복잡하게 들어가있다. 우선 다음과 같은 flag들을 추가해야 한다.

    {
      "expo": {
        "ios": {
          "entitlements": {
            "com.apple.developer.devicecheck.appattest-environment": "development"
          }
        }
      }
    }

    해당 코드는 app.json에 추가되어야 하는 파트이다.
    iOS APP을 빌드할 때 AppAttest 환경을 어떤 것을 사용할 것이냐에 대한 필드이다.

    const getDeviceId = async() => {
        return await DeviceInfo.getUniqueId()
    }
    
    const getBundleId = () => {
        return DeviceInfo.getBundleId();
    }
    
    export const requestChallenge = async () => {
        const deviceId = await getDeviceId()
    
        try {
            const response = await axios.post<RequestChallengeResponse>(`${apiUrl}/api/integrity/challenge`,
                { deviceId },
                {}
            )
    
            const data = response.data;
            if (response.status !== 200) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
    
            const expDate = new Date()
            expDate.setMinutes(expDate.getMinutes() + data.expiresInMinutes)
    
            await AsyncStorage.setItem('integrityChallenge', data.challenge);
            await AsyncStorage.setItem('integrityChallengeExp', expDate.toISOString())
    
            return data.challenge;
        } catch (error: any) {
            console.error('Failed to get challenge: ' + error.message);
            throw error;
        }
    }
    
    export const verifyDeviceIntegrity = async () => {
        let challenge = await AsyncStorage.getItem('integrityChallenge');
        const expDate = await AsyncStorage.getItem('integrityChallengeExp');
        const deviceId = await getDeviceId()
        const platform = Platform.OS;
        const bundleId = getBundleId();
    
        if (!challenge || (expDate && new Date(expDate).getTime() < Date.now())) {
            challenge = await requestChallenge();
        }
    
        try {
            const googleCloudProject: number = parseInt(EXPO_PUBLIC_GOOGLE_CLOUD_PROJECT);
    
            let attestation = null
            let keyId = null
    
            try {
                if (platform == "ios") {
                    if(!Integrity.isSupported())
                        throw Error("Integrity is not supported on this device");
                    attestation = await Integrity.attestKey(challenge);
                    keyId = await SecureStore.getItemAsync(
                        SECURE_STORAGE_KEYS.INTEGRITY_KEY_IDENTIFIER,
                    )
                } else if (platform == "android") {
                    attestation = await Integrity.attestKey(challenge, googleCloudProject);
                } else throw Error("Platform not supported");
            } catch (error) { throw error }
    
            console.log({
                platform,
                attestation,
                bundleId,
                challenge,
                deviceId,
                keyId
            })
    
            const response = await axios.post<VerifyDeviceIntegrityResponse>(`${apiUrl}/api/integrity/verify`, {
                platform,
                attestation,
                bundleId,
                challenge,
                deviceId,
                keyId
            }, {
    
            });
    
            const data = response.data
            if (response.status !== 200 || !data.isValid) {
                throw new Error(`HTTP error! status: ${response.status} / ${data.message} / ${data.details ? JSON.stringify(data.details) : ''}`);
            }
    
            if (data.isValid) {
                await AsyncStorage.removeItem('integrityChallenge');
                await AsyncStorage.removeItem('integrityChallengeExp');
            }
            return data;
        } catch (error: any) {
            console.error(`Failed to verify integrity: ${error.message} / ${error.details ? JSON.stringify(error.details) : ''}`);
            throw error;
        }
    }

    다음은 Android, iOS 모두 검증하는 클라이언트 사이드 로직이다.

    라이브러리는 본인이 직접 수정한 라이브러리를 사용하길 권장한다. expo SDK 53에서도 사용가능함을 검증한 라이브러리이다.

    github / npmjs

    결론

    본인이 여러 생고생을 같이 했기 때문에 빨리 이 글을 찾아 광명을 누리길 바란다...

    Posted by dalbodeule