How to authenticate the GKLocalPlayer on my 'third party server'?
Asked Answered
F

10

21

iOS7 introduced new GKLocalPlayer method generateIdentityVerificationSignatureWithCompletionHandler().

Does anyone know how to use it for good? I assume there will be some public API at Apple server-side..

Fatherless answered 1/7, 2013 at 15:56 Comment(5)
Parallel discussion @DevForums.Fatherless
KiryIP, were you able to make this work?Hamitic
Yes. Look for my own answer ('Python' version).Fatherless
Just add one reference of PHP implementation here: #24622339Norri
As of 2021 this method is deprecated. Use the fetchItems function instead. See developer.apple.com/documentation/gamekit/gklocalplayer/…Expedite
B
9

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:&timestampBE 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

Bubb answered 5/10, 2013 at 21:5 Comment(2)
As I understand the logic of such verification function 'verifyPlayer' should be implemented at my server-side. Am I right? So at actual game verifyPlayer() callback will transmit data to the server, which one will check the signature using OpenSSL or other library because there is no iOS APIs.Fatherless
Correct you should implement this verifyPlayer server side. I just showed how it works using iOS code, but your server needs to do the same sort of steps outlined above to correctly verify.Bubb
E
13

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; }
}
Exhilarative answered 2/1, 2014 at 19:4 Comment(6)
While this is a nice bit of code, I'm not sure it answers the user's question. Can you explain on how you are using the generateIdentityVerificationSignatureWithCompletionHandler function? And what it is to be used for?Nicolina
I wasn't answering his question so much as I was providing a C# implementation to the accepted answer for other C# devs who come across this as I did.Exhilarative
Works great for us. We built this into an Azure Function so we can verify our players.Disremember
Hmmm.. How you guys make this work. In net4.6 I able to download cert but got cert.Verify() failed. Did apple have any changesReproduction
anyone know how to do this in .net Core 2.2Broomrape
since the certificate won't have a private key the variable csp will always be nullFelipafelipe
B
9

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:&timestampBE 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

Bubb answered 5/10, 2013 at 21:5 Comment(2)
As I understand the logic of such verification function 'verifyPlayer' should be implemented at my server-side. Am I right? So at actual game verifyPlayer() callback will transmit data to the server, which one will check the signature using OpenSSL or other library because there is no iOS APIs.Fatherless
Correct you should implement this verifyPlayer server side. I just showed how it works using iOS code, but your server needs to do the same sort of steps outlined above to correctly verify.Bubb
R
5

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

[...]

  1. Use the publicKeyURL on the third party server to download the public key.
  2. Verify with the appropriate signing authority that the public key is signed by Apple.
  3. Retrieve the player’s playerID and bundleID.
  4. 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
  5. Generate a SHA-256 hash value for the buffer.
  6. 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_verify
if (($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;
}
Rosamondrosamund answered 17/11, 2015 at 15:26 Comment(0)
F
3

Thanks, @odyth. Thanks, @Lionel. I want to add Python version (based on yours) here. It has minor flaw - Apple certificate is not verified - there is no such API at pyOpenSSL binding.

import urllib2
import OpenSSL
import struct

def authenticate_game_center_user(gc_public_key_url, app_bundle_id, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature):
    apple_cert = urllib2.urlopen(gc_public_key_url).read()
    gc_pkey_certificate = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, apple_cert)

    payload = gc_player_id.encode('UTF-8') + app_bundle_id.encode('UTF-8') + struct.pack('>Q', int(gc_timestamp)) + gc_salt

    try:
        OpenSSL.crypto.verify(gc_pkey_certificate, gc_unverified_signature, payload, 'sha1')
        print 'Signature verification is done. Success!'
    except Exception as res:
        print res

public_key_url = 'https://sandbox.gc.apple.com/public-key/gc-sb.cer'
player_GC_ID = 'G:1870391344'
timestamp = '1382621610281'
your_app_bundle_id = 'com.myapp.bundle_id'

