본문 바로가기

진리는어디에

[C++] 구글 인앱 결제 영수증 서버 검증 (Server side Google Play receipts verification using C++)

들어가며

필자가 만드는 모바일 게임 서버는 C++기반으로 작성되어 있다. php나 python을 이용한 서버 사이드 구글 인앱 결제 검증 예제는 인터넷에서 쉽게 찾을 수 있었지만 C/C++의 경우는 관련 예제를 찾을 수 없어 고생하다 openssl 기반 검증 코드를 직접 만들어야만했고 실제 사용하면서 문제가 없었기에 여기에 공유하도록 한다. 코드를 보시는 분들의 이해를 돕기 위해 실제 사용된 퍼블릭 키와 영수증을 예제에 첨부 했으므로 테스트 해보기 편할 것이라 생각한다.

본 포스트에 사용된 예제 프로젝트의 전체 코드는 [여기]에서 확인 할 수 있다.

본 포스트에서는..

  • 클라이언트 사이드 구글 인앱 결제를 어떻게 하는지 설명하지 않는다. 워낙 클라이언트 종류가 많고 그걸 일일이 다 나열하기에는 시간과 공간이 허럭하지 않는다. 여기에서는 구글 플레이 스토어와 결제가 완료 된 후 구글로 부터 돌려 받은 영수증과 signature 데이터를 검증하는 방법에 대해서 다룬다.
  • 구글 결제 프로세스에 대해 설명하지 않는다. 결제 프로세스가 궁금한 분들은 구글 공식 문서와 '구글 인앱(in-app) 결제, 서버 사이드 인증의 불편한 진실'을 참고하길 바란다.
  • Google Developer Console에 등록하는 방법, public_key를 얻는 방법, 상품을 등록하는 방법들에 대해 설명하지 않는다. 이미 인터넷에 관련 글들이 충분히 작성 되어 있다.
  • C++ 언어 사용자를 대상으로 한다. 본 포스트에 포함된 예제는 std::shared_ptr를 사용했으니 C++11 이상 컴파일러를 사용해야 한다. 아니면 코드를 약간만 수정하여 shared_ptr에서 자동으로 호출 되는 XXX_free 함수들을 직접 호출하면 된다.

OpenSSL 설치

OpenSSL을 기반으로 작성된 코드이므로 무엇보다 OpenSSL 라이브러리 설치가 필수다. OpenSSL 라이브러리는 https://www.openssl.org/source/에서 다운로드 할 수 있다. 공식 사이트에서는 소스 코드 형태로 배포하기 때문에 별도의 빌드 과정이 필요하다.

만일 빌드가 불가능하거나 번거롭다면 리눅스의 경우에는 패키지 인스톨 시스템이 잘 되어 있어 쉽게 설치가 가능할 것이다. 필자의 개발 환경은 윈도우이므로 윈도우용 OpenSSL 라이브러리 Win64OpenSSL-1_0_2c.exe를 설치했다.

OpenSSL 라이브러리를 프로젝트에 링크하는 방법은 본 포스트 맨 아래에 설명 되어 있으니 혹시나 라이브러리 링크에 어려움을 겪는다면 살펴 보도록 하자.

base64 Encoding/Decoding

구글에서 돌려 주는 signature 데이터는 base64로 인코딩 되어 있다. 하지만 signature를 openssl 라이브러리에서 사용하기 위해서는 base64로 디코딩해서 사용해야 한다. C++에서는 base64관련 표준 라이브러리를 제공하지 않으므로 서드파티 라이브러리를 사용하거나 직접 작성해야 한다. 아래는 참고하거나 복붙으로 가져다 쓸만한 base64 모듈이다.

Example

아래는 C++을 이용하여 구글 영수증을 검증하는 간단한 예제다. 익숙하지 않은 openssl 라이브러리 함수들이 당황스러울수도 있지만 그리 어려운 내용은 아니므로 한번 쓱 살펴보고, 복붙해서 사용하면 된다. 실제 필자가 만든 게임에서 사용된 영수증과 공개키를 예제에 포함했으므로 테스트하긴 편할 것이다. 고마우면 좋아요와 댓글 한번 달아주면 된다.

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <memory>
#include <iostream>
#include "Base64.h" // 별도 Base64 코드 필요

bool InappBillingVerify(const char* data, const char* signature, const char* pub_key_id)
{
    std::shared_ptr<EVP_MD_CTX> mdctx = std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_destroy);
    const EVP_MD* md = EVP_get_digestbyname("SHA1");

    EVP_VerifyInit_ex(mdctx.get(), md, NULL);

    EVP_VerifyUpdate(mdctx.get(), (void*)data, strlen(data));

    std::shared_ptr<BIO> b64 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(), pub_key_id);
    BIO_push(b64.get(), bPubKey.get());
    std::shared_ptr<EVP_PKEY> pubkey = std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b64.get(), NULL), EVP_PKEY_free);
    std::string decoded_signature = Base64Decode(std::string(signature)); // 별도 Base64 코드 필요. 그냥 컴파일하면 에러남

    return 1 == EVP_VerifyFinal(
        mdctx.get(),
        (unsigned char*)decoded_signature.c_str(),
        decoded_signature.length(),
        pubkey.get()
    );
}

