如何在我的第三方服务器上验证GKLocalPlayer'用PHP?

时间:2014-07-08 00:07:15

标签: php ios ios7 unity3d game-center

请原谅我的笨拙,我是Stackoverflow,C#和Objective C的新手。

简而言之,我正在尝试做这个问题的答案,但在PHP中: How to authenticate the GKLocalPlayer on my 'third party server'? 希望这也可以帮助其他PHP开发人员做同样的事情。

我正在使用Unity(Unity3D)和PHP服务器端。我已经将Objective C正确连接到GameCenter并通过调用generateIdentityVerificationSignatureWithCompletionHandler返回数据。 不幸的是,我无法弄清楚我在验证SHA1哈希时做错了什么。过去一周我一直在研究这个问题,尝试各种各样的事情,但没有运气。

我正在尝试三种不同的方式来制作SHA1哈希(如下所示)。一旦进入Objective C,另一个进入Unity的C#,最后第三次进入我的PHP服务器。 Objective C和C#SHA1哈希最终完全相同。但是,PHP与它们不匹配。没有人会对Apple的公共证书和签名进行验证。

不可否认,我可能会误解一些基本的东西。至少要使Objective C和C#哈希验证是一个巨大的步骤。

感谢。

目标C代码:

[localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) {
    NSDictionary *params = @{@"public_key_url": [publicKeyUrl absoluteString],
                             @"timestamp": [NSString stringWithFormat:@"%llu", timestamp],
                             @"signature": [signature base64EncodedStringWithOptions:0],
                             @"salt": [salt base64EncodedStringWithOptions:0],
                             @"player_id": [GKLocalPlayer localPlayer].playerID,
                             @"app_bundle_id": [[NSBundle mainBundle] bundleIdentifier]};
    //  Build hash using iOS...
    NSMutableData *payload = [[NSMutableData alloc] init];
    [payload appendData:[[GKLocalPlayer localPlayer].playerID dataUsingEncoding:NSASCIIStringEncoding]];
    [payload appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSASCIIStringEncoding]];
    uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
    [payload appendBytes:&timestampBE length:sizeof(timestampBE)];
    [payload appendData:salt];
    uint8_t sha1HashDigest[CC_SHA1_DIGEST_LENGTH];
    CC_SHA1([payload bytes], [payload length], sha1HashDigest);
    //  Convert to hex string so it can be sent to Unity's C# then to the PHP webserver...
    NSString *sIOSHash = [self stringFromDigest:sha1HashDigest length:CC_SHA1_DIGEST_LENGTH];
    //  END - Build hash using iOS

    //  Build string to send to Unity's C#...
    NSMutableString * data = [[NSMutableString alloc] init];
    [data appendString:params[@"public_key_url"]];
    [data appendString:@","];
    [data appendString:params[@"timestamp"]];
    [data appendString:@","];
    [data appendString:params[@"signature"]];
    [data appendString:@","];
    [data appendString:params[@"salt"]];
    [data appendString:@","];
    [data appendString:params[@"player_id"]];
    [data appendString:@","];
    [data appendString:params[@"app_bundle_id"]];
    [data appendString:@","];
    [data appendString:sIOSHash];
    //  END - Build string to send to Unity's C#.

    //  Send string to Unity's C# for parsing and sending off to PHP webserver.
    NSString *str = [[data copy] autorelease];
    UnitySendMessage("GameCenterManager", "onAuthenticateLocalPlayer", [ISNDataConvertor NSStringToChar:str]);
}];
//  Helper method to convert uint8_t into a hex string for sending to the webserver.
- (NSString *)stringFromDigest:(uint8_t *)digest length:(int)length {
    NSMutableString *ms = [[NSMutableString alloc] initWithCapacity:length * 2];
    for (int i = 0; i < length; i++) {
        [ms appendFormat: @"%02x", (int)digest[i]];
    }
    return [ms copy];
}

以下是生成第二版SHA1哈希的C#代码(在Unity3D中)。 这些变量都是从iOS代码发送到Unity的(上图),并以字符串形式出现:player_idapp_bundle_idtimestampsalt。 (我没有显示任何Unity3D C#代码发送到我的服务器。但我正在使用WWWFormAddField发送它。我也没有显示“桥接”代码来移动数据目标C到C#。)

