关于ios:如何在我的”第三方服务器”上验证GKLocalPlayer?

How to authenticate the GKLocalPlayer on my 'third party server'?

iOS7引入了新的GKLocalPlayer方法generateIdentityVerificationSignatureWithCompletionHandler().

有人知道如何永久使用它吗?
我假设苹果服务器端会有一些公共 API ..


这是一个 C# WebApi 服务器端版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public class GameCenterController : ApiController
{
    // POST api/gamecenter
    public HttpResponseMessage Post(GameCenterAuth data)
    {
        string token;
        if (ValidateSignature(data, out token))
        {
            return Request.CreateResponse(HttpStatusCode.OK, token);
        }
        return Request.CreateErrorResponse(HttpStatusCode.Forbidden, string.Empty);
    }

    private bool ValidateSignature(GameCenterAuth auth, out string token)
    {
        try
        {
            var cert = GetCertificate(auth.PublicKeyUrl);
            if (cert.Verify())
            {
                var csp = cert.PublicKey.Key as RSACryptoServiceProvider;
                if (csp != null)
                {
                    var sha256 = new SHA256Managed();
                    var sig = ConcatSignature(auth.PlayerId, auth.BundleId, auth.Timestamp, auth.Salt);
                    var hash = sha256.ComputeHash(sig);

                    if (csp.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA256"), Convert.FromBase64String(auth.Signature)))
                    {
                        // Valid user.
                        // Do server related user management stuff.
                        return true;
                    }
                }
            }

            // Failure
            token = null;
            return false;
        }
        catch (Exception ex)
        {
            // Log the error
            token = null;
            return false;
        }
    }

    private static byte[] ToBigEndian(ulong value)
    {
        var buffer = new byte[8];
        for (int i = 0; i < 8; i++)
        {
            buffer[7 - i] = unchecked((byte)(value & 0xff));
            value = value >> 8;
        }
        return buffer;
    }

    private X509Certificate2 GetCertificate(string url)
    {
        var client = new WebClient();
        var rawData = client.DownloadData(url);
        return new X509Certificate2(rawData);
    }

    private byte[] ConcatSignature(string playerId, string bundleId, ulong timestamp, string salt)
    {
        var data = new List<byte>();
        data.AddRange(Encoding.UTF8.GetBytes(playerId));
        data.AddRange(Encoding.UTF8.GetBytes(bundleId));
        data.AddRange(ToBigEndian(timestamp));
        data.AddRange(Convert.FromBase64String(salt));
        return data.ToArray();
    }
}


public class GameCenterAuth
{
    public string PlayerId { get; set; }
    public string BundleId { get; set; }
    public string Name { get; set; }
    public string PublicKeyUrl { get; set; }
    public string Signature { get; set; }
    public string Salt { get; set; }
    public ulong Timestamp { get; set; }
}


以下是使用objective C 进行身份验证的方法。如果你需要它在另一种语言应该是微不足道的翻译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
-(void)authenticate
{
    __weak GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
    localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error)
    {
        if(viewController)
        {
            [[[UIApplication sharedApplication] keyWindow].rootViewController presentViewController:viewController animated:YES completion:nil];
        }
        else if(localPlayer.isAuthenticated == YES)
        {
            [localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) {

                if(error != nil)
                {
                    return; //some sort of error, can't authenticate right now
                }

                [self verifyPlayer:localPlayer.playerID publicKeyUrl:publicKeyUrl signature:signature salt:salt timestamp:timestamp];


            }];
        }
        else
        {
            NSLog(@"game center disabled");
        }
    };
}

