I've been trying to encrypt and decrypt a string in swift using a Diffie Hellman key exchange and an elliptic curve encryption.
Following is the code that I followed.
SWIFT Code :
let attributes: [String: Any] = [kSecAttrKeySizeInBits as String: 256,
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false]]
var error: Unmanaged<CFError>?
if #available(iOS 10.0, *) {
**// Step 1: Generating the Public & Private Key**
guard let privateKey1 = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {return false}
let publicKey1 = SecKeyCopyPublicKey(privateKey1)
guard let privateKey2 = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {return false}
let publicKey2 = SecKeyCopyPublicKey(privateKey2)
let dict: [String: Any] = [:]
**// Step 2: Generating Shared Key**
guard let shared1 = SecKeyCopyKeyExchangeResult(privateKey1, SecKeyAlgorithm.ecdhKeyExchangeStandardX963SHA256, publicKey2!, dict as CFDictionary, &error) else {return false}
**// Step 3: Encrypt string using Sharedkey**
let options: [String: Any] = [kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits as String : 256]
// Stuck from this line on
guard let key = SecKeyCreateWithData(shared1 ,
options as CFDictionary,
&error) else {return false}
print(key)
let str = "Hello"
let byteStr: [UInt8] = Array(str.utf8)
let cfData = CFDataCreate(nil, byteStr, byteStr.count)
guard let encrypted = SecKeyCreateEncryptedData(publicKey1!,
SecKeyAlgorithm.ecdsaSignatureDigestX962SHA256,
cfData!,
&error) else {return false}
print(encrypted)
} else {
print("unsupported")
}
JAVA Code :
public static Map<String, Object> ecEncrypt(String deviceData, String serverPubKey, String dsTranID)
throws DataEncryptionException {
provider = new BouncyCastleProvider();
HashMap<String, Object> result = null;
JWEObject jweObject = null;
JWK jwk = null;
SecretKey Z = null;
JWEHeader header = null;
ECPublicKey ecpubkey = null;
byte[] byte_pubkey = null;
try {
result = new HashMap<String, Object>();
/*
* Generate Ephemeral keypair for SDk which constitute Public and
* Private key of SDK
*/
STEP 1:
sdkKeyPair = Crypto.generateEphemeralKeyPair();
/*
* Compute Secrete Key Z from SDKs Private Key(pSDK),DS Public
* key(serverPubKey) and DS ID
*/
//converting string to Bytes
STEP 2:
byte_pubkey = Base64.decode(serverPubKey, android.util.Base64.DEFAULT);
//converting it back to EC public key
STEP 3:
KeyFactory factory = KeyFactory.getInstance("ECDSA", provider);
ecpubkey = (ECPublicKey) factory.generatePublic(new X509EncodedKeySpec(byte_pubkey));
System.out.println("FINAL OUTPUT" + ecpubkey);
STEP 4:
Z = Crypto.generateECDHSecret(ecpubkey,
(ECPrivateKey) sdkKeyPair.getPrivate(), dsTranID,
"A128CBC_HS256");
System.out.println("****Secrete key Z for SDK Computed succesfully *****");
/*
* Build JWK to construct header
*/
STEP 5:
jwk = new ECKey.Builder(Curve.P_256,
(ECPublicKey) sdkKeyPair.getPublic()).build();
STEP 6:
header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES,
EncryptionMethod.A128CBC_HS256).ephemeralPublicKey(
ECKey.parse(jwk.toJSONString())).build();
System.out.println("****Header for SDK Computed succesfully*****");
/*
* Add Header and payload before encrypting payload using secret key
* Z
*/
STEP 7:
jweObject = new JWEObject(header, new Payload(deviceData));
jweObject.encrypt(new DirectEncrypter(Z));
/*
* serialize JWEobject which contains
* [header-base64url].[encryptedKey
* -base64url].[iv-base64url].[cipherText
* -base64url].[authTag-base64url]
*/
System.out
.println("****Payload of SDK encrypted succesfully *****");
return result;
} catch (Exception e) {
e.printStackTrace();
throw new DataEncryptionException();
} finally {
sdkKeyPair = null;
jweObject = null;
jwk = null;
Z = null;
header = null;
}
}
I included Java code as well. I have to do the same in Swift. How to do EC Encryption using the Shared key(Shared1) to encrypt the string? I need to do Step 3. Anyone please help on this?
First of all, you're trying to implement ECIES. Knowing the actual name is important if you want to look up information about the scheme.
So lets assume that key pair 1 is from the sender of the ciphertext and key pair 2 is then from the receiver of the ciphertext. In that case key pair 1 should be ephemeral (created on the spot, tied to one encrypted message) and key pair 2 is static (created beforehand and kept). Furthermore, public key 2 is trusted to be from the receiving party. This is all not clear from your simplified code, and in your code you could still switch around the sender and receiver.
So with the public key (2) of the receiver the sender can use their private key to create a shared secret, called shared1 in your code. You can now use shared1 to perform symmetric encryption of the data. Then you just have to send the ephemeral public key of the sender and the ciphertext to the receiver. The receiver uses the public key of the sender (1) and their static private key (2) to create shared2. This is identical to shared1 and can thus be used as a key to decrypt the data.
So that's it, other than to note that since the private key (1) of the sender is tied to the data, it isn't needed anymore once shared1 is calculated, and may be discarded even before the message is encrypted.
If you read above then you can see that having all of this in one method is not a good idea:
create the static key pair of the receiver;
send the public key of the receiver to the sender and make sure that the sender can trust the public key to be from the receiver (e.g. using certificate infrastructure);
Now for the encryption and sending:
create the key pair of the sender;
derive the symmetric key;
throw away the private key;
encrypt the data;
send the public key and the data;
and for the receiving:
use the private key to derive the symmetric key;
decrypt the data.
And that's it. You probably want to have these steps made explicit in your code.
I am trying to call a method associated with the device using connection string.
I tried with the samples provided with other languages I am able to call the method in the device. eg: "setState" or "getState" of the lamp.
But I am not able to implement in iOS using swift.
I tried to match parameter parameter requirement by referring to the C sample. But I am getting
1. Func:sendHttpRequestDeviceMethod Line:337 Http Failure status code 400.
2. Func:IoTHubDeviceMethod_Invoke Line:492 Failure sending HTTP request for device method invoke
var status :Int32! = 0
var deviceId = "simulated_device_one";
var methodName = "GetState";
var uint8Pointer:UnsafeMutablePointer<UInt8>!
uint8Pointer = UnsafeMutablePointer<UInt8>.allocate(capacity:8)
var size = size_t(10000)
var bytes: [UInt8] = [39, 77, 111, 111, 102, 33, 39, 0]
uint8Pointer?.initialize(from: &bytes, count: 8)
var intValue : UnsafeMutablePointer<UInt8>?
intValue = UnsafeMutablePointer(uint8Pointer)
var char: UInt8 = UInt8(20)
var charPointer = UnsafeMutablePointer<UInt8>(&char)
var prediction = intValue
let serviceClientDeviceMethodHandle = IoTHubDeviceMethod_Create(service_client_handle)
let payLoad = "test"
var responsePayload = ""
let invoke = IoTHubDeviceMethod_Invoke(serviceClientDeviceMethodHandle, deviceId, methodName, payLoad , 100, &status, &prediction,&size )
I want to call a method in the device using IoTHubDeviceMethod_Invoke
You can download View controller file which I worked on from here
1.Create Connection in view did load
// declaring your connection string you can find it in azure iot dashboard
private let connectionString = "Enter your connection String";
// creating service handler
private var service_client_handle: IOTHUB_SERVICE_CLIENT_AUTH_HANDLE!;
// handler for the method invoke
private var iot_device_method_handle:IOTHUB_SERVICE_CLIENT_DEVICE_METHOD_HANDLE!;
// In view did load establish the connection
service_client_handle = IoTHubServiceClientAuth_CreateFromConnectionString(connectionString)
if (service_client_handle == nil) {
showError(message: "Failed to create IoT Service handle", sendState: false)
}
create method invoke function
I created it based on the demo provided for sending message
func openIothubMethodInvoke() -> Bool
{
print("In openIotHub method invoke")
let result: Bool;
iot_device_method_handle = IoTHubDeviceMethod_Create(service_client_handle);
let testValue : Any? = iot_device_method_handle;
if (testValue == nil) {
showError(message: "Failed to create IoT devicemethod", sendState: false);
result = false;
}
else
{
result = true;
}
return result;
}
call method invoke
** this is the main function for calling the method
func methodInvoke()
{
let testValue : Any? = iot_device_method_handle;
if (testValue == nil && !openIothubMethodInvoke() ) {
print("Failued to open IoThub messaging");
}
else {
let size = UnsafeMutablePointer<Int>.allocate(capacity: 1)
let responseStatus = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
// Payload is the main change it is like specifying the format
var payload = UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>.allocate(capacity: 1)
// if payload is not expected please send empty json "{}"
let result = IoTHubDeviceMethod_Invoke(iot_device_method_handle, "nameOfTheDeviceYouWantToCallOn", "MethodName", "{payload you want to send}", 100, responseStatus, payload , size)
// extracting the data from response
let b = UnsafeMutableBufferPointer(start: payload.pointee, count: size.pointee)
let data = Data(buffer: b)
let str = String(bytes: data, encoding: .utf8)
print(str)
do{
let value = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(value)
}catch{
print(error)
}
}
}
As discussed above: the payload needs to be valid JSON. Even an empty json will do such as {}
In my Xamarin.iOS project I used SecRecord/SecKeyChain to store my token values and app version. From production log I found keychain related exceptions with status code 'InteractionNotAllowed' when try to write/read items in keychain. Apple documents states that to resolve InteractionNotAllowed error we need to change the default kSecAttrAccessible attribute value from ‘WhenUnlocked' to ‘Always’.
But in my existing code when I changed accessible attribute to 'Always' app log out because it failed to read token from keychain. It return’s 'Item not found' when read. But when I tried to save token again it returns 'Duplicate item'. So again I tried to remove same item but this time it again returns 'Item not found'. That’s really strange I can’t delete it and I can’t read it with same key.
Below is the code snippet -
private SecRecord CreateRecordForNewKeyValue(string accountName, string value)
{
return new SecRecord(SecKind.GenericPassword)
{
Service = App.AppName,
Account = accountName,
ValueData = NSData.FromString(value, NSStringEncoding.UTF8),
Accessible = SecAccessible.Always //This line of code is newly added.
};
}
private SecRecord ExistingRecordForKey(string accountName)
{
return new SecRecord(SecKind.GenericPassword)
{
Service = App.AppName,
Account = accountName,
Accessible = SecAccessible.Always //This line of code is newly added.
};
}
public void SetValueForKeyAndAccount(string value, string accountName, string key)
{
var record = ExistingRecordForKey(accountName);
try
{
if (string.IsNullOrEmpty(value))
{
if (!string.IsNullOrEmpty(GetValueFromAccountAndKey(accountName, key)))
RemoveRecord(record);
return;
}
// if the key already exists, remove it before set value
if (!string.IsNullOrEmpty(GetValueFromAccountAndKey(accountName, key)))
RemoveRecord(record);
}
catch (Exception e)
{
//Log exception here -("RemoveRecord Failed " + accountName, e,);
}
//Adding new record values to keychain
var result = SecKeyChain.Add(CreateRecordForNewKeyValue(accountName, value));
if (result != SecStatusCode.Success)
{
if (result == SecStatusCode.DuplicateItem)
{
try
{
//Log exception here -("Error adding record: {0} for Account-" + accountName, result), "Try Remove account");
RemoveRecord(record);
}
catch (Exception e)
{
//Log exception here -("RemoveRecord Failed after getting error SecStatusCode.DuplicateItem for Account-" + accountName, e);
}
}
else
throw new Exception(string.Format("Error adding record: {0} for Account-" + accountName, result));
}
}
public string GetValueFromAccountAndKey(string accountName, string key)
{
try
{
var record = ExistingRecordForKey(accountName);
SecStatusCode resultCode;
var match = SecKeyChain.QueryAsRecord(record, out resultCode);
if (resultCode == SecStatusCode.Success)
{
if (match.ValueData != null)
{
string valueData = NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
if (string.IsNullOrEmpty(valueData))
return string.Empty;
return valueData;
}
else if (match.Generic != null)
{
string valueData = NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
if (string.IsNullOrEmpty(valueData))
return string.Empty;
return valueData;
}
else
return string.Empty;
}
}
catch (Exception e)
{
// Exception logged here -("iOS Keychain Error for account-" + accountName, e);
}
return string.Empty;
}
Any help would be great! Thanks
The property Service is also an unique identification when we store or retrieve data using KeyChain. You didn't post your GetValueFromAccountAndKey() method, so we don't know what is the key used for? But in your case, you should use the same Service to retrieve value:
string GetValueFromAccountAndKey(string accoundName, string service)
{
var securityRecord = new SecRecord(SecKind.GenericPassword)
{
Service = service,
Account = accoundName
};
SecStatusCode status;
NSData resultData = SecKeyChain.QueryAsData(securityRecord, false, out status);
var result = resultData != null ? new NSString(resultData, NSStringEncoding.UTF8) : "Not found";
return result;
}
Since you just make a hard code in your CreateRecordForNewKeyValue()( the Service has been written as a constant ), if you want to retrieve your value you should also set the Service as App.AppName in the method GetValueFromAccountAndKey().
It return’s 'Item not found' when read. But when I tried to save token
again it returns 'Duplicate item'.
This is because when we use the same Account but different Service to retrieve data, KeyChain can't find the corresponding SecRecord. This made you thought the SecRecord didn't exist, then use the same Account to store value. The Duplicate item result throws out. For a SecRecord, the Account and Service must both be unique.
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..
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_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;
}
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())
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."
require 'base64'
require 'httparty'
module GameCenter
include HTTParty
# HHTTParty settings
HTTPARTY_TIMEOUT = 10
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::SHA1.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.
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
}
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
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
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