with open('./salt.dat', 'rb') as f_salt:
    with open('./signature.dat', 'rb') as f_sign:
        authenticate_game_center_user(public_key_url, your_app_bundle_id, player_GC_ID, timestamp, f_salt.read(), f_sign.read())
Fatherless answered 6/12, 2013 at 10:20 Comment(6)
@KiryIP, can you post the client code you used to build the post params on the client? I'm using AFNetworking to make the POST to my server, but I can't figure out how to massage the signature and salt into the correct format for my server to pass along to OpenSSL for verification.Calica
Hi, unfortunately I can't help you right now because our client-server communication is based on custom binary protocol over TCP sockets. But I think 'base64' is what you have to get a closer look at.Fatherless
@Calica on the client side i am doing [salt base64Encoding] and on the server side I am doing salt = base64.b64decode(salt). But it's not working. Have you guys figured it out?Equisetum
@JoãoAbrantes I added a snippet to my answer on a similar question. Hope that helps. #15755989Calica
What are the security implications of not verifying the Apple certificate before using? I'm wondering if it would be enough to check that the domain of the public key url is coming from Apple (sandbox.gc.apple.com or production server)Indocile
The Apple certificate has changed from SHA1 to SHA256 (at least in sandbox mode as at 2015-04-13). You will get an error 'algorithm mismatch' with this code. Replace 'sha1' with 'sha256' to fix.Phyllys
I
3

Adding an answer for Python, but using PyCrypto 2.6 (Which is the Google App Engine solution). Also note that verification of the public certificate after downloading is not done here, similar to the python answer above using OpenSSL. Is this step really necessary anyway? If we check that the public key URL is going to an apple domain and it's using ssl (https), doesn't that mean that it's protected from man-in-the-middle attacks?

Anyway, here is the code. Note the binary text is reconverted to binary before concatenation and use. Also I had to update my local python installation to use PyCrypto 2.6 before this would work:

from Crypto.PublicKey import RSA 
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
from base64 import b64decode 
from Crypto.Util.asn1 import DerSequence
from binascii import a2b_base64
import struct
import urlparse

def authenticate_game_center_user(gc_public_key_url, app_bundle_id, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature):

    apple_cert = urllib2.urlopen(gc_public_key_url).read()

    #Verify the url is https and is pointing to an apple domain.
    parts = urlparse.urlparse(gc_public_key_url)
    domainName = ".apple.com"
    domainLocation = len(parts[1]) - len(domainName)
    actualLocation = parts[1].find(domainName)
    if parts[0] != "https" or domainName not in parts[1] or domainLocation != actualLocation:
        logging.warning("Public Key Url is invalid.")
        raise Exception

    cert = DerSequence()
    cert.decode(apple_cert)
    tbsCertificate = DerSequence()
    tbsCertificate.decode(cert[0])
    subjectPublicKeyInfo = tbsCertificate[6]

    rsakey = RSA.importKey(subjectPublicKeyInfo) 
    verifier = PKCS1_v1_5.new(rsakey) 

    payload = gc_player_id.encode('UTF-8') 
    payload = payload + app_bundle_id.encode('UTF-8')
    payload = payload + struct.pack('>Q', int(gc_timestamp))
    payload = payload + b64decode(gc_salt)
    digest = SHA.new(payload)

    if verifier.verify(digest, b64decode(gc_unverified_signature)):
        print "The signature is authentic."
    else:
        print "The signature is not authentic."
Indocile answered 7/1, 2015 at 20:0 Comment(6)
And from what side does our server get that URL? Are you sure we can trust it?Fatherless
This code makes 2 assumptions - the client is connecting to the server via https, and that the server is verifying that the domain in the URL is in fact an apple domain (sandbox.gc.apple.com or corresponding production server). AFAIK there is no way to validate the authenticity of a certificate directly using Google App Engine.Indocile
by the way, your url verification code does not protect against badapple.comTondatone
@zaphod - Thanks for the check. I prefixed the domain with a period, and I think this should do the trick. Please correct me if I'm wrong.Indocile
now you'll fail to validate when the domain is exactly "apple.com". here's a more generic function that i think can be used pastebin.com/EbgTYinSTondatone
The Apple certificate has changed from SHA1 to SHA256 (at least in sandbox mode as at 2015-04-13). verify() will return False with this code. Replace SHA.new() with SHA256.new() to fix (and of course, from Crypto.Hash import SHA256).Phyllys
D
2
require 'base64'
require 'httparty'

module GameCenter
  include HTTParty

  # HHTTParty settings
  HTTPARTY_TIMEOUT = 10

  # gc_salt and gc_unverified_signature must first be decoded with Base64.decode64(....) before this example code will work.
  def authenticate_game_center_user(gc_public_key_url, gc_player_id, gc_timestamp, gc_salt, gc_unverified_signature)
    # Get game center public key certificate
    gc_pkey_certificate = get_gc_public_key_certificate(gc_public_key_url)

    # Check public key certificate
    unless public_key_certificate_is_valid?(gc_pkey_certificate) do
        # Handle error
    end
    # Check SSL errors
    unless OpenSSL.errors.empty? do
        # Handle OpenSSL errors
    end
    # Payload building
    payload = build_payload(gc_player_id, gc_timestamp, gc_salt)

    # Test signature
    unless signature_is_valid?(gc_pkey_certificate, gc_unverified_signature, payload) do
        # Handle error
    end
    # Check SSL errors
    unless OpenSSL.errors.empty? do
        # Handle OpenSSL errors
    end

    # Return player ID
    gc_player_id
  end

  def build_payload(player_id, timestamp, salt)
    player_id.encode("UTF-8") + "com.myapp.bundle_id".encode("UTF-8") + [timestamp.to_i].pack("Q>") + salt
  end

  private

  def get_gc_public_key_certificate(url)
    cert = HTTParty.get(url, timeout: HTTPARTY_TIMEOUT, debug_output: Rails.env.production?)
    OpenSSL::X509::Certificate.new(cert)
  rescue SocketError => e
    puts "Key error: " + e.inspect.to_s
  end

  def get_ca_certificate
    OpenSSL::X509::Certificate.new(File.read('./certs/apple/verisign_class_3_code_signing_2010_ca.cer'))
  end

  def public_key_certificate_is_valid?(pkey_cert)
    pkey_cert.verify(get_ca_certificate.public_key)
  end

  def signature_is_valid?(pkey_cert, signature, payload)
    pkey_cert.public_key.verify(OpenSSL::Digest::SHA256.new, signature, payload)
  end

end

Here's my ruby implementation (as module). Thanks to your Objective-C, it has been a lot easier.

Please note, I had been forced to download the CA certificate on a third party ssl service website, because the public key certificate hasn't been signed Apple and Apple doesn't provide any CA certificate to validate sandbox game center certificate so far.

I haven't tested this in production but it works fine in sandbox mode.

Draghound answered 19/11, 2013 at 15:24 Comment(0)
C
2

Thanks for the code samples, here comes golang solution:

func DownloadCert(url string) []byte {
    b, err := inet.HTTPGet(url)
    if err != nil {
        log.Printf("http request error %s", err)
        return nil
    }
    return b
}


func VerifySig(sSig, sGcId, sBundleId, sSalt, sTimeStamp string, cert []byte) (err error) {
    sig, err := base64.StdEncoding.DecodeString(sSig)
    if err != nil {
        return
    }
    salt, err := base64.StdEncoding.DecodeString(sSalt)
    if err != nil {
        return
    }
    timeStamp, err := strconv.ParseUint(sTimeStamp, 10, 64)
    if err != nil {
        return
    }

    payload := new(bytes.Buffer)
    payload.WriteString(sGcId)
    payload.WriteString(sBundleId)
    binary.Write(payload, binary.BigEndian, timeStamp)
    payload.Write(salt)

    return verifyRsa(cert, sig, payload.Bytes())
}

