请原谅我的笨拙,我是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:×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];
}
以下是生成第二版SHA1哈希的C#代码(在Unity3D中)。
这些变量都是从iOS代码发送到Unity的(上图),并以字符串形式出现:player_id
,app_bundle_id
,timestamp
,salt
。
(我没有显示任何Unity3D C#代码发送到我的服务器。但我正在使用WWWForm
和AddField
发送它。我也没有显示“桥接”代码来移动数据目标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:×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";
}
欢迎提供反馈。我相信有很多东西可以改进。但希望这有助于拯救某人一周的工作。
答案 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: ×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;
}
使用所有这些,我终于能够得到用户验证&#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() {
//...
}