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
I had a heck of a time with this. Garraeth's code was helpful, but there were a few other helpful hints scattered around SO, plus the php docs, plus some lucky guessing, and I finally arrived at this:
On the iOS side:
Main verify-user code:
// 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.
}
My verifyPlayer: routine:
-(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
}
My routine that passes code to the server (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];
}
On the server side:
Somewhat cribbed from this question, and others, and php docs and...
$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
}
Don't forget the handy utility routine:
function der2pem($der_data)
{
$pem = chunk_split(base64_encode($der_data), 64, "\n");
$pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
return $pem;
}
Using all of this, I was finally able to get "user validated" back from my server. Yay! :)
NOTE: This method seems very open to hacking, as anyone could sign whatever they want with their own certificate then pass the server the data, signature and URL to their certificate and get back a "that's a valid GameCenter login" answer so, while this code "works" in the sense that it implements the GC algorithm, the algorithm itself seems flawed. Ideally, we would also check that the certificate came from a trusted source. Extra-paranoia to check that it is Apple's Game Center certificate would be good, too.
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() {
//...
}