프로그래밍/Android

[안드로이드] Google SafetyNet Attestation으로 기기 무결성을 확보하기 (클라이언트/서버 Python)

Lou Park 2021. 6. 3. 11:39

SafetyNet Attestation으로 기기 무결성을 확보하기
SafetyNet Attestation 구현을 위해서 클라이언트(앱)와 서버에서 필요한 절차들을 나누어 설명할 예정이다.

전체 절차

  1. 서버에 Nonce를 요청한다.
  2. 해당 요청에 대한 Nonce를 생성하여 다시 앱으로 돌려준다.
  3. 앱에서 해당 Nonce를 싣고 SafetyNet API를 호출하여 Google Services에 Attestation을 요청한다.
  4. Attestation 결과를 JWT로 받는다.
  5. 받은 결과를 서버로 전송한다.
  6. 서버는 JWT를 통해 Attestation 검증 후 결과를 앱으로 돌려준다.

클라이언트

API 키 얻기

 

SafetyNet Attestation API 호출을 위해 API 키를 먼저 얻어야한다. GCP에서 API 키를 얻을 수 있다.
어떻게 얻을 수 있는지는 공식 문서에 잘 나와있다.

  1. Google API 콘솔의 라이브러리(https://console.cloud.google.com/apis/library) 페이지로 이동합니다.
  2. Android Device Verification API를 검색하여 선택합니다. Android Device Verification API 대시보드 화면이 나타납니다.
  3. 아직 이 API를 사용 설정하지 않았다면 사용 설정을 클릭합니다.
  4. 사용자 인증 정보 만들기 버튼이 나타나면 클릭하여 API 키를 생성합니다. 이 버튼이 나타나지 않으면 모든 API 사용자 인증 정보 드롭다운 목록을 클릭한 다음 Android Device Verification API를 사용 설정한 프로젝트와 연결된 API 키를 선택합니다.
  5. 왼쪽 사이드바에서 사용자 인증 정보를 클릭합니다. 표시되는 API 키를 복사합니다.
  6. SafetyNetClient 클래스의 attest() 메서드를 호출할 때 이 API 키를 사용합니다.

 

SafetyNet Dependency 추가

app/build.gradle에 다음과 같이 dependency를 추가한다.

implementation "com.google.android.gms:play-services-safetynet:17.0.0"

SafetyNet Attest API 호출하기

 

앱이 조작되었을 가능성도 있으므로 서버에서 nonce를 만들어주는 것이 좋다. 서버에서 적절한 nonce를 받았다고 생각하고 구현하면 된다.
API_KEY는 앞서 GCP에서 얻은 API 키를 사용하면된다. sendJwsToServer()의 경우 그냥 서버로 JWS Result string을 보내는 것이다.

val client = SafetyNet.getClient(context)
client.attest(Base64.decode(nonce, Base64.DEFAULT),  API_KEY)
    .addOnSuccessListener { response ->
         // TODO: 예시 - sendJwsToServer(response.jwsResult)
    }
    .addOnFailureListener {
        // TODO...실패시 
    }

서버로 부터 응답 결과 받기

 

JWT를 보냈으니, 그 응답으로 서버에서는 검증 결과를 내려줄 것이다. 구현하기 나름이지만 예를 들면 True/False?
그 결과에 따라 뭐 "검증된 기기가 아니므로 앱을 사용할 수 없습니다" 같은 메세지를 띄워주면 되겠다.

서버

서버에서는 앱과 통신하기 위해 2가지 Rest API가 준비되어야 한다.
첫째, Nonce를 생성하는 API.
둘째, 클라이언트가 보낸 JWT를 검증하여 결과를 반환하는 API.

*예시로는 python 코드를 사용할 것이다.

API 키 얻기

 

서버에서도 Attestation을 위해 API 키가 필요한데, 클라이언트 스텝에서 얻은 키와 동일한 키를 써주면된다.

Nonce 생성하기

 

Nonce는 Request와 Response가 매치되는지 확인할때 사용되며, Replay attack을 방지하기 위해서 해당 request 당시에만 가능한 고유한 키로 만들어야한다.
nonce에 가능한 다양한 정보가 들어갈 수록 좋다. (유니크해지기 위해서)

nonce_string = self.user_id + get_timestamp() # 예로 user_id와 timestamp를 섞었다.
nonce = base64.b64encode(nonce_string.encode('utf-8')).decode('utf-8')

JWT 검증하기

 

클라이언트가 보낸 JWT가 jwt_token에 저장되어있다고 가정할때 아래 방법으로 JWT의 payload 내용을 볼 수 있다.

from google.auth import jwt
decoded = jwt.decode(jwt_token, verify=False)

풀어낸 결과를 예로 들자면 이렇다.

{'apkCertificateDigestSha256': ['oQw9gShjaOobKrv3fKmxKSuObamwckA1ESWmxI9Xc='],
 'apkDigestSha256': 'XPiCae5PD6fv6TrRakEAUT8A88cBrxbGqVFWemwct4gY=',
 'apkPackageName': 'com.example.king',
 'basicIntegrity': True,
 'ctsProfileMatch': True,
 'evaluationType': 'BASIC,HARDWARE_BACKED',
 'nonce': 'A9dREMcBJOakErTWpRME16SShPRFk7Cg==',
 'timestampMs': 1620724831423}

이 내용을 바탕으로 검증을 하면되는데, 각 요소들의 설명과 확인할 사항은 아래와 같다.
nonce: 앱으로 내려준 Nonce와 일치하는지 확인.
apkPackageName: 앱의 패키지명과 일치하는지 확인.
apkCertificateDigestSha256: 구글 플레이 서명 인증서 SHA-256 값을 Base64로 인코딩한 값과 일치하는지 확인하면된다.
터미널을 이용해 간단히 base64 인코딩이 가능하다.

echo SHA256입력 | xxd -r -p | openssl base64

basicIntegrity: 관대한 기기 무결성 결과. true라면 앱을 실행하는 기기가 조작되지 않았을 가능성이있으나 100% 믿으면 안됨.
ctsProfileMatch: 엄격한 기기 무결성 결과. true라면 앱을 실행하는 기기의 프로필이 Android 호환성 테스트를 통과한 기기의 프로필과 일치.
timestampMs: 요청후 응답을 받기까지 얼마나 걸렸는지 체크할 때 사용할 수 있음. 몇 시간, 혹은 몇 일이 걸렸거나 과거로 돌아갔을 경우 의심할 수 있음.

 

Attestation API로 JWT Validate하기

 

python에서는 서버측 attestation을 위한 라이브러리가 있다. 이를 이용하면 아래와 같이 간편하게 attest를 할 수 있다.

from safetynet_attestation import Attestation
def attest():
    attestation = Attestation(jwt_token)
    result = attestation.verify_online(api_key)
    print(result) # True

또 다른 방법으로는 Google의 REST API를 직접 호출하는 방법이있다. 다른 언어 환경에서 개발을 하시는 분들이라면 훨씬 더 도움이 될 것 같다.
RequestMethod=POST로 API 키와 클라이언트로 부터 받은 JWT를 보내주면 올바른 서명인지 확인해서 준다.

def attest(jwt_token):
    payload = dict()
    payload['signedAttestation'] = jwt_token
    attest_url = f'https://www.googleapis.com/androidcheck/v1/attestations/verify?key={api_key}'
    res = requests.post(attest_url, data=payload)
    return res.json() # {'isValidSignature': True}

주의 사항

Attestation API 호출은 하루에 10,000회로 제한되어있다.
더 많은 호출을 원한다면 (https://support.google.com/googleplay/android-developer/contact/safetynetqr)에 양식을 제출하면 된다.

더 알아보기

조금 더 알아보고 싶다면 아래 두 사이트를 추천한다.
https://www.netguru.com/codestories/stay-safe-with-safetynet-attestation-api-in-android
https://android-developers.googleblog.com/2017/11/10-things-you-might-be-doing-wrong-when.html