Пожалуйста, простите мою неуклюжесть, я новичок в Stackoverflow, C # и Objective C.
В двух словах, я пытаюсь сделать то, на что ответили в этом вопросе, но на PHP: как проверить подлинность GKLocalPlayer на моем «стороннем сервере»? Надеюсь, это также поможет другим PHP-разработчикам работать над одним и тем же.
Я использую Unity (Unity3D) и PHP-сервер. У меня есть Objective C, правильно подключающийся к GameCenter и возвращающий данные по вызову generateIdentityVerificationSignatureWithCompletionHandler
. К сожалению, я не могу понять, что я делаю неправильно, чтобы проверить хэш SHA1. Я работал над этим на прошлой неделе, пробовал всевозможные вещи, но не повезло.
Я пытаюсь три разных способа сделать хэш SHA1 (показано ниже). Один раз в Objective C, другой в C # Unity и, наконец, третий раз на моем сервере в 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:×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. Эти переменные все отправляются в Unity из кода iOS (см. Выше) и player_id
в виде строк: player_id
, app_bundle_id
, timestamp
, salt
. (Я не показываю код Unity3D C # для отправки на мой сервер, но я использую WWWForm
и AddField
для отправки. Я также не показываю код «bridge» для перемещения данных с 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-сервер, который получает данные от клиента, проверяет открытый сертификат и затем пытается проверить хэш против него и подпись. Эта последняя часть – это то место, где я застрял.
/* 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.
Обновление: 9 июля 2014 г.
Я получил его для проверки данных, не делая 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:×tampBE 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"; }
Обратная связь приветствуется. Я уверен, что есть много вещей, которые можно улучшить. Но, надеюсь, это поможет спасти кого-нибудь неделю работы.
У меня было время с этим. Код Гаррата был полезен, но было несколько полезных советов, разбросанных вокруг 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. }
Моя проверкаPlayer: рутина:
-(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 from this question):
- (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 docs и …
$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; }
Используя все это, я, наконец, смог получить «пользователь проверен» с моего сервера. Ура! 🙂
ПРИМЕЧАНИЕ. Этот метод кажется очень открытым для взлома, поскольку каждый может подписывать все, что захочет, со своим собственным сертификатом, а затем передавать серверу данные, подпись и URL-адрес в свой сертификат и возвращать ответ «это правильный ответ в GameCenter», поэтому, код «работает» в том смысле, что он реализует алгоритм GC, сам алгоритм кажется ошибочным. В идеале мы также должны проверить, что сертификат получен из надежного источника. Экстра-паранойя, чтобы проверить, что это сертификат Game Center от Apple, тоже будет хорошим.
Спасибо @garraeth, ваш код помог мне реализовать логику.
Из кода C #, concat данные полезной нагрузки на стороне сервера работают отлично для меня. При использовании openssl_verify нам не нужно делать хэш самостоятельно.
Кроме того, я считаю, что publicKeyUrl является формой HTTPS и требуется apple.com.
Некоторый псевдокод здесь (обратите внимание, что Apple изменит алгоритм на OPENSSL_ALGO_SHA256 в 2015 году).
// 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() { //... }