如何使用PHP在我的“第三方服务器”上validationGKLocalPlayer?
请原谅我的笨拙,我是新来的Stackoverflow,C#和客观C.
简而言之,我正在试图解决这个问题,但在PHP中: 如何在我的“第三方服务器”上validationGKLocalPlayer? 希望这也将有助于其他PHP开发人员在同一件事情上工作。
我正在使用Unity(Unity3D)和PHP服务器端。 我已经有Objective C正确地连接到GameCenter,并通过调用generateIdentityVerificationSignatureWithCompletionHandler
来返回数据。 不幸的是,我不知道我在做什么错误来validationSHA1哈希。 过去一周我一直在努力,尝试各种各样的事情,但没有运气。
我正在尝试三种不同的方法来制作SHA1哈希(如下所示)。 一旦进入Objective C,另一个在Unity的C#中,最后是第三次在我的服务器上。 Objective C和C#SHA1哈希结果完全相同。 但是,PHP不匹配它们。 而且没有任何证据反对苹果公共证书和签名。
无可否认,我可能会误解一些根本性的东西。 至less得到Objective C和C#哈希validation将是一个巨大的步骤。
谢谢。
目标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:×tampBE 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]; }
接下来是C#代码(在Unity3D中)来生成SHA1哈希的第二个版本。 这些variables都从iOS代码(上面)发送到Unity,并以stringforms出现: player_id
, app_bundle_id
, timestamp
, salt
。 (我没有显示任何Unity3D C#代码发送到我的服务器,但我使用WWWForm
和AddField
发送它,也没有显示“桥”代码将数据从Objective 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,它接收来自客户端的数据,validation公共证书,然后尝试validation哈希对它和签名。 最后一部分是我卡住的地方。
/* 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来validation数据。 我很困惑的苹果文档( 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:×tampBE length:sizeof(timestampBE)]; [payload appendData:salt]; NSString *siOSData = [payload base64EncodedStringWithOptions:0];
注意删除SHA1。
我放弃了试图在PHP中创build有效载荷。 我尝试了许多变化的包,转换,升级我的服务器到64位等,但我认为(请纠正我,如果我错了),因为我传输完全相同的数据从客户端组成的有效载荷,它应好的。
给苹果的提示:
请执行OAuth 2.0。
我也想出了如何validation苹果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"; }
欢迎反馈。 我确定有很多东西可以改进。 但希望这将有助于拯救一个星期的工作。
我有一个这样的时间。 Garraeth的代码是有帮助的,但还有其他一些有用的提示,散布在所有的PHP文档,加上一些幸运的猜测,我终于到了这个:
在iOS端:
主要validation用户代码:
// 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: ×tampBE 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; }
使用所有这些,我终于能够从我的服务器得到“用户validation”。 好极了! 🙂
注:这种方法似乎非常开放的黑客攻击,因为任何人都可以用他们自己的证书签署任何他们想要的,然后将数据,签名和URL传递给他们的证书,并得到一个“这是一个有效的GameCenterlogin”的答案,代码“起作用”,因为它实现了GCalgorithm,algorithm本身似乎有缺陷。 理想情况下,我们还会检查证书是否来自可信来源。 额外的偏执狂,以检查它是苹果的游戏中心证书也会很好。
谢谢@garraeth,你的代码帮助我实现了逻辑。
从C#代码,concat在服务器端的有效载荷数据对我来说工作正常。 当使用openssl_verify时,我们不需要自己做哈希。
另外,我认为publicKeyUrlvalidation是HTTPSforms,而apple.com是必需的。
这里有一些伪代码(请注意,苹果在2015年将algorithm更改为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() { //... }