Skip to content

Instantly share code, notes, and snippets.

@BastianBlokland
Last active February 16, 2024 22:19
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BastianBlokland/bbc02a407b05beaf3f55ead3dd10f808 to your computer and use it in GitHub Desktop.
Save BastianBlokland/bbc02a407b05beaf3f55ead3dd10f808 to your computer and use it in GitHub Desktop.
Way to get the game-center verification signature with Unity.

Even tho Unity has implemented a game-center api (Social api) it doesn't have a way to generate a verification signature, which you need to use game-center as a authentication method.

Luckily the objective-c code required is pretty straight forward. (Apple documentation: generateIdentityVerificationSignature)

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>

typedef void (*IdentityVerificationSignatureCallback)(const char * publicKeyUrl, const char * signature, int signatureLength, const char * salt, int saltLength, const uint64_t timestamp, const char * error);

extern void generateIdentityVerificationSignature(IdentityVerificationSignatureCallback callback) {
    
    GKLocalPlayer * localPlayer = [GKLocalPlayer localPlayer];

    NSLog(@"LocalPlayer: %@", localPlayer.playerID);
    [localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL * publicKeyUrl, NSData * signature, NSData * salt, uint64_t timestamp, NSError * error) {

        NSLog(@"Received 'generateIdentityVerificationSignature' callback, error: %@", error.description);

        // Create a pool for releasing the resources we create
        @autoreleasepool {
            
            // PublicKeyUrl
            const char * publicKeyUrlCharPointer = NULL;
            if (publicKeyUrl != NULL)
            {
                const NSString * publicKeyUrlString = [[NSString alloc] initWithString:[publicKeyUrl absoluteString]];
                publicKeyUrlCharPointer = [publicKeyUrlString UTF8String];
            }

            // Signature
            const char * signatureBytes = [signature bytes];
            int signatureLength = (int)[signature length];

            // Salt
            const char * saltBytes = [salt bytes];
            int saltLength = (int)[salt length];

            // Error
            const NSString * errorString = error.description;
            const char * errorStringPointer = [errorString UTF8String];

            // Callback
            callback(publicKeyUrlCharPointer, signatureBytes, signatureLength, saltBytes, saltLength, timestamp, errorStringPointer);
        }
    }];
}

Strings are marshalled automatically for us and for the byte[]'s we pass a pointer to the obj-c memory, that way we can copy the data into managed arrays.

// Add a using for: AOT
// Add a using for: System.Runtime.InteropServices

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void IdentityVerificationSignatureCallback(
    string publicKeyUrl, 
    IntPtr signaturePointer, int signatureLength,
    IntPtr saltPointer, int saltLength,
    ulong timestamp,
    string error);

[DllImport("__Internal")]
private static extern void generateIdentityVerificationSignature(
    [MarshalAs(UnmanagedType.FunctionPtr)]IdentityVerificationSignatureCallback callback);

// Note: This callback has to be static because Unity's il2Cpp doesn't support marshalling instance methods.
[MonoPInvokeCallback(typeof(IdentityVerificationSignatureCallback))]
private static void OnIdentityVerificationSignatureGenerated(
    string publicKeyUrl, 
    IntPtr signaturePointer, int signatureLength,
    IntPtr saltPointer, int saltLength,
    ulong timestamp,
    string error)
{
    // Create a managed array for the signature
    var signature = new byte[signatureLength];
    Marshal.Copy(signaturePointer, signature, 0, signatureLength);

    // Create a managed array for the salt
    var salt = new byte[saltLength];
    Marshal.Copy(saltPointer, salt, 0, saltLength);

    UnityEngine.Debug.Log($"publicKeyUrl: {publicKeyUrl}");
    UnityEngine.Debug.Log($"signature: {signature.Length}");
    UnityEngine.Debug.Log($"salt: {salt.Length}");
    UnityEngine.Debug.Log($"timestamp: {timestamp}");
    UnityEngine.Debug.Log($"error: {error}");
}

You can then call generateIdentityVerificationSignature and give it OnIdentityVerificationSignatureGenerated as a callback, you can then in the callback save the results to a static variable for example.

Note: this assumes that the user is already logged-in to game-center, luckily Unity has already implemented the api to login:

Social.localUser.Authenticate(success =>
{

});

For info on where to place the objective-c code Unity has a manual page: PluginsForIOS

@jacobfdunbar
Copy link

Hey Bastian, thanks for this gist! I ran into some trouble when building in XCode however, was wondering if you'd be able to lend a hand:

Undefined symbols for architecture arm64: "_generateIdentityVerificationSignature", referenced from: _IOSService_LinkWithPlayFab_m50C06AB4A24D0671D8FFFA339CDF9E97E004B60B in Assembly-CSharp1.o _IOSService_generateIdentityVerificationSignature_m984F45BAEDBD21D460CB748008F996C974F752BE in Assembly-CSharp1.o _U3CU3Ec__DisplayClass3_0_U3CLinkWithPlayFabU3Eb__0_m41D6AB0E02E8D40953DC88EC2554F12591BEED1F in Assembly-CSharp1.o (maybe you meant: _IOSService_generateIdentityVerificationSignature_m984F45BAEDBD21D460CB748008F996C974F752BE) ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation)

The code in the project is exactly as shown here, aside from some minor changes in the .mm file (that shouldn't be causing this linker issue). Thanks in advance for any help you can provide!

@BastianBlokland
Copy link
Author

Hey Jacob,

Sounds like Unity did not include the objective-c code and it fails to link the extern void generateIdentityVerificationSignature in the c# to the objective-c function.

You could double check if the .m file (its objective-c not objective-c++ so .m is sufficient however .mm will work also), is flagged to be included in Unity:
screenshot
The obj-c uses the GameKit api so its important make the obj-c file depend on the GameKit framework.

Alternatively you could check in the xcode project if the code with the generateIdentityVerificationSignature obj-c function is present.

@jacobfdunbar
Copy link

Thanks for the swift reply! I've now got it fixed. The plugin was set up correctly in Unity, but changing it to a .m file instead of .mm seemed to do the trick. I also renamed the file to how you had it named, but I don't think that made any difference. Thanks again for your help!

@BastianBlokland
Copy link
Author

Good to hear that it works, interesting that .mm did not work. Could be that the c++ function name mangling caused a mismatch between the expected function name, anyway good to know.

@BastianBlokland
Copy link
Author

BastianBlokland commented May 6, 2020 via email

@petracles
Copy link

Hey there - I was wondering if you or anyone else in this thread are going to change their authentication-flows now that generateIdentityVerificationSignature is deprecated? I've been learning GameKit recently and have been catching up on these recent changes with player identities and 3rd-party authorization.

@martman100
Copy link

I must have accidentally deleted my post. I got this working. What I am now confused on is the fact that it is deprecated. Does that mean if I submit my app to the App Store it will get rejected? If so what is the alternative? My main problem is I am using PlayFab but authentication is up them for what they offer on their services.

@petracles
Copy link

I am confused too @martman100 - I see there's authenticateHandler and isAuthenticated now, but those don't help solve the problem of getting the player authenticated by a 3rd party 😑 I don't think your app will get rejected by the App Store if it still has generateIdentityVerificationSignature.

@BastianBlokland
Copy link
Author

BastianBlokland commented May 10, 2020

Hey there - I was wondering if you or anyone else in this thread are going to change their authentication-flows now that generateIdentityVerificationSignature is deprecated? I've been learning GameKit recently and have been catching up on these recent changes with player identities and 3rd-party authorization.

Thanks for the heads up about the deprecation, will have to look into that.

I must have accidentally deleted my post. I got this working. What I am now confused on is the fact that it is deprecated. Does that mean if I submit my app to the App Store it will get rejected? If so what is the alternative? My main problem is I am using PlayFab but authentication is up them for what they offer on their services.

Good to hear that you got it working. Atm i don't think you will be rejected, at work we are using this in two live projects and have had no problems shipping updates. But it's an interesting question what the replacement api for this is.

@petracles
Copy link

From this doc here, it seems that 3rd-party authentication is no longer necessary... what do you guys think?

To authenticate the local player, you create your own method that sets the authenticateHandler property. The method retrieves the shared instance of the GKLocalPlayer class and then sets that object’s authenticateHandler property to point to a block object that handles authentication events. Once you set an authentication handler, Game Kit automatically authenticates the player asynchronously, calling your authentication handler as necessary to complete the process.

@martman100
Copy link

The problem I am having is 3rd party auth for services like PlayFab that are being used through Unity 3D. Not sure what the appropriate process there is now

@BastianBlokland
Copy link
Author

The problem I am having is 3rd party auth for services like PlayFab that are being used through Unity 3D. Not sure what the appropriate process there is now

Also having some trouble finding the replacement api, we are using the gamecenter as a way to authenticate to our own account system. So we need some way to verify the users identity on the backend.

@martman100
Copy link

This type of stuff makes it really difficult to get away from using Facebook for 3rd party auth and verification. My focus is mobile games on android and IOS and Facebook is just not the desire now for authentication.

@petracles
Copy link

Okay so... hiding in the source is the new auth-handle called fetchItemsForIdentityVerificationSignature. It looks like the same signature as the deprecated generateIdentityVerificationSignature but with teamPlayerId instead.

@BastianBlokland
Copy link
Author

Okay so... hiding in the source is the new auth-handle called fetchItemsForIdentityVerificationSignature. It looks like the same signature as the deprecated generateIdentityVerificationSignature but with teamPlayerId instead.

Nice. thanks allot for sharing that. I'm happy we don't need to do major changes to our login 😄

@lucasmonje
Copy link

Okay so... hiding in the source is the new auth-handle called fetchItemsForIdentityVerificationSignature. It looks like the same signature as the deprecated generateIdentityVerificationSignature but with teamPlayerId instead.

Nice. thanks allot for sharing that. I'm happy we don't need to do major changes to our login 😄

Did you manage to tackled this problem? I'm on the same situation, If you did it, could you please update unity_ios_verificationsignature.md, or sharing your experience? thanks!

@BastianBlokland
Copy link
Author

I must admit we didn't get around to it yet 😊

@mirkosrsen1
Copy link

Hello there, I need to convert byte arrays of salt and signature to strings(in C#). Do I just use Convert.ToBase64String or there is some other method for it. Thanks in advance!

@BastianBlokland
Copy link
Author

That would depend on what format the service you are using expects it in.

On the backend you end up needing both as raw bytes. You use salt, playerId, bundleId and timestamp to generate a new hash and then compare that to the signature using Apple's public key.
Apple has some more info on the (now deprecated 😢) docs for 'generateIdentityVerificationSignature'.

But having said that, if you are using an external service with a http api, its highly likely they expect them to be base64 encoded indeed. But their documentation should really say the format.

@natewilliford
Copy link

Hello there, I need to convert byte arrays of salt and signature to strings(in C#). Do I just use Convert.ToBase64String or there is some other method for it. Thanks in advance!

FWIW, PlayFab expects base64 encoded salt and signature.

@okankayhan
Copy link

okankayhan commented Nov 23, 2022

Updated the code to work with new auth handle fetchItemsForIdentityVerificationSignature + some casts to make it work.

.m File

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>

typedef void (*IdentityVerificationSignatureCallback)(const char * publicKeyUrl, const char * signature, int signatureLength, const char * salt, int saltLength, const uint64_t timestamp, const char * error);

extern void generateIdentityVerificationSignature(IdentityVerificationSignatureCallback callback) {
    
    GKLocalPlayer * localPlayer = [GKLocalPlayer localPlayer];

    NSLog(@"LocalPlayer: %@", localPlayer.playerID);
    
    [localPlayer fetchItemsForIdentityVerificationSignature:^(NSURL * _Nullable publicKeyURL, NSData * _Nullable signature, NSData * _Nullable salt, uint64_t timestamp, NSError * _Nullable error) {

        NSLog(@"Received 'generateIdentityVerificationSignature' callback, error: %@", error.description);

        // Create a pool for releasing the resources we create
        @autoreleasepool {
            
            // PublicKeyUrl
            const char * publicKeyUrlCharPointer = NULL;
            if (publicKeyURL != NULL)
            {
                const NSString * publicKeyUrlString = [[NSString alloc] initWithString:[publicKeyURL absoluteString]];
                publicKeyUrlCharPointer = [publicKeyUrlString UTF8String];
            }


            // Signature
            const char * signatureBytes = (char*)[signature bytes];
            int signatureLength = (int)[signature length];

            // Salt
            const char * saltBytes = (char*)[salt bytes];
            int saltLength = (int)[salt length];

            // Error
            const NSString * errorString = error.description;
            const char * errorStringPointer = [errorString UTF8String];

            // Callback
            callback(publicKeyUrlCharPointer, signatureBytes, signatureLength, saltBytes, saltLength, timestamp, errorStringPointer);
        }
    }];
}

.cs file

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void IdentityVerificationSignatureCallback(
        string publicKeyUrl, 
        IntPtr signaturePointer, int signatureLength,
        IntPtr saltPointer, int saltLength,
        ulong timestamp,
        string error);

    [DllImport("__Internal")]
    public static extern void generateIdentityVerificationSignature(
        [MarshalAs(UnmanagedType.FunctionPtr)]IdentityVerificationSignatureCallback callback);

// Note: This callback has to be static because Unity's il2Cpp doesn't support marshalling instance methods.
    [MonoPInvokeCallback(typeof(IdentityVerificationSignatureCallback))]
    public static void OnIdentityVerificationSignatureGenerated(
        string publicKeyUrl, 
        IntPtr signaturePointer, int signatureLength,
        IntPtr saltPointer, int saltLength,
        ulong timestamp,
        string error)
    {
        // Create a managed array for the signature
        var signature = new byte[signatureLength];
        Marshal.Copy(signaturePointer, signature, 0, signatureLength);

        // Create a managed array for the salt
        var salt = new byte[saltLength];
        Marshal.Copy(saltPointer, salt, 0, saltLength);

        UnityEngine.Debug.Log($"publicKeyUrl: {publicKeyUrl}");
        UnityEngine.Debug.Log($"signature: {signature.Length}");
        UnityEngine.Debug.Log($"salt: {salt.Length}");
        UnityEngine.Debug.Log($"timestamp: {timestamp}");
        UnityEngine.Debug.Log($"error: {error}");
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment