Question

Using the Apple's Game Center authentication verification steps outlined here, the verification logic below has been implemented using Java. However, this always fails.

import java.net.URL;

import java.nio.ByteBuffer;

import java.nio.ByteOrder;

import java.security.KeyPair;

import java.security.KeyPairGenerator;

import java.security.MessageDigest;

import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.SecureRandom;

import java.security.Signature;

import java.security.cert.Certificate;

import java.security.cert.CertificateFactory;

import java.security.spec.AlgorithmParameterSpec;

import java.util.Arrays;

import javax.crypto.Cipher;

import javax.xml.bind.DatatypeConverter;

public class Verifier {

    public static void main(String[] args) {

        verify1();   

    }



    public static void verify1() {

        try {

            byte[] playerID = "G:90082947".getBytes("UTF-8");

            byte[] bundleID = "com.appledts.GameCenterSamples".getBytes("UTF-8");



            long ts = 1392078336714L;

            final ByteBuffer tsByteBuffer = ByteBuffer.allocate(8);

            tsByteBuffer.order(ByteOrder.BIG_ENDIAN);

            tsByteBuffer.putLong(ts);           

            byte[] timestamp = tsByteBuffer.array();



            byte[] salt = DatatypeConverter.parseBase64Binary("xmvbZQ==");



            byte[] sigToCheck = DatatypeConverter.parseBase64Binary("AmyNbm+7wJOjXv6GXI/vAEcl6gSX1AKxPr3GeExSYCiaxVaAeIvC23TWtp1/Vd/szfq1r1OzwrvkHeSSiskWMsMXaGQWUmiGtCnf9fqBU75T5PwNLCj4H9Nd5QENCMV/CFgVyGEi4X6Wlp18kqJPk/ooS6jLJwcWIe6DyrR1bQHl6YzKTfB4ACl2JEccBDz8dArKTrh4vFcQF4a+DtERm283Y2ue1DwG8lqWrYhsRO5v7vrW3lVpn5t25QXc+Y35zJ/il+lZJxKAgASwrKaq3G8RStdkeXCER23fSYhTmbLFqkFRWnmzu38hmLt5/iivUbm8NgELXP0SyQoYLMvfmA==");



            ByteBuffer dataBuffer = ByteBuffer.allocate(playerID.length+bundleID.length+8+salt.length)

                .put(playerID)

                .put(bundleID)

                .put(timestamp)

                .put(salt);





            Certificate cert = CertificateFactory.getInstance("X.509")

                    .generateCertificate(new URL("https://sandbox.gc.apple.com/public-key/gc-sb.cer").openConnection().getInputStream());





            Signature sig = Signature.getInstance("SHA1withRSA");

            sig.initVerify(cert);



            sig.update(dataBuffer);



            final boolean verify = sig.verify(sigToCheck);

            System.out.println("signature verifies: " + verify);                            



        } catch (Exception e) {            

            e.printStackTrace();

        }

    }        

}

There were no loss of bits in transferring data from the iOS 7 client to the server. This was verified by writing the binary bits to a file both from xCode and Java, generating their hex, and seeing if there were any diffs (note, the diffs just show the file name diffs):

$ xxd -i salt_Java.txt salt_java.xxd

$ xxd -i salt_xcode.txt salt_xcode.xxd

$ xxd -i sigToCheck_Java.txt sigToCheck_java.xxd

$ xxd -i sigToCheck_xcode.txt sigToCheck_xcode.xxd

$ diff salt_java.xxd salt_xcode.xxd 

1c1

< unsigned char salt_Java_txt[] = {

---

> unsigned char salt_xcode_txt[] = {

4c4

< unsigned int salt_Java_txt_len = 4;

---

> unsigned int salt_xcode_txt_len = 4;

$ diff sigToCheck_java.xxd sigToCheck_xcode.xxd 

1c1

< unsigned char sigToCheck_Java_txt[] = {

---

> unsigned char sigToCheck_xcode_txt[] = {

25c25

< unsigned int sigToCheck_Java_txt_len = 256;

---

> unsigned int sigToCheck_xcode_txt_len = 256;

$ 

I believe this fails because of the underlying Java libraries that Signature class uses, since the Objective-C solution listed here appears to successfully verify the same credentials.

My next attempt was to use the Java's [Cipher] and [MessageDigest] libraries instead of the [Signature] library, but this too fails. I suspect there are other steps missing before the signature digest bits can be checked with the provided signature bits.

final MessageDigest md = MessageDigest.getInstance("SHA1");

byte[] digest = md.digest(dataBuffer.array());

// RSA decrypt

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

cipher.init(Cipher.DECRYPT_MODE, cert);

byte[] decrypted = cipher.doFinal(sigToCheck);    



System.out.println("signature verifies: " + Arrays.equals(digest, decrypted));

Are there alternatives to verifying the digital signature or any gaps in the solutions posted above?

Was it helpful?

Solution

The problem appears to be with the ByteBuffer you're passing to Signature.update(). If you pass the underlying array by changing

sig.update(dataBuffer);

to

sig.update(dataBuffer.array());

the verification appears to succeed. Based on the documentation for Signature.update(ByteBuffer), I suspect it's because it's trying to read from the last position you wrote to in the buffer, and not finding any data.

OTHER TIPS

Still not sure why Signature.verify fails, but found a work around for now: decrypt the signature to check, and unpad the SHA1 hash from the decrypted hash, and compare with the data buffer digest. If the two match, then it validates the game center user credentials, otherwise it does not. See sample code below.

 final MessageDigest md = MessageDigest.getInstance("SHA-1");
 byte[] digest = md.digest(dataBuffer.array());

 Cipher c2 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
 c2.init(Cipher.DECRYPT_MODE, cert.getPublicKey());
 byte[] decrypted2 = c2.doFinal(sigToCheck);
 final byte[] unpaddedSHA1 = Utils.unpadSHA1(decrypted2);

 System.out.println("signature verifies: " + Arrays.equals(digest, unpaddedSHA1));

Where the upadSHA1 is defined as follows:

private static final String SHA1_PAD = "3021300906052b0e03021a05000414";
private static final byte[] sha1pad = DatatypeConverter.parseHexBinary(SHA1_PAD);

public static byte[] unpadSHA1(byte[] padded) throws BadPaddingException {
    int k = 0;

    if (padded.length < sha1pad.length) {
        throw new BadPaddingException("Padding string too short");
    }
    while (true) {
        if (padded[k] != sha1pad[k]) {
            break;
        }
        k++;                  
        if (k == sha1pad.length) {
            break;
        }
    }
    int n = padded.length - k;
    if (n > 256) {
        throw new BadPaddingException("Padding string too short");
    }
    byte[] data = new byte[n];
    System.arraycopy(padded, padded.length - n, data, 0, n);
    return data;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top