var sha1 = new SHA1Managed();
var data = new List<byte>();
data.AddRange(Encoding.UTF8.GetBytes(player_id));
data.AddRange(Encoding.UTF8.GetBytes(app_bundle_id));
data.AddRange(ToBigEndian(Convert.ToUInt64(timestamp)));
data.AddRange(Convert.FromBase64String(salt));
var sig = data.ToArray();
public static string CSharpHash = ToHex(sha1.ComputeHash(sig), false);

这最后一个代码块是我的服务器端PHP,它从客户端接收数据,验证公共证书,然后尝试验证对它的哈希和签名。最后一部分是我被困住的地方。

/*
Sample data as received within the PHP (all strings):
$public_cert_url    eg: https://sandbox.gc.apple.com/public-key/gc-sb.cer
$timestamp          eg: 00-00-01-47-12-9C-16-D4             [derived from: 1404766525140]
$signature          eg: EGc8J9D7SdZ0qq2xl2XLz2[lots more...]
$salt               eg: LDfyIQ==
$player_id          eg: G:[#########]
$app_bundle_id      eg: com.[mydomain].[myapp]

$sIOSHash           eg: 00032b9416315c8298b5a6e7f5d9dec71bd5ace2        [The C# and Objective C code both generate the same hash.]
$CSharpHash     eg: 00032b9416315c8298b5a6e7f5d9dec71bd5ace2
*/


//  Verify the public cert.
//  As far as I understand, PHP's openssl_pkey_get_public() cannot read raw 
//      cer data, so I download and convert to PEM. Optimize later.
$fp = fopen("temp.cer", "w");       //  Open file for writing.
$header[] = "Content-Type: application/pkix-cert";
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, $public_cert_url);
curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_exec($curl);
curl_close($curl);
fclose($fp);
shell_exec("openssl x509 -inform der -in temp.cer -out temp.pem");  //  Convert to PEM.
$pub_cert = file_get_contents("temp.pem");
$sKey = openssl_pkey_get_public($pub_cert);     //  Validate PEM file here.
If( $sKey === False ) echo "pkey bad";
//  This ^^ works.


//  This is where I am stuck:

