duanjingsen7904 2014-07-08 00:07
浏览 122

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

Please forgive my clumsiness, I'm new to Stackoverflow, C#, and Objective C.

In a nutshell, I'm trying to do what is answered in this question, but in PHP: How to authenticate the GKLocalPlayer on my 'third party server'? Hopefully this will also help other PHP devs working on the same thing.

I'm using Unity (Unity3D) and PHP server-side. I've got Objective C properly connecting to GameCenter and returning data via a call to generateIdentityVerificationSignatureWithCompletionHandler. Unfortunately, I can't figure out what I'm doing wrong to validate the SHA1 hash. I've been working on this for the past week, trying all sorts of things, but with no luck.

I'm trying three different ways of making the SHA1 hash (shown below). Once in Objective C, another in Unity's C#, and finally a third time on my server in PHP. The Objective C and C# SHA1 hashes end up identical. However, the PHP one does not match them. And none validate against Apple's public cert and signature.

Admittedly, I could be misunderstanding something fundamental. It would be a huge step to at least get the Objective C and C# hashes to validate.

Thanks.

Objective C code:

[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];
}

What follows is the C# code (within Unity3D) to generate the second version of the SHA1 hash. These variables are all sent to Unity from the iOS code (above), and came in as strings: player_id, app_bundle_id, timestamp, salt. (I'm not showing any Unity3D C# code to send to my server. But I'm using the WWWForm and AddField to send it. Nor am I showing the "bridge" code to move data from Objective C to 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);

This last code block is my server-side PHP that receives the data from the client, validates the public cert, and then tries to verify the hash against it and the signature. That last part is where I am stuck.

/*
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!
";

//  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!
";

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

//  None of these ^^ ever validate.   

Update: Jul 9 2014
I got it to validate the data by not doing SHA1 on it. I was confused by the Apple documentation (https://developer.apple.com/library/prerelease/ios/documentation/GameKit/Reference/GKLocalPlayer_Ref/index.html#//apple_ref/occ/instm/GKLocalPlayer/generateIdentityVerificationSignatureWithCompletionHandler:). Specifically #7 which says: "Generate a SHA-1 hash value for the buffer."

I removed ALL the C# code (to try and generate the payload) and now only use the Objective C.

Modified as follows:

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];

Notice the removal of SHA1.

I gave up on trying to create the payload in PHP. I tried many variations of pack, conversions, upgrading my server to 64 bit, etc. But I think (please correct me if I am wrong) that since I am transmitting the exact same data from the client as makes up the payload, it should be ok.

Note to Apple:
PLEASE implement OAuth 2.0.

I also figured out how to validate the Apple cer file without wasting processing on saving to a file. As follows:

// 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, "
");
$sPublicKey = "-----BEGIN CERTIFICATE-----
".$sPublicKey."-----END CERTIFICATE-----
";
$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!
";
    echo "error: ".openssl_error_string()."
";
}else{
    echo "WORKED!
";
}

Feedback is welcome. I'm sure there are tons of things that can be improved on. But hopefully this will help save someone a week of work.

  • 写回答

2条回答 默认 最新

  • dougan1205 2015-05-22 11:12
    关注

    Thank you @garraeth, your code helped me implement the logic.

    From the C# code, concat a payload data on server side is working fine for me. When using openssl_verify we needn't do the hash ourselves.

    Also, I think validate the publicKeyUrl is form HTTPS and apple.com is required.

    Some pseudo code here (Note that Apple has change the algorithm to OPENSSL_ALGO_SHA256 in 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() {
        //...
    }
    
    评论

报告相同问题?

悬赏问题

  • ¥15 ROS Turtlebot3 多机协同自主探索环境时遇到的多机任务分配问题,explore节点
  • ¥15 Matlab怎么求解含参的二重积分?
  • ¥15 苹果手机突然连不上wifi了?
  • ¥15 cgictest.cgi文件无法访问
  • ¥20 删除和修改功能无法调用
  • ¥15 kafka topic 所有分副本数修改
  • ¥15 小程序中fit格式等运动数据文件怎样实现可视化?(包含心率信息))
  • ¥15 如何利用mmdetection3d中的get_flops.py文件计算fcos3d方法的flops?
  • ¥40 串口调试助手打开串口后,keil5的代码就停止了
  • ¥15 电脑最近经常蓝屏,求大家看看哪的问题