How to authenticate the GKLocalPlayer on my 'third party server'?
Here is a C# WebApi server side version:
public class GameCenterController : ApiController{ // POST api/gamecenter public HttpResponseMessage Post(GameCenterAuth data) { string token; if (ValidateSignature(data, out token)) { return Request.CreateResponse(HttpStatusCode.OK, token); } return Request.CreateErrorResponse(HttpStatusCode.Forbidden, string.Empty); } private bool ValidateSignature(GameCenterAuth auth, out string token) { try { var cert = GetCertificate(auth.PublicKeyUrl); if (cert.Verify()) { var csp = cert.PublicKey.Key as RSACryptoServiceProvider; if (csp != null) { var sha256 = new SHA256Managed(); var sig = ConcatSignature(auth.PlayerId, auth.BundleId, auth.Timestamp, auth.Salt); var hash = sha256.ComputeHash(sig); if (csp.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA256"), Convert.FromBase64String(auth.Signature))) { // Valid user. // Do server related user management stuff. return true; } } } // Failure token = null; return false; } catch (Exception ex) { // Log the error token = null; return false; } } private static byte[] ToBigEndian(ulong value) { var buffer = new byte[8]; for (int i = 0; i < 8; i++) { buffer[7 - i] = unchecked((byte)(value & 0xff)); value = value >> 8; } return buffer; } private X509Certificate2 GetCertificate(string url) { var client = new WebClient(); var rawData = client.DownloadData(url); return new X509Certificate2(rawData); } private byte[] ConcatSignature(string playerId, string bundleId, ulong timestamp, string salt) { var data = new List<byte>(); data.AddRange(Encoding.UTF8.GetBytes(playerId)); data.AddRange(Encoding.UTF8.GetBytes(bundleId)); data.AddRange(ToBigEndian(timestamp)); data.AddRange(Convert.FromBase64String(salt)); return data.ToArray(); }}public class GameCenterAuth{ public string PlayerId { get; set; } public string BundleId { get; set; } public string Name { get; set; } public string PublicKeyUrl { get; set; } public string Signature { get; set; } public string Salt { get; set; } public ulong Timestamp { get; set; }}
Here is how you can authenticate using objective c. If you need it in another language should be trivial to translate.
-(void)authenticate{ __weak GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; localPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error) { if(viewController) { [[[UIApplication sharedApplication] keyWindow].rootViewController presentViewController:viewController animated:YES completion:nil]; } else if(localPlayer.isAuthenticated == YES) { [localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) { if(error != nil) { return; //some sort of error, can't authenticate right now } [self verifyPlayer:localPlayer.playerID publicKeyUrl:publicKeyUrl signature:signature salt:salt timestamp:timestamp]; }]; } else { NSLog(@"game center disabled"); } };}-(void)verifyPlayer:(NSString *)playerID publicKeyUrl:(NSURL *)publicKeyUrl signature:(NSData *)signature salt:(NSData *)salt timestamp:(uint64_t)timestamp{ //get certificate NSData *certificateData = [NSData dataWithContentsOfURL:publicKeyUrl]; //build payload NSMutableData *payload = [[NSMutableData alloc] init]; [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]; //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(@"could not create trust"); return; } SecTrustResultType resultType; OSStatus statusTrustEval = SecTrustEvaluate(trust, &resultType); if(statusTrustEval != errSecSuccess) { NSLog(@"could not evaluate trust"); return; } if(resultType != kSecTrustResultProceed && resultType != kSecTrustResultRecoverableTrustFailure) { NSLog(@"server can not be trusted"); return; } SecKeyRef publicKey = SecTrustCopyPublicKey(trust); uint8_t sha256HashDigest[CC_SHA256_DIGEST_LENGTH]; CC_SHA256([payload bytes], (CC_LONG)[payload length], sha256HashDigest); //check to see if its a match OSStatus verficationResult = SecKeyRawVerify(publicKey, kSecPaddingPKCS1SHA256, sha256HashDigest, CC_SHA256_DIGEST_LENGTH, (const uint8_t *)[signature bytes], [signature length]); CFRelease(publicKey); CFRelease(trust); CFRelease(secPolicy); CFRelease(certificateFromFile); if (verficationResult == errSecSuccess) { NSLog(@"Verified"); } else { NSLog(@"Danger!!!"); }}
EDIT:
as of March 2nd 2015, apple now uses SHA256 instead of SHA1 on the certificate. https://devforums.apple.com/thread/263789?tstart=0
It took me a lot of time to implement it in PHP. Now I would like to share my result.
Documentation
You can find a very simple documentation at Apple: https://developer.apple.com/library/ios/documentation/GameKit/Reference/GKLocalPlayer_Ref/index.html#//apple_ref/occ/instm/GKLocalPlayer/generateIdentityVerificationSignatureWithCompletionHandler
[...]
- Use the publicKeyURL on the third party server to download the public key.
- Verify with the appropriate signing authority that the public key is signed by Apple.
- Retrieve the player’s playerID and bundleID.
- Concatenate into a data buffer the following information, in the order listed:
- The playerID parameter in UTF-8 format
- The bundleID parameter in UTF-8 format
- The timestamp parameter in Big-Endian UInt-64 format
- The salt parameter
- Generate a SHA-256 hash value for the buffer.
- Using the public key downloaded in step 3, verify that the hash value generated in step 7 matches the signature parameter provided by the API.
Notice! Number 7 is a trap in PHP that cost me hours. You have to pass only the raw concatenated string to the openssl_verify() function.
The update from Jul 9 2014 in the question How to authenticate the GKLocalPlayer on my 'third party server' using PHP? helped me to find the problem.
Final Source
<?php// signature, publicKeyUrl, timestamp and salt are included in the base64/json data you will receive by calling generateIdentityVerificationSignatureWithCompletionHandler.$timestamp = $params["timestamp"]; // e.g. 1447754520194$user_id = $params["user_id"]; // e.g. G:20010412315$bundle_id = "com.example.test";$public_key_url = $params["publicKeyUrl"]; // e.g. https://static.gc.apple.com/public-key/gc-prod-2.cer$salt = base64_decode($params["salt"]); // Binary$signature = base64_decode($params["signature"]); // Binary// Timestamp is unsigned 64-bit integer big endian$highMap = 0xffffffff00000000;$lowMap = 0x00000000ffffffff;$higher = ($timestamp & $highMap) >>32;$lower = $timestamp & $lowMap;$timestamp = pack('NN', $higher, $lower);// Concatenate the string$data = $user_id . $bundle_id . $timestamp . $salt;// ATTENTION!!! Do not hash it! $data = hash("sha256", $packed);// Fetch the certificate. This is dirty because it is neither cached nor verified that the url belongs to Apple.$ssl_certificate = file_get_contents($public_key_url);$pem = chunk_split(base64_encode($ssl_certificate), 64, "\n");$pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";// it is also possible to pass the $pem string directly to openssl_verifyif (($pubkey_id = openssl_pkey_get_public($pem)) === false) { echo "invalid public key\n"; exit;}// Verify that the signature is correct for $data$verify_result = openssl_verify($data, $signature, $pubkey_id, OPENSSL_ALGO_SHA256);openssl_free_key($pubkey_id);switch($verify_result) { case 1: echo "Signature is ok.\n"; break; case 0: echo "Signature is wrong.\n"; break; default: echo "An error occurred.\n"; break;}