-(void)verifyPlayer:(NSString *)playerID publicKeyUrl:(NSURL *)publicKeyUrl signature:(NSData *)signature salt:(NSData *)salt timestamp:(uint64_t)timestamp
{
    //get certificate
    NSData *certificateData = [NSData dataWithContentsOfURL:publicKeyUrl];

    //build payload
    NSMutableData *payload = [[NSMutableData alloc] init];
    [payload appendData:[playerID dataUsingEncoding:NSASCIIStringEncoding]];
    [payload appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSASCIIStringEncoding]];

    uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
    [payload appendBytes:&timestampBE length:sizeof(timestampBE)];
    [payload appendData:salt];

    //sign
    SecCertificateRef certificateFromFile = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData); // load the certificate
    SecPolicyRef secPolicy = SecPolicyCreateBasicX509();

    SecTrustRef trust;
    OSStatus statusTrust = SecTrustCreateWithCertificates( certificateFromFile, secPolicy, &trust);
    if(statusTrust != errSecSuccess)
    {
        NSLog(@"could not create trust");
        return;
    }

    SecTrustResultType resultType;
    OSStatus statusTrustEval =  SecTrustEvaluate(trust, &resultType);
    if(statusTrustEval != errSecSuccess)
    {
        NSLog(@"could not evaluate trust");
        return;
    }

    if(resultType != kSecTrustResultProceed && resultType != kSecTrustResultRecoverableTrustFailure)
    {
        NSLog(@"server can not be trusted");
        return;
    }

    SecKeyRef publicKey = SecTrustCopyPublicKey(trust);
    uint8_t sha256HashDigest[CC_SHA256_DIGEST_LENGTH];
    CC_SHA256([payload bytes], (CC_LONG)[payload length], sha256HashDigest);

    //check to see if its a match
    OSStatus verficationResult = SecKeyRawVerify(publicKey,  kSecPaddingPKCS1SHA256, sha256HashDigest, CC_SHA256_DIGEST_LENGTH, (const uint8_t *)[signature bytes], [signature length]);

    CFRelease(publicKey);
    CFRelease(trust);
    CFRelease(secPolicy);
    CFRelease(certificateFromFile);
    if (verficationResult == errSecSuccess)
    {
        NSLog(@"Verified");
    }
    else
    {
        NSLog(@"Danger!!!");
    }
}

编辑:

自 2015 年 3 月 2 日起,Apple 现在在证书上使用 SHA256 而不是 SHA1。 https://devforums.apple.com/thread/263789?tstart=0


我花了很多时间在 PHP 中实现它。现在我想分享我的结果。

文档

您可以在 Apple 找到一个非常简单的文档:https://developer.apple.com/library/ios/documentation/GameKit/Reference/GKLocalPlayer_Ref/index.html#//apple_ref/occ/instm/GKLocalPlayer/generateIdentityVerificationSignatureWithCompletionHandler