func verifyRsa(key, sig, content []byte) error {
    cert, err := x509.ParseCertificate(key)
    if err != nil {
        log.Printf("parse cert error %s", err)
        return err
    }
    pub := cert.PublicKey.(*rsa.PublicKey)

    h := sha256.New()
    h.Write(content)
    digest := h.Sum(nil)

    err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest, sig)
    return err
}

a little http helper

func HTTPGet(fullUrl string) (content []byte, err error) {
    log.Printf("http get url %s", fullUrl)
    resp, err := http.Get(fullUrl)
    if err != nil {
        log.Printf("url can not be reached %s,%s", fullUrl, err)
        return
    }

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New("ERROR_STATUS_NOT_OK")
    }
    body := resp.Body
    content, err = ioutil.ReadAll(body)
    if err != nil {
        log.Printf("url read error %s, %s", fullUrl, err)
        return
    }
    body.Close()
    return
}

test code

func TestVerifyFull(t *testing.T) {
    cert := DownloadCert("https://sandbox.gc.apple.com/public-key/gc-sb-2.cer")
    if cert == nil {
        log.Printf("cert download error ")
    }
    sig := "sig as base64"
    salt := "salt as base64"
    timeStamp := "1442816155502"
    gcId := "G:12345678"
    bId := "com.xxxx.xxxx"
    err := VerifySig(sig, gcId, bId, salt, timeStamp, cert)
    log.Printf("result %v", err)
}

a little function to validate the cert download url. Prevent to download from any where anything

func IsValidCertUrl(fullUrl string) bool {
    //https://sandbox.gc.apple.com/public-key/gc-sb-2.cer
    uri, err := url.Parse(fullUrl)
    if err != nil {
        log.Printf("not a valid url %s", fullUrl)
        return false
    }

    if !strings.HasSuffix(uri.Host, "apple.com") {
        log.Printf("not a valid host %s", fullUrl)
        return false
    }

    if path.Ext(fullUrl) != ".cer" {
        log.Printf("not a valid ext %s, %s", fullUrl, path.Ext(fullUrl))
        return false
    }
    return true
}
Critta answered 21/9, 2015 at 10:8 Comment(0)
B
0

Thanks to those who provided solutions in other languages.

Here are the relevant bits of solution in Scala (trivial to convert to Java):

private def verify(
  signatureAlgorithm: String, 
  publicKey: PublicKey, 
  message: Array[Byte], 
  signature: Array[Byte]): Boolean = {

  val sha1Signature = Signature.getInstance(signatureAlgorithm)
  sha1Signature.initVerify(publicKey)
  sha1Signature.update(message)
  sha1Signature.verify(signature)
}

val x509Cert = Try(certificateFactory.generateCertificate(new ByteArrayInputStream(publicKeyBytes)).asInstanceOf[X509Certificate])
x509Cert.foreach { cert =>
  signatureAlgorithm = Some(cert.getSigAlgName)
}
x509Cert.map(_.getPublicKey) match {
  case Success(pk) =>
    log.debug("downloaded public key successfully")
    publicKey = Some(pk)
}

val buffer = 
  r.id.getBytes("UTF-8") ++ 
  bundleId.getBytes("UTF-8") ++
  ByteBuffer.allocate(8).putLong(r.timestamp).array() ++ 
  Base64.decode(r.salt)

val result = verify(signatureAlgorithm.getOrElse("SHA256withRSA"), pk, buffer, Base64.decode(r.signature))

log.info("verification result {} for request {}", result, r)

where r is an instance of:

case class IOSIdentityVerificationRequest(
  id: PlayerIdentity, // String
  publicKeyURL: String, 
  signature: String, // base64 encoded bytes
  salt: String, // base64 encoded bytes
  timestamp: Long, 
  error: Option[String]) extends IdentityVerificationRequest
Bouffe answered 14/3, 2015 at 16:42 Comment(0)
P
0

Here is an updated and improved Ruby version. I have tested it with the Apple sandbox, but not with production yet. I have also documented where to get the CA certificate in order to verify the certificate you receive from the public key URL.

