이 글의 원문은 여기서 확인할 수 있으며 블로그 리뷰에는 이승민(GDE)님이 참여해주셨습니다.
최근 Play Integrity API 출시와 함께, 더 많은 개발자가 의심스럽고 위험한 상호작용으로부터 게임과 앱을 보호하기 위한 조치를 취하고 있습니다.
앱의 무결성, 기기의 무결성, 라이선스 정보에 대한 유용한 신호 외에도, Play Integrity API는 'nonce'라는 간단하지만 매우 유용한 기능을 갖추고 있는데, 이 기능을 올바르게 사용하면 Play Integrity API가 제공하는 기존의 보호 기능을 더욱 강화할 수 있으며 PITM(person-in-the-middle) 변조 공격, 리플레이 공격 등 특정 유형의 공격도 완화할 수 있습니다.
이번 게시물에서는 nonce가 무엇이고 어떻게 작동하며, 어떻게 사용하면 앱을 추가로 보호할 수 있는지 자세히 살펴보겠습니다.
Nonce란 무엇일까요?
암호화 및 보안 엔지니어링에서 nonce(number once)는 보안 통신에서 한 번만 사용되는 숫자입니다. 인증, 암호화, 해싱 등 nonce를 위한 많은 애플리케이션이 있습니다.
Play Integrity API에서 nonce는 API 무결성 검사를 호출하기 전에 설정한 불투명한 base-64 인코딩 바이너리 blob으로, API의 서명된 응답에 그대로 반환됩니다. Nonce를 만들고 그 유효성을 확인하는 방법에 따라, nonce를 활용하여 Play Integrity API가 제공하는 기존 보호 기능을 더욱 강화할 뿐 아니라, PITM(person-in-the-middle) 변조 공격, 리플레이 공격 등 특정 유형의 공격을 완화할 수도 있습니다.
서명된 응답에서 nonce를 있는 그대로 반환하는 것과는 별개로, Play Integrity API는 실제 nonce 데이터 처리를 수행하지 않으므로 nonce가 유효한 base-64 값이면 임의의 값을 설정할 수 있습니다. 즉, 응답에 디지털 서명을 하기 위해 nonce가 Google 서버로 전송되므로, nonce를 사용자 이름이나 전화 번호, 이메일 주소 같은 어떤 유형의 PII(개인 식별 정보)로도 설정하지 않는 것이 매우 중요합니다.
Nonce 설정
Play Integrity API를 사용하도록 앱을 설정한 후, API의 Kotlin, Java, Unity 및 Native 버전에서 사용할 수 있는 setNonce() 메서드 또는 적절한 변형으로 nonce를 설정합니다.
Kotlin:
val nonce: String = ...
// Create an instance of a manager.
val integrityManager =
IntegrityManagerFactory.create(applicationContext)
// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
integrityManager.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce) // Set the nonce
.build())
Java:
String nonce = ...
// Create an instance of a manager.
IntegrityManager integrityManager =
IntegrityManagerFactory.create(getApplicationContext());
// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
integrityManager
.requestIntegrityToken(
IntegrityTokenRequest.builder()
.setNonce(nonce) // Set the nonce
.build());
Unity:
string nonce = ...
// Create an instance of a manager.
var integrityManager = new IntegrityManager();
// Request the integrity token by providing a nonce.
var tokenRequest = new IntegrityTokenRequest(nonce);
var requestIntegrityTokenOperation =
integrityManager.RequestIntegrityToken(tokenRequest);
Native:
/// Create an IntegrityTokenRequest object.
const char* nonce = ...
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce); // Set the nonce
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
IntegrityManager_requestIntegrityToken(request, &response);
Nonce 확인
Play Integrity API의 응답은 페이로드가 일반 텍스트 JSON이고 다음 형식의 JWT(JSON Web Token) 형태로 반환됩니다.
{
requestDetails: { ... }
appIntegrity: { ... }
deviceIntegrity: { ... }
accountDetails: { ... }
}
Nonce는 다음과 같은 형식의 requestDetails 구조체 내부에서 찾을 수 있습니다.
requestDetails: {
requestPackageName: "...",
nonce: "...",
timestampMillis: ...
}
nonce 필드의 값은 이전에 API에 전달한 값과 정확히 일치해야 합니다. 게다가 nonce는 Play Integrity API의 암호화 방식으로 서명된 응답 내부에 있으므로 응답을 받은 후에는 값을 변경할 수 없습니다. Nonce를 사용하여 앱을 추가로 보호할 수 있는 방법은 바로 이러한 속성을 활용하는 것입니다.
중요한 작업 보호
악의적인 사용자가 플레이어 점수를 게임 서버에 보고하는 온라인 게임과 상호 작용하는 시나리오를 생각해 보겠습니다. 이 경우, 기기는 손상되지 않지만 사용자가 프록시 서버나 VPN의 도움을 받아 게임과 서버 간의 네트워크 데이터 흐름을 확인하고 수정할 수 있으므로, 악의적인 사용자라면 실제 점수는 훨씬 낮음에도 더 높은 점수를 얻은 것처럼 보고할 수 있습니다.
이 경우에는 기기가 손상되지 않고 앱이 합법적이라 Play Integrity API에서 수행되는 모든 검사를 통과할 것이므로, 단순히 Play Integrity API를 호출하는 것만으로는 앱을 보호하기에 충분하지 않습니다.
하지만 Play Integrity API의 nonce를 활용하여 nonce 내부의 작업 값을 인코딩함으로써 게임 점수 보고라는 바로 이 중요한 작업을 보호할 수 있습니다. 구현은 다음과 같습니다.
사용자가 중요 작업을 시작합니다.
앱이 보호하려는 메시지를 준비합니다. 예를 들어 Json 형식의 메시지를 준비합니다.
앱이 보호하려는 메시지의 암호화 해시를 계산합니다. 예를 들어 SHA-256 또는 SHA-3-256 해싱 알고리즘을 사용해 계산합니다.
앱이 Play Integrity API를 호출하고 setNonce()를 호출하여 nonce 필드를 이전 단계에서 계산된 암호화 해시로 설정합니다.
앱이 보호하려는 메시지와 Play Integrity API의 서명된 결과를 모두 서버로 보냅니다.
앱 서버가 수신한 메시지의 암호화 해시가 서명된 결과의 nonce 필드 값과 일치하는지 확인하고, 일치하지 않는 결과는 전부 거부합니다.
다음 시퀀스 다이어그램에서 이러한 단계를 보여줍니다.
보호할 원본 메시지가 서명된 결과와 함께 전송되고 서버와 클라이언트가 모두 nonce 계산을 위해 정확히 똑같은 메커니즘을 사용하는 한, 이를 통해 메시지가 변조되지 않았음을 확실히 보장합니다.
이 시나리오에서는 보안 모델이 기기나 앱이 아니라 네트워크에서 공격이 발생한다는 가정 하에 작동하므로, Play Integrity API가 제공하는 기기 및 앱 무결성 신호도 확인하는 것이 특히 중요합니다.
리플레이 공격 방지
악의적인 사용자가 Play Integrity API로 보호되는 서버-클라이언트 앱과 상호 작용하되, 서버가 이를 감지하지 못하도록 손상된 기기와 상호 작용하려고 하는 또 다른 시나리오를 생각해 보겠습니다.
이를 위해, 공격자는 먼저 합법적인 기기로 앱을 사용하고 Play Integrity API의 서명된 응답을 수집합니다. 그런 다음 공격자는 손상된 기기로 앱을 사용하고, Play Integrity API 호출을 가로채며, 무결성 검사를 수행하는 대신에 이전에 기록된 서명된 응답을 그냥 반환합니다.
서명된 응답은 어떤 식으로든 변경되지 않았기에 디지털 서명이 괜찮아 보일 것이며, 앱 서버는 앱이 합법적인 기기와 통신하고 있다고 생각하도록 속아 넘어갈 수 있습니다. 이것을 리플레이 공격이라고 합니다.
이와 같은 공격에 대한 첫 번째 방어선은 서명된 응답의 timestampMillis 필드를 확인하는 것입니다. 이 필드는 응답이 생성된 시점의 타임스탬프를 포함하며, 인증된 디지털 서명으로 확인되었더라도 의심스럽게 오래된 응답을 감지하는 데 유용할 수 있습니다.
즉, Play Integrity API의 nonce를 활용하여 각 응답에 고유한 값을 할당하고 응답이 이전에 설정된 고유한 값과 일치하는지 확인하는 것도 가능합니다. 구현은 다음과 같습니다.
서버가 악의적인 사용자는 도저히 예측할 수 없는 방식으로 전역적으로 고유한 값을 생성합니다. 암호화 기술상 확실히 안전한 128비트 이상의 난수를 예로 들 수 있습니다.
앱이 Play Integrity API를 호출하고 nonce 필드를 앱 서버에서 수신한 고유한 값으로 설정합니다.
앱이 Play Integrity API의 서명된 결과를 서버로 보냅니다.
서버가 서명된 결과의 nonce 필드가 이전에 생성한 고유한 값과 일치하는지 확인하고, 일치하지 않는 결과는 전부 거부합니다.
다음 시퀀스 다이어그램에서 이러한 단계를 보여줍니다.
이러한 구현에서는 서버가 앱에 Play Integrity API 호출을 요청할 때마다 전역적으로 고유한 다른 값으로 호출하므로, nonce가 예상 값과 일치하지 않을 것이기 때문에 공격자가 이 값을 예측할 수 없는 한 이전 응답을 재사용하기란 불가능합니다.
두 보호 기능 결합
위에서 설명한 두 가지 메커니즘은 서로 매우 다른 방식으로 작동하지만, 앱이 두 가지 보호 기능을 동시에 요구하는 경우에는 두 보호 기능을 단일 Play Integrity API 호출에 결합할 수 있습니다. 예를 들어 두 보호 기능의 결과를 더 큰 base 64 nonce에 이어 붙입니다. 두 접근 방식을 결합한 구현은 다음과 같습니다.
사용자가 중요 작업을 시작합니다.
앱이 그 요청을 식별하기 위해 서버에 고유한 값을 요청합니다.
앱 서버가 악의적인 사용자는 도저히 예측할 수 없는 방식으로 전역적으로 고유한 값을 생성합니다. 예를 들어, 암호화 기술상 확실히 안전한 난수 생성기를 사용하여 그와 같은 값을 생성할 수 있습니다. 128비트 이상의 값을 생성하는 것이 좋습니다.
앱 서버가 전역적으로 고유한 값을 앱에 보냅니다.
앱이 보호하려는 메시지를 준비합니다. 예를 들어 Json 형식의 메시지를 준비합니다.
앱이 보호하려는 메시지의 암호화 해시를 계산합니다. 예를 들어 SHA-256 또는 SHA-3-256 해싱 알고리즘을 사용해 계산합니다.
앱이 앱 서버에서 받은 고유한 값과 보호하려는 메시지의 해시를 이어 붙인 문자열을 만듭니다.
앱이 Play Integrity API를 호출하고 setNonce()를 호출하여 nonce 필드를 이전 단계에서 생성된 문자열로 설정합니다.
앱이 보호하려는 메시지와 Play Integrity API의 서명된 결과를 모두 서버로 보냅니다.
앱 서버가 nonce 필드의 값을 분할하고, 메시지의 암호화 해시는 물론이고 이전에 생성한 고유한 값이 예상 값과 일치하는지 확인하고, 일치하지 않는 결과는 전부 거부합니다.
다음 시퀀스 다이어그램에서 이러한 단계를 보여줍니다.
이는 nonce를 사용해 악의적인 사용자로부터 앱을 추가로 보호할 수 있는 방법에 대한 몇 가지 예입니다. 앱이 민감한 데이터를 처리하거나 혹은 남용에 취약한 경우, Play Integrity API의 도움을 받아 이러한 위협을 완화하기 위한 조치를 취하시기 바랍니다.
Play Integrity API 사용에 대해 자세히 알아보고 시작하려면 g.co/play/integrityapi 웹 페이지를 확인해 보세요.