//  Verify the data from the client against the signature from the client 
//      and the downloaded public key.
//  First, try to verify against a hash created within PHP:
$iResult = openssl_verify(
        sha1($player_id . $app_bundle_id . $timestamp . $salt), 
        $signature, 
        $pub_cert, 
        OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid PHP hash!\n";

//  Second, see if it will verify by using the hash created in.
$iResult = openssl_verify($sIOSHash, $signature, $pub_cert, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid sIOSHash hash!\n";

//  Finally, does the C# has verify?
$iResult = openssl_verify($CSharpHash, $signature, $pub_cert, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) echo "not valid CSharpHash hash!\n";

//  None of these ^^ ever validate.   

更新:2014年7月9日
我通过不对其进行SHA1来验证数据。我对Apple文档(https://developer.apple.com/library/prerelease/ios/documentation/GameKit/Reference/GKLocalPlayer_Ref/index.html#//apple_ref/occ/instm/GKLocalPlayer/generateIdentityVerificationSignatureWithCompletionHandler :)感到困惑。特别是#7,它说:“为缓冲区生成一个SHA-1哈希值。”

我删除了所有C#代码(尝试生成有效负载),现在只使用Objective C.

修改如下:

NSMutableData *payload = [[NSMutableData alloc] init];
[payload appendData:[[GKLocalPlayer localPlayer].playerID dataUsingEncoding:NSUTF8StringEncoding]];
[payload appendData:[[[NSBundle mainBundle] bundleIdentifier] dataUsingEncoding:NSUTF8StringEncoding]];
uint64_t timestampBE = CFSwapInt64HostToBig(timestamp);
[payload appendBytes:&timestampBE length:sizeof(timestampBE)];
[payload appendData:salt];
NSString *siOSData = [payload base64EncodedStringWithOptions:0];

注意删除SHA1。

我放弃了尝试在PHP中创建有效负载。我尝试了很多变化的包,转换,将我的服务器升级到64位等等。但我认为(请纠正我,如果我错了)因为我从客户端传输完全相同的数据作为有效载荷,它应该好的

Apple的注意事项:
请实现OAuth 2.0。

我还想出了如何验证Apple cer文件而不浪费保存到文件的处理。如下:

// Get data from client. I urlencoded it before sending. So need to urldecode now.
// The payload is in "iosdata" and it, along with the signature, both need to be
//  base64_decoded.
$sIOSData       = ( isset($_REQUEST["iosdata"]) ) ? urldecode(Trim($_REQUEST["iosdata"])) : "";
$sIOSData       = base64_decode($sIOSData);
$sSignature = ( isset($_REQUEST["signature"]) ) ? urldecode(Trim($_REQUEST["signature"])) : "";
$sSignature = base64_decode($sSignature);

// Here is where I download Apple's cert (DER format), save it as raw bits  
//  to a variable, convert it to PEM format (the ONLY format PHP's OpenSSL 
//  works with apparently...?) and then validate it.
// TODO: figure out if Apple live returns different results each time, and/or if
//  this can be cached. Apple sandbox returns the same each time.
$header[0] = "Content-Type: application/pkix-cert";
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, $sPublicKeyUrl);
curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
$der_data = curl_exec($curl);
curl_close($curl);
$sPublicKey = chunk_split(base64_encode($der_data), 64, "\n");
$sPublicKey = "-----BEGIN CERTIFICATE-----\n".$sPublicKey."-----END CERTIFICATE-----\n";
$sKey = openssl_pkey_get_public($sPublicKey);
If( $sKey === False ) Return "pkey bad";

// Here I use the package ($sIOSData) and signature to validate against Apple's
//  public certificate.
$iResult = openssl_verify($sIOSData, $sSignature, $sKey, OPENSSL_ALGO_SHA1);
If( $iResult != 1 ) {
    echo "BAD!\n";
    echo "error: ".openssl_error_string()."\n";
}else{
    echo "WORKED!\n";
}

欢迎提供反馈。我相信有很多东西可以改进。但希望这有助于拯救某人一周的工作。

2 个答案:

答案 0 :(得分:3)

我有一段时间了。 Garraeth的代码很有帮助,但是有一些其他有用的提示散落在SO,加上php文档,加上一些幸运的猜测,我终于到了这个:

在iOS方面:

主验证用户代码:

// Don't bother verifying not-authenticated players
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
if (localPlayer.authenticated)
{
    // __weak copy for use within code-block
    __weak GKLocalPlayer *useLocalPlayer = localPlayer;
    [useLocalPlayer generateIdentityVerificationSignatureWithCompletionHandler: ^(NSURL * _Nullable publicKeyUrl,
                                                                                  NSData * _Nullable signature,
                                                                                  NSData * _Nullable salt,
                                                                                  uint64_t timestamp,
                                                                                  NSError * _Nullable error) {

        if (error == nil)
        {
            [self verifyPlayer: useLocalPlayer.playerID // our verify routine: below
                  publicKeyUrl: publicKeyUrl
                     signature: signature
                          salt: salt
                     timestamp: timestamp];
        }
        else
        {
            // GameCenter returned an error; deal with it here.
        }
    }];
}
else
{
    // User is not authenticated; it makes no sense to try to verify them.
}

我的verifyPlayer:例程:

-(void)verifyPlayer: (NSString*) playerID
       publicKeyUrl: (NSURL*) publicKeyUrl
          signature: (NSData*) signature
               salt: (NSData*) salt
          timestamp: (uint64_t) timestamp
{
    NSDictionary *paramsDict = @{ @"publicKeyUrl": [publicKeyUrl absoluteString],
                                  @"timestamp"   : [NSString stringWithFormat: @"%llu", timestamp],
                                  @"signature"   : [signature base64EncodedStringWithOptions: 0],
                                  @"salt"        : [salt base64EncodedStringWithOptions: 0],
                                  @"playerID"    : playerID,
                                  @"bundleID"    : [[NSBundle mainBundle] bundleIdentifier]
                                  };

    // NOTE: A lot of the code below was cribbed from another SO answer for which I have lost the URL.
    // FIXME: <When found, insert other-SO-answer URL here>

    // build payload
    NSMutableData *payload = [NSMutableData new];
    [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];

    // Verify with server
    [self verifyPlayerOnServer: payload withSignature: signature publicKeyURL: publicKeyUrl];

#if 0   // verify locally (for testing)

    //get certificate
    NSData *certificateData = [NSData dataWithContentsOfURL: publicKeyUrl];

    //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(@"%s ***** Could not create trust certificate", __PRETTY_FUNCTION__);
        return;
    }

    SecTrustResultType resultType;
    OSStatus statusTrustEval =  SecTrustEvaluate(trust, &resultType);
    if (statusTrustEval != errSecSuccess)
    {
        NSLog(@"%s ***** Could not evaluate trust", __PRETTY_FUNCTION__);
        return;
    }

    if ((resultType != kSecTrustResultProceed)
    &&  (resultType != kSecTrustResultRecoverableTrustFailure) )
    {
        NSLog(@"%s ***** Server can not be trusted", __PRETTY_FUNCTION__);
        return;
    }

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

    NSLog(@"%s [DEBUG] sha256HashDigest: %@", __PRETTY_FUNCTION__, [NSData dataWithBytes: sha256HashDigest length: CC_SHA256_DIGEST_LENGTH]);

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

    CFRelease(publicKey);
    CFRelease(trust);
    CFRelease(secPolicy);
    CFRelease(certificateFromFile);
    if (verficationResult == errSecSuccess)
    {
        NSLog(@"%s [DEBUG] Verified", __PRETTY_FUNCTION__);

        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateGameCenterUI];
        });
    }
    else
    {
        NSLog(@"%s ***** Danger!!!", __PRETTY_FUNCTION__);
    }