# iOS Game Center verifier for 3rd party game servers written in Ruby.
#
# *** Credits ***
#   Based off of code and comments at https://mcmap.net/q/121659/-how-to-authenticate-the-gklocalplayer-on-my-39-third-party-server-39
#
# *** Improvements ***
#   This version uses Ruby's built in HTTP client instead of a 3rd party gem.
#   It's updated to use SHA256 instead of SHA1.
#   It Base64 decodes the salt and signature.  If your client or server already does this then you will need to remove the calls to Base64.decode64().
#   It validates that the public key URL is from apple.com.
#   It has been tested with Apple's Game Center's sandbox public key URL (https://sandbox.gc.apple.com/public-key/gc-sb-2.cer) and works as of June 24th, 2015.
#
# *** Notes on public key certificate validation ***
#   You will need the correct code signing CA to verify the certificate returned from the pubic key URL.
#   You can download/verify the CA certificate here: https://knowledge.symantec.com/support/code-signing-support/index?page=content&actp=CROSSLINK&id=AR2170
#   I have embedded the CA certificate for convenience so that you don't need to save it to your filesystem.
#   When the public key URL changes in the future, you may need to update the text in the ca_certificate_text() method.
#
# *** Usage ***
#   verified, reason = GameCenterVerifier.verify(...)

class GameCenterVerifier
  # Verify that user provided Game Center data is valid.
  # False will be returned along with a reason if any validations fail.
  # Otherwise, it will return true and a nil reason if all validations pass.
  def self.verify(game_center_id, public_key_url, timestamp, salt, signature, bundle_id)
    salt = Base64.decode64(salt)
    signature = Base64.decode64(signature)
    payload = game_center_id.encode('UTF-8') + bundle_id.encode('UTF-8') + [timestamp.to_i].pack('Q>') + salt
    pkey_certificate = get_public_key_certificate(public_key_url)
    return false, 'Invalid public key url' unless public_key_url_is_valid?(public_key_url)
    return false, 'Invalid public key certificate' unless public_key_certificate_is_valid?(pkey_certificate)
    return false, 'OpenSSL errors (before signature check)' unless OpenSSL.errors.empty?
    return false, 'Invalid signature' unless signature_is_valid?(pkey_certificate, signature, payload)
    return false, 'OpenSSL errors (after signature check)' unless OpenSSL.errors.empty?
    return true, nil
  end

  private

  def self.get_public_key_certificate(url)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    request = Net::HTTP::Get.new(uri.request_uri)
    http.use_ssl = true
    http.open_timeout = 5
    http.read_timeout = 5
    cert = http.request(request).body
    OpenSSL::X509::Certificate.new(cert)
  end

  def self.public_key_url_is_valid?(public_key_url)
    uri = URI(public_key_url)
    tokens = uri.host.split('.')
    return false if uri.scheme != 'https'
    return false if tokens[-1] != 'com' || tokens[-2] != 'apple'
    true
  end

  def self.public_key_certificate_is_valid?(pkey_cert)
    pkey_cert.verify(get_ca_certificate.public_key)
  end

  def self.signature_is_valid?(pkey_cert, signature, payload)
    pkey_cert.public_key.verify(OpenSSL::Digest::SHA256.new, signature, payload)
  end

  def self.get_ca_certificate
    OpenSSL::X509::Certificate.new(ca_certificate_text)
  end

  def self.ca_certificate_text
    data = <<EOF