int main()
{
    // 결제 완료 후 구글로 부터 받은 영수증
    const char* receipt = "{"
        "\"orderId\":\"GPA.3331-7513-9788-96070\","
        "\"packageName\":\"com.kukuta.pentatiles\","
        "\"productId\":\"pentatiles.google.hint.10\","
        "\"purchaseTime\":1633449519729,"
        "\"purchaseState\":0,"
        "\"purchaseToken\":\"apookopndinajikkicgkkifo.AO-J1OzvmCTyKoD4-I93-1xHhddHSpseIRCbBup53Vl83o7A2LwUX9Wl3-2Hnml69AI3p6ZNtHrNoQYE7mMt3VYopfkCrPfAJ9m_HBIrjd_ZTHCTW6TMQlQ\","
        "\"acknowledged\":false"
    "}";

    // 공개키
    const char* signature = "nGNND0XpGqUNMA8GZ69BFsGEXYtqWukTaETrzf8dhxqWGo2zB1ZV7xzujruLnRVqwJD3cb9PtV2bEgTF7VrNpuxoXIiOxJNleJ05L0g+O0ex6BClBUscPeE5TnjMnEBfk6IOs0r8VFaq9/EmDSG4f4KkurprNVenpCmtBqSQPPj9wYR1BNu8fW9qVrTzx3RqpN41ytwyqm2OmW4Of0gLDlvrAYBsv43pzJD+J6ejX9fcVfZc1ZpO7pgi/fsirYah9R+BFZQCML6spFZwrzG5w+WfmpNTfwIzBFJ9m4d7DckKxIwCoQNsORaKSMCIGvynRGYaalGFFG4Bx5FNWcpsDg==";

    const char* public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzc9hFdWcGG6eONxzt/bfnk+MYIwbroAY+V/5b8I8R+z8VIKUvYyLcNEYOzYhOdQ+F0lPVS49t/TTAJaynrvdbixdbb0zMftCWcBFBOOnsU30D3jo7yzzhsOpbClI+hi4fApb0/I21VLVlol8mW0r++537cKKibaYZy1MbvCDJyUDRfmTVaAg3X1ZDROhGS8epZuDrEXXfGkKrmlXV9gA+0pRiZn3cjb3E13KE1ljCbjTUQNdBEK/pcTj9RhBnbn2qd3R7HiLdmBuctXALgRoupLNg37nhKi2rZKcY+afk6h82oPKROYhtdiaFUviZ1w4c3u8p9GJ+RzRN2Yc8eoM2QIDAQAB";

    OpenSSL_add_all_digests();
    std::cout << std::boolalpha << InappBillingVerify(receipt, signature, public_key) << std::endl;
    EVP_cleanup();

    return 0;
}

// OUTPUT :
// true
  • OpenSSL_add_all_digests(), EVP_cleanup() 함수가 InappBillingVerify() 함수 외부에 있다는 것에 주목하자. 앞의 두 함수는 thread-safe하지 않아 아래 예제에서 InappBillingVerify() 함수 내부에 위치하는 경우, 멀티 스레드 환경에서 여러 스레드가 동시에 InappBillingVerify()를 호출하면 에러를 발생 시킨다. 멀티 스레드의 경우 OpenSSL_add_all_digests, EVP_cleanup 함수를 호출함에 있어서 주의 하도록 하자.
  • receipt : 클라이언트에서 구글 인앱 결제 프로세스 완료 후 받은 결과 JSON 데이터
    • packageName : 패키지 이름. 정상적인 app으로 부터 도착한 메시지인지 검증
    • productId : 상품 아이디. 게임 서버에 상품아이디와 아이템을 매핑 시켜 놓고, 상품 아이디를 키로 삼아 아이템 지급
    • developerPayload : 개발시 임의로 넣는 값. 클라이언트 사이드에서 Google Play 구매 요청시 전송하면 결과 json에 포함되어 리턴. 매번 다른 값을 발급하여 인증 요청 중복 체크등에 사용 할수 있다.
  • signature : Google Play 결제 결과로 리턴되는 암호화 된 값(RSA+SHA1, Base64 encoded, PKCS#1 padding)
  • public_key : Google Developer Console에서 발급한 공개키
    문서에는 public license key라고 되어 있음 - 
    http://developer.android.com/training/in-app-billing/preparing-iab-app.html#AddToDevConsole 
  • 반환 값 :  valid : true invalid : false
  • 위 예제의 전체 코드는 [여기]에서 확인 할 수 있다.

마치며

지금까지 C++을 이용해 구글 인앱 결제 영수증을 검증하는 코드를 살펴 보았다. 실제 적용할 때 에러가 발생하는데 이것이 코드가 잘못 된것인지, 인증키 설정이나 base64 디코딩 등 다른 원인이 있는 것인지 원인을 제대로 찾지 못하는 케이스를 여러번 리포팅 받아서 아예 실제 공개키와 영수증을 예제에 포함했으므로 위 예제를 이용해 테스트하는데 보다 쉬울 것이다. 위 영수증 검증 코드가 의심스럽다면 receipt의 값을 변경하고 테스트 해보도록 하자.

이상 오늘의 포스팅을 마치도록 한다. 궁금한 점이 있다면 아래 댓글을 이용해 질문하도록 하자.

부록 1. 같이 읽으면 좋은 글

부록 2. OpenSSL 라이브러리 링크

헤더 파일 위치 지정. 필자의 경우는 Win64OpenSSL-1_0_2c.exe를 기본 위치에 설치했다.

헤더 파일 위치 지정

라이브러리 디렉토리 위치 지정

라이브러리 추가. libeay32.lib 파일을 '추가 종속성'에 추가 하도록 한다.

 

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!