[...]

  • 使用第三方服务器上的 publicKeyURL 下载公钥。
  • 向适当的签名机构验证公钥是否由 Apple 签名。
  • 检索玩家的 playerID 和 bundleID。
  • 按照列出的顺序将以下信息连接到数据缓冲区中:

    • UTF-8 格式的 playerID 参数
    • UTF-8 格式的 bundleID 参数
    • Big-Endian UInt-64 格式的时间戳参数
    • salt参数
  • 为缓冲区生成一个 SHA-256 哈希值。
  • 使用步骤 3 中下载的公钥,验证步骤 7 中生成的哈希值是否与 API 提供的签名参数匹配。
  • 注意!数字 7 是 PHP 中的一个陷阱,花了我几个小时。您只需将原始连接字符串传递给 openssl_verify() 函数。

    2014 年 7 月 9 日更新的问题如何使用 PHP 在我的"第三方服务器"上验证 GKLocalPlayer?帮我找到了问题。

    最终来源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    <?php
    // signature, publicKeyUrl, timestamp and salt are included in the base64/json data you will receive by calling generateIdentityVerificationSignatureWithCompletionHandler.

    $timestamp = $params["timestamp"]; // e.g. 1447754520194
    $user_id = $params["user_id"]; // e.g. G:20010412315
    $bundle_id ="com.example.test";
    $public_key_url = $params["publicKeyUrl"]; // e.g. https://static.gc.apple.com/public-key/gc-prod-2.cer
    $salt=base64_decode($params["salt"]); // Binary
    $signature = base64_decode($params["signature"]); // Binary

    // Timestamp is unsigned 64-bit integer big endian
    $highMap = 0xffffffff00000000;
    $lowMap = 0x00000000ffffffff;
    $higher = ($timestamp & $highMap) >>32;
    $lower = $timestamp & $lowMap;
    $timestamp = pack('NN', $higher, $lower);

    // Concatenate the string
    $data = $user_id . $bundle_id . $timestamp . $salt;

    // ATTENTION!!! Do not hash it! $data = hash("sha256", $packed);

    // Fetch the certificate. This is dirty because it is neither cached nor verified that the url belongs to Apple.
    $ssl_certificate = file_get_contents($public_key_url);

    $pem = chunk_split(base64_encode($ssl_certificate), 64,"\
    ");
    $pem ="-----BEGIN CERTIFICATE-----\
    " . $pem ."-----END CERTIFICATE-----\
    ";

    // it is also possible to pass the $pem string directly to openssl_verify
    if (($pubkey_id = openssl_pkey_get_public($pem)) === false) {
        echo"invalid public key\
    ";
        exit;
    }

    // Verify that the signature is correct for $data
    $verify_result = openssl_verify($data, $signature, $pubkey_id, OPENSSL_ALGO_SHA256);

    openssl_free_key($pubkey_id);

    switch($verify_result) {
      case 1:
        echo"Signature is ok.\
    ";
        break;
      case 0:
        echo"Signature is wrong.\
    ";
        break;
      default:
        echo"An error occurred.\
    ";
        break;
    }

    添加 Python 的答案,但使用 PyCrypto 2.6(这是 Google App Engine 解决方案)。
    另请注意,此处未完成下载后对公共证书的验证,类似于上面使用 OpenSSL 的 python 答案。这一步真的有必要吗?如果我们检查公钥 URL 是否要访问苹果域并且它使用 ssl (https),??这是否意味着它受到保护免受中间人攻击?

    不管怎样,这里是代码。请注意,二进制文本在连接和使用之前会重新转换为二进制。此外,我必须更新我的本地 python 安装以使用 PyCrypto 2.6,然后才能使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    from Crypto.PublicKey import RSA
    from Crypto.Signature import PKCS1_v1_5
    from Crypto.Hash import SHA
    from base64 import b64decode
    from Crypto.Util.asn1 import DerSequence
    from binascii import a2b_base64
    import struct
    import urlparse

    def authenticate_game_center_user(gc_public_key_url, app_bundle_id, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature):

        apple_cert = urllib2.urlopen(gc_public_key_url).read()

        #Verify the url is https and is pointing to an apple domain.
        parts = urlparse.urlparse(gc_public_key_url)
        domainName =".apple.com"
        domainLocation = len(parts[1]) - len(domainName)
        actualLocation = parts[1].find(domainName)
        if parts[0] !="https" or domainName not in parts[1] or domainLocation != actualLocation:
            logging.warning("Public Key Url is invalid.")
            raise Exception

        cert = DerSequence()
        cert.decode(apple_cert)
        tbsCertificate = DerSequence()
        tbsCertificate.decode(cert[0])
        subjectPublicKeyInfo = tbsCertificate[6]

        rsakey = RSA.importKey(subjectPublicKeyInfo)
        verifier = PKCS1_v1_5.new(rsakey)

        payload = gc_player_id.encode('UTF-8')
        payload = payload + app_bundle_id.encode('UTF-8')
        payload = payload + struct.pack('>Q', int(gc_timestamp))
        payload = payload + b64decode(gc_salt)
        digest = SHA.new(payload)

        if verifier.verify(digest, b64decode(gc_unverified_signature)):
            print"The signature is authentic."
        else:
            print"The signature is not authentic."


    谢谢,@odyth。谢谢,@Lionel。
    我想在这里添加 Python 版本(基于你的)。它有一个小缺陷 - Apple 证书未经验证 - 在 pyOpenSSL 绑定中没有这样的 API。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import urllib2
    import OpenSSL
    import struct

    def authenticate_game_center_user(gc_public_key_url, app_bundle_id, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature):
        apple_cert = urllib2.urlopen(gc_public_key_url).read()
        gc_pkey_certificate = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, apple_cert)

        payload = gc_player_id.encode('UTF-8') + app_bundle_id.encode('UTF-8') + struct.pack('>Q', int(gc_timestamp)) + gc_salt

        try:
            OpenSSL.crypto.verify(gc_pkey_certificate, gc_unverified_signature, payload, 'sha1')
            print 'Signature verification is done. Success!'
        except Exception as res:
            print res

    public_key_url = 'https://sandbox.gc.apple.com/public-key/gc-sb.cer'
    player_GC_ID = 'G:1870391344'
    timestamp = '1382621610281'
    your_app_bundle_id = 'com.myapp.bundle_id'

    with open('./salt.dat', 'rb') as f_salt:
        with open('./signature.dat', 'rb') as f_sign:
            authenticate_game_center_user(public_key_url, your_app_bundle_id, player_GC_ID, timestamp, f_salt.read(), f_sign.read())


    感谢代码示例,golang 解决方案来了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    func DownloadCert(url string) []byte {
        b, err := inet.HTTPGet(url)
        if err != nil {
            log.Printf("http request error %s", err)
            return nil
        }
        return b
    }


    func VerifySig(sSig, sGcId, sBundleId, sSalt, sTimeStamp string, cert []byte) (err error) {
        sig, err := base64.StdEncoding.DecodeString(sSig)
        if err != nil {
            return
        }
        salt, err := base64.StdEncoding.DecodeString(sSalt)
        if err != nil {
            return
        }
        timeStamp, err := strconv.ParseUint(sTimeStamp, 10, 64)
        if err != nil {
            return
        }

        payload := new(bytes.Buffer)
        payload.WriteString(sGcId)
        payload.WriteString(sBundleId)
        binary.Write(payload, binary.BigEndian, timeStamp)
        payload.Write(salt)

        return verifyRsa(cert, sig, payload.Bytes())
    }

    func verifyRsa(key, sig, content []byte) error {
        cert, err := x509.ParseCertificate(key)
        if err != nil {
            log.Printf("parse cert error %s", err)
            return err
        }
        pub := cert.PublicKey.(*rsa.PublicKey)

        h := sha256.New()
        h.Write(content)
        digest := h.Sum(nil)

        err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest, sig)
        return err
    }

    一个小小的http助手

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func HTTPGet(fullUrl string) (content []byte, err error) {
        log.Printf("http get url %s", fullUrl)
        resp, err := http.Get(fullUrl)
        if err != nil {
            log.Printf("url can not be reached %s,%s", fullUrl, err)
            return
        }

        if resp.StatusCode != http.StatusOK {
            return nil, errors.New("ERROR_STATUS_NOT_OK")
        }
        body := resp.Body
        content, err = ioutil.ReadAll(body)
        if err != nil {
            log.Printf("url read error %s, %s", fullUrl, err)
            return
        }
        body.Close()
        return
    }

    测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func TestVerifyFull(t *testing.T) {
        cert := DownloadCert("https://sandbox.gc.apple.com/public-key/gc-sb-2.cer")
        if cert == nil {
            log.Printf("cert download error")
        }
        sig :="sig as base64"
        salt :="salt as base64"
        timeStamp :="1442816155502"
        gcId :="G:12345678"
        bId :="com.xxxx.xxxx"
        err := VerifySig(sig, gcId, bId, salt, timeStamp, cert)
        log.Printf("result %v", err)
    }

    验证证书下载 url 的小功能。防止从任何地方下载任何东西

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func IsValidCertUrl(fullUrl string) bool {
        //https://sandbox.gc.apple.com/public-key/gc-sb-2.cer
        uri, err := url.Parse(fullUrl)
        if err != nil {
            log.Printf("not a valid url %s", fullUrl)
            return false
        }

        if !strings.HasSuffix(uri.Host,"apple.com") {
            log.Printf("not a valid host %s", fullUrl)
            return false
        }

        if path.Ext(fullUrl) !=".cer" {
            log.Printf("not a valid ext %s, %s", fullUrl, path.Ext(fullUrl))
            return false
        }
        return true
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    require 'base64'
    require 'httparty'

    module GameCenter
      include HTTParty

      # HHTTParty settings
      HTTPARTY_TIMEOUT = 10

      def authenticate_game_center_user(gc_public_key_url, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature)
        # Get game center public key certificate
        gc_pkey_certificate = get_gc_public_key_certificate(gc_public_key_url)

        # Check public key certificate
        unless public_key_certificate_is_valid?(gc_pkey_certificate) do
            # Handle error
        end
        # Check SSL errors
        unless OpenSSL.errors.empty? do
            # Handle OpenSSL errors
        end
        # Payload building
        payload = build_payload(gc_player_id, gc_timestamp, gc_salt)

        # Test signature
        unless signature_is_valid?(gc_pkey_certificate, gc_unverified_signature, payload) do
            # Handle error
        end
        # Check SSL errors
        unless OpenSSL.errors.empty? do
            # Handle OpenSSL errors
        end

        # Return player ID
        gc_player_id
      end

      def build_payload(player_id, timestamp, salt)
        player_id.encode("UTF-8") +"com.myapp.bundle_id".encode("UTF-8") + [timestamp.to_i].pack("Q>") + salt
      end

      private

      def get_gc_public_key_certificate(url)
        cert = HTTParty.get(url, timeout: HTTPARTY_TIMEOUT, debug_output: Rails.env.production?)
        OpenSSL::X509::Certificate.new(cert)
      rescue SocketError => e
        puts"Key error:" + e.inspect.to_s
      end

      def get_ca_certificate
        OpenSSL::X509::Certificate.new(File.read('./certs/apple/verisign_class_3_code_signing_2010_ca.cer'))
      end

      def public_key_certificate_is_valid?(pkey_cert)
        pkey_cert.verify(get_ca_certificate.public_key)
      end

      def signature_is_valid?(pkey_cert, signature, payload)
        pkey_cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, payload)
      end

    end

    这是我的 ruby?? 实现(作为模块)。多亏了您的 Objective-C,它变得容易多了。

    请注意,我在第三方 ssl 服务网站上被迫下载 CA 证书,因为公钥证书尚未签署 Apple 并且 Apple 未提供任何 CA 证书来验证沙盒游戏中心到目前为止的证书。

    我还没有在生产中测试过这个,但是在沙盒模式下它可以正常工作。


    这是我在 Elixir 中的实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    def verify_login(player_id, public_key_url, timestamp, salt64, signature64, bundle_id) do
      salt=Base.decode64!(salt64)
      pay_load = <<player_id :: binary, bundle_id :: binary, timestamp :: big-size(64), salt :: binary>>
      pkey_cert = get_public_key_certificate(public_key_url)
      cert = :public_key.pkix_decode_cert(pkey_cert, :otp)
      case cert do
        {:OTPCertificate,
         {:OTPTBSCertificate, _, _, _, _, _, _,
          {:OTPSubjectPublicKeyInfo, _, key}, _, _, _}, _, _} ->
          signature = Base.decode64!(signature64)
          case :public_key.verify(pay_load, :sha256, signature, key) do
            true ->
              :ok
            false ->
              {:error,"apple login verify failed"}
          end
      end
    end

    def get_public_key_certificate(url) do
      case HTTPoison.get(url) do
        {:ok, %HTTPoison.Response{body: body}} ->
          body
      end
    end

    这是一个更新和改进的 Ruby 版本。我已经使用 Apple 沙箱对其进行了测试,但还没有进行生产。我还记录了从哪里获取 CA 证书,以验证您从公钥 URL 收到的证书。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    # iOS Game Center verifier for 3rd party game servers written in Ruby.
    #
    # *** Credits ***
    #   Based off of code and comments at https://stackoverflow.com/questions/17408729/how-to-authenticate-the-gklocalplayer-on-my-third-party-server
    #
    # *** Improvements ***
    #   This version uses Ruby's built in HTTP client instead of a 3rd party gem.
    #   It's updated to use SHA256 instead of SHA1.
    #   It Base64 decodes the salt and signature.  If your client or server already does this then you will need to remove the calls to Base64.decode64().
    #   It validates that the public key URL is from apple.com.
    #   It has been tested with Apple's Game Center's sandbox public key URL (https://sandbox.gc.apple.com/public-key/gc-sb-2.cer) and works as of June 24th, 2015.
    #
    # *** Notes on public key certificate validation ***
    #   You will need the correct code signing CA to verify the certificate returned from the pubic key URL.
    #   You can download/verify the CA certificate here: https://knowledge.symantec.com/support/code-signing-support/index?page=content&actp=CROSSLINK&id=AR2170
    #   I have embedded the CA certificate for convenience so that you don't need to save it to your filesystem.
    #   When the public key URL changes in the future, you may need to update the text in the ca_certificate_text() method.
    #
    # *** Usage ***
    #   verified, reason = GameCenterVerifier.verify(...)

    class GameCenterVerifier
      # Verify that user provided Game Center data is valid.
      # False will be returned along with a reason if any validations fail.
      # Otherwise, it will return true and a nil reason if all validations pass.
      def self.verify(game_center_id, public_key_url, timestamp, salt, signature, bundle_id)
        salt=Base64.decode64(salt)
        signature = Base64.decode64(signature)
        payload = game_center_id.encode('UTF-8') + bundle_id.encode('UTF-8') + [timestamp.to_i].pack('Q>') + salt
        pkey_certificate = get_public_key_certificate(public_key_url)
        return false, 'Invalid public key url' unless public_key_url_is_valid?(public_key_url)
        return false, 'Invalid public key certificate' unless public_key_certificate_is_valid?(pkey_certificate)
        return false, 'OpenSSL errors (before signature check)' unless OpenSSL.errors.empty?
        return false, 'Invalid signature' unless signature_is_valid?(pkey_certificate, signature, payload)
        return false, 'OpenSSL errors (after signature check)' unless OpenSSL.errors.empty?
        return true, nil
      end

      private

      def self.get_public_key_certificate(url)
        uri = URI.parse(url)
        http = Net::HTTP.new(uri.host, uri.port)
        request = Net::HTTP::Get.new(uri.request_uri)
        http.use_ssl = true
        http.open_timeout = 5
        http.read_timeout = 5
        cert = http.request(request).body
        OpenSSL::X509::Certificate.new(cert)
      end

      def self.public_key_url_is_valid?(public_key_url)
        uri = URI(public_key_url)
        tokens = uri.host.split('.')
        return false if uri.scheme != 'https'
        return false if tokens[-1] != 'com' || tokens[-2] != 'apple'
        true
      end

      def self.public_key_certificate_is_valid?(pkey_cert)
        pkey_cert.verify(get_ca_certificate.public_key)
      end

      def self.signature_is_valid?(pkey_cert, signature, payload)
        pkey_cert.public_key.verify(OpenSSL::Digest::SHA256.new, signature, payload)
      end

      def self.get_ca_certificate
        OpenSSL::X509::Certificate.new(ca_certificate_text)
      end

      def self.ca_certificate_text
        data = <<EOF
    -----BEGIN CERTIFICATE-----
    MIIFWTCCBEGgAwIBAgIQPXjX+XZJYLJhffTwHsqGKjANBgkqhkiG9w0BAQsFADCB
    yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
    ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp
    U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
    ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
    aG9yaXR5IC0gRzUwHhcNMTMxMjEwMDAwMDAwWhcNMjMxMjA5MjM1OTU5WjB/MQsw
    CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV
    BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVjIENs
    YXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
    ggEPADCCAQoCggEBAJeDHgAWryyx0gjE12iTUWAecfbiR7TbWE0jYmq0v1obUfej
    DRh3aLvYNqsvIVDanvPnXydOC8KXyAlwk6naXA1OpA2RoLTsFM6RclQuzqPbROlS
    Gz9BPMpK5KrA6DmrU8wh0MzPf5vmwsxYaoIV7j02zxzFlwckjvF7vjEtPW7ctZlC
    n0thlV8ccO4XfduL5WGJeMdoG68ReBqYrsRVR1PZszLWoQ5GQMWXkorRU6eZW4U1
    V9Pqk2JhIArHMHckEU1ig7a6e2iCMe5lyt/51Y2yNdyMK29qclxghJzyDJRewFZS
    AEjM0/ilfd4v1xPkOKiE1Ua4E4bCG53qWjjdm9sCAwEAAaOCAYMwggF/MC8GCCsG
    AQUFBwEBBCMwITAfBggrBgEFBQcwAYYTaHR0cDovL3MyLnN5bWNiLmNvbTASBgNV
    HRMBAf8ECDAGAQH/AgEAMGwGA1UdIARlMGMwYQYLYIZIAYb4RQEHFwMwUjAmBggr
    BgEFBQcCARYaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9jcHMwKAYIKwYBBQUHAgIw
    HBoaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9ycGEwMAYDVR0fBCkwJzAloCOgIYYf
    aHR0cDovL3MxLnN5bWNiLmNvbS9wY2EzLWc1LmNybDAdBgNVHSUEFjAUBggrBgEF
    BQcDAgYIKwYBBQUHAwMwDgYDVR0PAQH/BAQDAgEGMCkGA1UdEQQiMCCkHjAcMRow
    GAYDVQQDExFTeW1hbnRlY1BLSS0xLTU2NzAdBgNVHQ4EFgQUljtT8Hkzl699g+8u
    K8zKt4YecmYwHwYDVR0jBBgwFoAUf9Nlp8Ld7LvwMAnzQzn6Aq8zMTMwDQYJKoZI
    hvcNAQELBQADggEBABOFGh5pqTf3oL2kr34dYVP+nYxeDKZ1HngXI9397BoDVTn7
    cZXHZVqnjjDSRFph23Bv2iEFwi5zuknx0ZP+XcnNXgPgiZ4/dB7X9ziLqdbPuzUv
    M1ioklbRyE07guZ5hBb8KLCxR/Mdoj7uh9mmf6RWpT+thC4p3ny8qKqjPQQB6rqT
    og5QIikXTIfkOhFf1qQliZsFay+0yQFMJ3sLrBkFIqBgFT/ayftNTI/7cmd3/SeU
    x7o1DohJ/o39KK9KEr0Ns5cF3kQMFfo2KwPcwVAB8aERXRTl4r0nS1S+K4ReD6bD
    dAUK75fDiSKxH3fzvc1D1PFMqT+1i4SvZPLQFCE=
    -----END CERTIFICATE-----
    EOF
      end
    end


    感谢那些提供其他语言解决方案的人。

    以下是 Scala 中的相关解决方案(转换为 Java 很简单):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    private def verify(
      signatureAlgorithm: String,
      publicKey: PublicKey,
      message: Array[Byte],
      signature: Array[Byte]): Boolean = {

      val sha1Signature = Signature.getInstance(signatureAlgorithm)
      sha1Signature.initVerify(publicKey)
      sha1Signature.update(message)
      sha1Signature.verify(signature)
    }

    val x509Cert = Try(certificateFactory.generateCertificate(new ByteArrayInputStream(publicKeyBytes)).asInstanceOf[X509Certificate])
    x509Cert.foreach { cert =>
      signatureAlgorithm = Some(cert.getSigAlgName)
    }
    x509Cert.map(_.getPublicKey) match {
      case Success(pk) =>
        log.debug("downloaded public key successfully")
        publicKey = Some(pk)
    }

    val buffer =
      r.id.getBytes("UTF-8") ++
      bundleId.getBytes("UTF-8") ++
      ByteBuffer.allocate(8).putLong(r.timestamp).array() ++
      Base64.decode(r.salt)

    val result = verify(signatureAlgorithm.getOrElse("SHA256withRSA"), pk, buffer, Base64.decode(r.signature))

    log.info("verification result {} for request {}", result, r)

    其中 r 是一个实例:

    1
    2
    3
    4
    5
    6
    7
    case class IOSIdentityVerificationRequest(
      id: PlayerIdentity, // String
      publicKeyURL: String,
      signature: String, // base64 encoded bytes
      salt: String, // base64 encoded bytes
      timestamp: Long,
      error: Option[String]) extends IdentityVerificationRequest