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

后端 未结 10 416
鱼传尺愫
鱼传尺愫 2020-11-30 02:53

iOS7 introduced new GKLocalPlayer method generateIdentityVerificationSignatureWithCompletionHandler().

Does anyone know how to use it for good? I assum

相关标签:
10条回答
  • 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."
    
    0 讨论(0)
  • 2020-11-30 03:28

    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;
    }
    
    0 讨论(0)
  • 2020-11-30 03:29

    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
    }
    
    0 讨论(0)
  • 2020-11-30 03:29

    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://stackoverflow.com/questions/17408729/how-to-authenticate-the-gklocalplayer-on-my-third-party-server
    #
    # *** 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
    
    0 讨论(0)
提交回复
热议问题