#endif
}

我将代码传递给服务器的例程(来自这个问题的Cribbed):

- (void) verifyPlayerOnServer: (NSData*) payload withSignature: signature publicKeyURL: (NSURL*) publicKeyUrl
{
    // hint courtesy of: http://stackoverflow.com/questions/24621839/how-to-authenticate-the-gklocalplayer-on-my-third-party-server-using-php
    NSDictionary *jsonDict = @{ @"data" : [payload base64EncodedStringWithOptions: 0] };

    //NSLog(@"%s [DEBUG] jsonDict: %@", __PRETTY_FUNCTION__, jsonDict);

    NSError *error = nil;
    NSData *bodyData = [NSJSONSerialization dataWithJSONObject: jsonDict options: 0 error: &error];

    if (error != nil)
    {
        NSLog(@"%s ***** dataWithJson error: %@", __PRETTY_FUNCTION__, error);
    }

    // To validate at server end:
    //  http://stackoverflow.com/questions/21570700/how-to-authenticate-game-center-user-from-3rd-party-node-js-server

    // NOTE: MFURLConnection is my subclass of NSURLConnection.
    // .. this routine just builds an NSMutableURLRequest, then
    // .. kicks it off, tracking a tag and calling back to delegate
    // .. when the request is complete.
    [MFURLConnection connectionWitURL: [self serverURLWithSuffix: @"gameCenter.php"]
                              headers: @{ @"Content-Type"   : @"application/json",
                                          @"Publickeyurl"   : [publicKeyUrl absoluteString],
                                          @"Signature"      : [signature base64EncodedStringWithOptions: 0],
                                          }
                             bodyData: bodyData
                             delegate: self
                                  tag: worfc2_gameCenterVerifyConnection
                             userInfo: nil];
}

在服务器端:

从这个问题,以及其他人,以及php文档和... ...

    $publicKeyURL = filter_var($headers['Publickeyurl'], FILTER_SANITIZE_URL);
    $pkURL = urlencode($publicKeyURL);
    if (empty($pkURL))
    {
        $response->addparameters(array('msg' => "no pku"));
        $response->addparameters(array("DEBUG-headers" => $headers));
        $response->addparameters(array('DEBUG-publicKeyURL' => $publicKeyURL));
        $response->addparameters(array('DEBUG-pkURL' => $pkURL));
        $response->setStatusCode(400);              // bad request
    }
    else
    {
        $sslCertificate = file_get_contents($publicKeyURL);
        if ($sslCertificate === false)
        {
            // invalid read
            $response->addparameters(array('msg' => "no certificate"));
            $response->setStatusCode(400);                  // bad request
        }
        else
        {
            // Example code from http://php.net/manual/en/function.openssl-verify.php
            try
            {
                // According to: http://stackoverflow.com/questions/10944071/parsing-x509-certificate
                $pemData = der2pem($sslCertificate);

                // fetch public key from certificate and ready it                    
                $pubkeyid = openssl_pkey_get_public($pemData);
                if ($pubkeyid === false)
                {
                    $response->addparameters(array('msg' => "public key error"));
                    $response->setStatusCode(400);              // bad request
                }
                else
                {
                    // According to: http://stackoverflow.com/questions/24621839/how-to-authenticate-the-gklocalplayer-on-my-third-party-server-using-php
                    // .. we use differently-formatted parameters
                    $sIOSData   = $body['data'];
                    $sIOSData   = base64_decode($sIOSData);
                    $sSignature = $headers['Signature'];
                    $sSignature = base64_decode($sSignature);

                    //$iResult = openssl_verify($sIOSData, $sSignature, $sKey, OPENSSL_ALGO_SHA1);

                    $dataToUse = $sIOSData;
                    $signatureToUse = $sSignature;

                    // state whether signature is okay or not
                    $ok = openssl_verify($dataToUse, $signatureToUse, $pubkeyid, OPENSSL_ALGO_SHA256);
                    if ($ok == 1)
                    {
                        //* echo "good";
                        $response->addparameters(array('msg' => "user validated"));
                    }
                    elseif ($ok == 0)
                    {
                        //* echo "bad";
                        $response->addparameters(array('msg' => "INVALID USER SIGNATURE"));
                        $response->addparameters(array("DEBUG-$dataToUse" => $dataToUse));
                        $response->addparameters(array("DEBUG-$signatureToUse" => $signatureToUse));
                        $response->addparameters(array("DEBUG-body" => $body));
                        $response->setStatusCode(401);                  // unauthorized
                    }
                    else
                    {
                        //* echo "ugly, error checking signature";
                        $response->addparameters(array('msg' => "***** ERROR checking signature"));
                        $response->setStatusCode(500);                  // server error
                    }

                    // free the key from memory
                    openssl_free_key($pubkeyid);
                }
            }
            catch (Exception $ex)
            {
                $response->addparameters(array('msg' => "verification error"));
                $response->addparameters(array("DEBUG-headers" => $headers));
                $response->addparameters(array('DEBUG-Exception' => $ex));
                $response->setStatusCode(400);              // bad request
            }
        }

        // NODE.js code at http://stackoverflow.com/questions/21570700/how-to-authenticate-game-center-user-from-3rd-party-node-js-server
    }

不要忘记方便的实用程序例程:

function der2pem($der_data)
{
   $pem = chunk_split(base64_encode($der_data), 64, "\n");
   $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
   return $pem;
}

使用所有这些,我终于能够得到用户验证&#34;从我的服务器回来。好极了! :)

注意:此方法似乎非常容易被黑客攻击,因为任何人都可以使用自己的证书签署他们想要的任何内容,然后将服务器的数据,签名和URL传递给他们的证书并返回&# 34;那是一个有效的GameCenter登录&#34;回答这个,而这段代码&#34;工作&#34;在它实现GC算法的意义上,算法本身似乎存在缺陷。理想情况下,我们还会检查证书来自可信来源。对于检查它是Apple的游戏中心证书的超级偏执也会很好。

答案 1 :(得分:1)

谢谢你@garraeth,你的代码帮我实现了逻辑。

从C#代码中,连接服务器端的有效负载数据对我来说很好。 使用openssl_verify时,我们不需要自己做哈希。

另外,我认为验证publicKeyUrl是表单HTTPS,并且需要apple.com。

这里有一些伪代码(请注意,Apple已在2015年将算法更改为OPENSSL_ALGO_SHA256。)

// do some urls, input params validate...

// do the signature validate
$payload = concatPayload($playerId, $bundleId, $timestamp, $salt);
$pubkeyId = openssl_pkey_get_public($pem);
$isValid = openssl_verify($payload, base64_decode($signature), 
$pubkeyId, OPENSSL_ALGO_SHA256);

function concatPayload($playerId, $bundleId, $timestamp, $salt) {
    $bytes = array_merge(
            unpack('C*', $playerId),
            unpack('C*', $bundleId),
            int64ToBigEndianArray($timestamp),
            base64ToByteArray($salt)
    );

    $payload = '';
    foreach ($bytes as $byte) {
        $payload .= chr($byte);
    }
    return $payload;
}

function int64ToBigEndianArray() {
    //... follow the C# code
}

function base64ToByteArray() {
    //...
}