-----BEGIN CERTIFICATE-----
MIIFWTCCBEGgAwIBAgIQPXjX+XZJYLJhffTwHsqGKjANBgkqhkiG9w0BAQsFADCB
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp
U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
aG9yaXR5IC0gRzUwHhcNMTMxMjEwMDAwMDAwWhcNMjMxMjA5MjM1OTU5WjB/MQsw
CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV
BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVjIENs
YXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJeDHgAWryyx0gjE12iTUWAecfbiR7TbWE0jYmq0v1obUfej
DRh3aLvYNqsvIVDanvPnXydOC8KXyAlwk6naXA1OpA2RoLTsFM6RclQuzqPbROlS
Gz9BPMpK5KrA6DmrU8wh0MzPf5vmwsxYaoIV7j02zxzFlwckjvF7vjEtPW7ctZlC
n0thlV8ccO4XfduL5WGJeMdoG68ReBqYrsRVR1PZszLWoQ5GQMWXkorRU6eZW4U1
V9Pqk2JhIArHMHckEU1ig7a6e2iCMe5lyt/51Y2yNdyMK29qclxghJzyDJRewFZS
AEjM0/ilfd4v1xPkOKiE1Ua4E4bCG53qWjjdm9sCAwEAAaOCAYMwggF/MC8GCCsG
AQUFBwEBBCMwITAfBggrBgEFBQcwAYYTaHR0cDovL3MyLnN5bWNiLmNvbTASBgNV
HRMBAf8ECDAGAQH/AgEAMGwGA1UdIARlMGMwYQYLYIZIAYb4RQEHFwMwUjAmBggr
BgEFBQcCARYaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9jcHMwKAYIKwYBBQUHAgIw
HBoaaHR0cDovL3d3dy5zeW1hdXRoLmNvbS9ycGEwMAYDVR0fBCkwJzAloCOgIYYf
aHR0cDovL3MxLnN5bWNiLmNvbS9wY2EzLWc1LmNybDAdBgNVHSUEFjAUBggrBgEF
BQcDAgYIKwYBBQUHAwMwDgYDVR0PAQH/BAQDAgEGMCkGA1UdEQQiMCCkHjAcMRow
GAYDVQQDExFTeW1hbnRlY1BLSS0xLTU2NzAdBgNVHQ4EFgQUljtT8Hkzl699g+8u
K8zKt4YecmYwHwYDVR0jBBgwFoAUf9Nlp8Ld7LvwMAnzQzn6Aq8zMTMwDQYJKoZI
hvcNAQELBQADggEBABOFGh5pqTf3oL2kr34dYVP+nYxeDKZ1HngXI9397BoDVTn7
cZXHZVqnjjDSRFph23Bv2iEFwi5zuknx0ZP+XcnNXgPgiZ4/dB7X9ziLqdbPuzUv
M1ioklbRyE07guZ5hBb8KLCxR/Mdoj7uh9mmf6RWpT+thC4p3ny8qKqjPQQB6rqT
og5QIikXTIfkOhFf1qQliZsFay+0yQFMJ3sLrBkFIqBgFT/ayftNTI/7cmd3/SeU
x7o1DohJ/o39KK9KEr0Ns5cF3kQMFfo2KwPcwVAB8aERXRTl4r0nS1S+K4ReD6bD
dAUK75fDiSKxH3fzvc1D1PFMqT+1i4SvZPLQFCE=
-----END CERTIFICATE-----
EOF
  end
end
Planography answered 24/6, 2015 at 22:23 Comment(2)
I would add ensure OpenSSL.errors.clear to main function because OpenSSL itself does not clear errors.Mythology
The CA link is dead now, how can I find an up-to-date version?Allophane
D
0

Here is my implementation in Elixir.

def verify_login(player_id, public_key_url, timestamp, salt64, signature64, bundle_id) do
  salt = Base.decode64!(salt64)
  pay_load = <<player_id :: binary, bundle_id :: binary, timestamp :: big-size(64), salt :: binary>>
  pkey_cert = get_public_key_certificate(public_key_url)
  cert = :public_key.pkix_decode_cert(pkey_cert, :otp)
  case cert do
    {:OTPCertificate,
     {:OTPTBSCertificate, _, _, _, _, _, _,
      {:OTPSubjectPublicKeyInfo, _, key}, _, _, _}, _, _} ->
      signature = Base.decode64!(signature64)
      case :public_key.verify(pay_load, :sha256, signature, key) do
        true ->
          :ok
        false ->
          {:error, "apple login verify failed"}
      end
  end
end

def get_public_key_certificate(url) do
  case HTTPoison.get(url) do
    {:ok, %HTTPoison.Response{body: body}} ->
      body
  end
end
Dipstick answered 10/6, 2016 at 10:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.