RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error - ios

I'm trying to use iOS swift (client side) to encrypt some data before sending it to node.js server to decrypt it. However, while decrypting in node.js I'm hitting:
Error: error:04099079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error
at Object.privateDecrypt (internal/crypto/cipher.js:53:12)
at decrypt (/Users/iosbeta/Documents/RSA/RSANode/testRsa.js:20:28)
at Object.<anonymous> (/Users/RSANode/testRsa.js:36:13)
at Module._compile (internal/modules/cjs/loader.js:777:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:788:10)
at Module.load (internal/modules/cjs/loader.js:643:32)
at Function.Module._load (internal/modules/cjs/loader.js:556:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:840:10)
at internal/main/run_main_module.js:17:11 {
library: 'rsa routines',
function: 'RSA_padding_check_PKCS1_OAEP_mgf1',
reason: 'oaep decoding error',
code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR'
}
I've tried to encrypt and decrypt using only node.js code and it works. However, I couldn't get it to work after encrypting using iOS and decrypt using node.js. Here's how I generate the cert, encryption and decryption in node.js:
// ****************************************************************************************************************
// For generating keys
// ****************************************************************************************************************
const { writeFileSync } = require('fs')
const { generateKeyPairSync } = require('crypto')
function generateKeys() {
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: '',
},
})
writeFileSync('private.pem', privateKey)
writeFileSync('public.pem', publicKey)
}
// ****************************************************************************************************************
// For encrypting and decrypting
// ****************************************************************************************************************
const crypto = require('crypto')
const path = require('path')
const fs = require('fs')
function encrypt(toEncrypt, relativeOrAbsolutePathToPublicKey) {
const absolutePath = path.resolve(relativeOrAbsolutePathToPublicKey)
const publicKey = fs.readFileSync(absolutePath, 'utf8')
const buffer = Buffer.from(toEncrypt, 'utf8')
const encrypted = crypto.publicEncrypt(publicKey, buffer)
return encrypted.toString('base64')
}
function decrypt(toDecrypt, relativeOrAbsolutePathtoPrivateKey) {
const absolutePath = path.resolve(relativeOrAbsolutePathtoPrivateKey)
const privateKey = fs.readFileSync(absolutePath, 'utf8')
const buffer = Buffer.from(toDecrypt, 'base64')
const decrypted = crypto.privateDecrypt(
{
key: privateKey.toString(),
passphrase: '',
},
buffer,
)
return decrypted.toString('utf8')
}
This is the iOS swift code for creating encryption using the public pem cert and SwiftyRSA framework:
import SwiftyRSA
func createEncryption(){
do {
let publicKey = try PublicKey(pemNamed: "public")
let clear = try ClearMessage(string: "Clear Text", using: .utf8)
let encrypted = try clear.encrypted(with: publicKey, padding: .PKCS1)
// Then you can use:
let data = encrypted.data
let base64String = encrypted.base64String
print(base64String)
} catch {
// handle other errors
print(error)
}
}
This is my public.pem file:
-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEA2CtJRRthGDNTfbO1SO0MZWMKNC8YWA4ziAbrnYcdURGycg+RnfDw
hLb28kmPD2h6da/35LuklZHfGhgQSV1g288mdhKQILS9qjKI0/d0HRXxeOiOHiOl
zKhTHh+IsXRMQWrk8V2esnyNZVQ/rqr7t1YxQxGyp+eFoyH7//re9Kc8/hc9DRtq
+vcXKrX8m0K3M6t4fu9KH8iN0xlSeT9ufIrl//2/Omneic7ifZGNIVF+Grr8gpw4
CPYw1v8z6KjV8+5UhAmtiGjT2Jfcb8v9TKv6mBzYY15cvjAToWYxZaN3BPHx4yj5
7zdvewF4gIDM0O+XO9xShGCk213V4jdGyv21jaeMNqG6cmizzOHOZimpdy52cWou
ycBiIHQJrbSwGfxLtJt7D+DNLQibF35e3nQFYb+wiqtENRQJdUrZ37WmNPkpzlvY
dfYEfxWbgyckTmtCkqV6k2repYtbwb/l4AhYp/DgOPrV9waNTJqkNmy1fpfx8Azv
BLsII0MdXO5Ji1boAi3bf+IVuWlMVj5be4h/IJ2p0DGXiPh6cNWpmEXm1BoNrZ3B
GcnrAVp1Aa4FRR1kJUaHb47qVtStg2lyw0q+bwqE2vcGtRwEXWKIOQuXU9G7Z/Ug
CTHQunmMNWxMRQM+oZaxApv7F6ktUkj7nEz7P+a9ATRZNRA+71GtKecCAwEAAQ==
-----END RSA PUBLIC KEY-----
This is my private.pem file:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,D6F812707BED2D64ECE71D7FA6DDFF3F
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
-----END RSA PRIVATE KEY-----
Update:
Managed to solve it by adding the padding type param in Node.js
function decrypt(toDecrypt, relativeOrAbsolutePathtoPrivateKey) {
const absolutePath = path.resolve(relativeOrAbsolutePathtoPrivateKey)
const privateKey = fs.readFileSync(absolutePath, 'utf8')
const buffer = Buffer.from(toDecrypt, 'base64')
const decrypted = crypto.privateDecrypt(
{
key: privateKey.toString(),
passphrase: '',
padding:crypto.constants.RSA_PKCS1_PADDING
},
buffer,
)
return decrypted.toString('utf8')
}

Related

Decryption error with golang using RSA crypto package

I am using dart/flutter package encrypt to encrypt a string like this:
Future<String> encryptPIN({required String pin}) async {
final RSAPublicKey publicKey;
final publicPem =
await rootBundle.loadString('assets/keys/private-key.pem');
publicKey = RSAKeyParser().parse(publicPem) as RSAPublicKey;
final encrypter =
Encrypter(RSA(publicKey: publicKey, encoding: RSAEncoding.OAEP));
final encryptedPIN = encrypter.encrypt(pin).base64;
return encryptedPIN;
}
}
And trying to decrypt using golang
func Decrypt(rsaPrivateKey *rsa.PrivateKey, cipherText []byte) []byte {
hash := sha256.New()
r := rand.Reader
label := []byte(nil)
decryptedSum, err := rsa.DecryptOAEP(hash, r, rsaPrivateKey, cipherText, label)
if err != nil {
panic(err)
}
return decryptedSum
}
The dart side works, the encryption happens but the golang side fails with an error crypto/rsa: decryption error.
What could i be doing wrong?

iOS using CryptoSwift Encryption AES/CBC/PKCS5PADDING Equivalent encryption with java not working properly

I am building a swift application where I need to have AES/CBC/PKCS5PADDING Encryption type. To encrypt API params and decrypt API response.
I have successfully added in android java and that works fine.
Now I am trying to implement the same formate type of encryption 'AES/CBC/PKCS5PADDING' cipher size 16 in swift.
Problem: I am getting different encryption result for both android and iOS
Example:
String: Hi how are u
Encrypted String for android: +A3p093WWrXU3Ey9i/Lv1Q==
Encrypted String for swift(iOS): Bp23PQX6yaCghvKFTieJDw==
Swift Implementation
import Foundation
import CommonCrypto
func aesEncrypt(key: String, iv: String, message: String) throws -> String{
let data = message.data(using: .utf8)!
// let enc = try AES(key: key, iv: iv, padding: .pkcs5).encrypt([UInt8](data))
let enc = try AES(key: Array(key.utf8), blockMode: CBC(iv: Array(iv.utf8)), padding: .pkcs5).encrypt([UInt8](data))
let encryptedData = Data(enc)
return encryptedData.base64EncodedString()
}
func aesDecrypt(key: String, iv: String, message: String) throws -> String {
let data = NSData(base64Encoded: message, options: NSData.Base64DecodingOptions(rawValue: 0))!
let dec = try AES(key: key, iv: iv, padding: .pkcs5).decrypt([UInt8](data))
let decryptedData = Data(dec)
return String(bytes: decryptedData.bytes, encoding: .utf8) ?? "Could not decrypt"
}
Android Implementation
package com.example.uribanew.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class EncrytData {
private static String CIPHER_NAME = "AES/CBC/PKCS5PADDING";
private static int CIPHER_KEY_LEN = 16; //128 bits
private static String KEY = ""; // key to use should be 16 bytes long (128 bits)
private static String IV = ""; // initialization vector
/**
* Encrypt data using AES Cipher (CBC) with 128 bit key
*
* #param data - data to encrypt
* #return encryptedData data in base64 encoding with iv attached at end after a :
*/
public static String encrypt(String data) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"));
SecretKeySpec secretKey = new SecretKeySpec(fixKey(KEY).getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance(EncrytData.CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedData = cipher.doFinal((data.getBytes()));
String encryptedDataInBase64 = android.util.Base64.encodeToString(encryptedData, android.util.Base64.DEFAULT); //Base64.getEncoder().encodeToString(encryptedData);
return encryptedDataInBase64;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private static String fixKey(String key) {
if (key.length() < EncrytData.CIPHER_KEY_LEN) {
int numPad = EncrytData.CIPHER_KEY_LEN - key.length();
for (int i = 0; i < numPad; i++) {
key += "0"; //0 pad to len 16 bytes
}
return key;
}
if (key.length() > EncrytData.CIPHER_KEY_LEN) {
return key.substring(0, CIPHER_KEY_LEN); //truncate to 16 bytes
}
return key;
}
/**
* Decrypt data using AES Cipher (CBC) with 128 bit key
*
* #param data - encrypted data with iv at the end separate by :
* #return decrypted data string
*/
public static String decrypt(String data) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8"));
// SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
SecretKeySpec secretKey = new SecretKeySpec(fixKey(KEY).getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance(EncrytData.CIPHER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decodedEncryptedData = android.util.Base64.decode(data, android.util.Base64.DEFAULT); //Base64.getDecoder().decode(parts[0]);
byte[] original = cipher.doFinal(decodedEncryptedData);
return new String(original);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
Please let me know why with the same encryption type I am getting different encrypted string for both android and iOS. Let me know what I am doing wrong

Create signature in nodejs algorithm rsa-sha1 private_key.pem

Can I create signature like code below in Nodejs?
# Load PRIVATE key
private_key = OpenSSL::PKey::RSA.new(File.read(Rails.root + ENV['EPAY_PRIVATE_KEY']))
# Sign your data
signMessage = private_key.sign(OpenSSL::Digest::SHA1.new, message)
# Base64 message
baseMessage = Base64.encode64(signMessage.to_s)
You should be able to do the same thing in Node.js, creating a signature is quite easy, for example:
const crypto = require('crypto');
const fs = require('fs');
const privateKey = fs.readFileSync('./private-key.pem', 'utf8');
const message = "some message data";
const sign = crypto.createSign('SHA1');
sign.update(message);
sign.end();
const signature = sign.sign(privateKey);
console.log("Signature: ", signature.toString('base64'));
This creates a base64 encoded SHA1 signature of the message.

iOS MDM SCEP PKIOperation: The SCEP server returned an invalid response

Preface
I am working on implementing an iOS MDM server in Node.js and using node-forge for PKI. Part of Device Enrollment requires the use of SCEP.
Issue
Devices are currently failing at the initial operation=PKIOperation CSR request to my server. The error message seen from the devices is rather vague:
May 18 14:39:46 iPad-2 Preferences[27999] <Notice>: (Error) MC: Install profile data, interactive error. Error: NSError:
Desc : Profile Installation Failed
Sugg : The SCEP server returned an invalid response.
US Desc: Profile Installation Failed
US Sugg: The SCEP server returned an invalid response.
Domain : MCInstallationErrorDomain
Code : 4001
Type : MCFatalError
...Underlying error:
NSError:
Desc : The SCEP server returned an invalid response.
US Desc: The SCEP server returned an invalid response.
Domain : MCSCEPErrorDomain
Code : 22013
Type : MCFatalError
Extra info:
{
isPrimary = 1;
}
I have attempted to model my CSR handling based on Simple Certificate Enrollment Protocol Overview and the following Ruby sample code (found here and elsewhere):
def sign_PKI(data)
p7sign = OpenSSL::PKCS7.new(data)
store = OpenSSL::X509::Store.new
p7sign.verify(nil, store, nil, OpenSSL::PKCS7::NOVERIFY)
signers = p7sign.signers
p7enc = OpenSSL::PKCS7.new(p7sign.data)
# Certificate Signing Request
csr = p7enc.decrypt(SSL.key, SSL.certificate)
# Signed Certificate
cert = self.sign_certificate(csr)
degenerate_pkcs7 = OpenSSL::PKCS7.new()
degenerate_pkcs7.type="signed"
degenerate_pkcs7.certificates=[cert]
enc_cert = OpenSSL::PKCS7.encrypt(p7sign.certificates, degenerate_pkcs7.to_der,
OpenSSL::Cipher::Cipher::new("des-ede3-cbc"), OpenSSL::PKCS7::BINARY)
reply = OpenSSL::PKCS7.sign(SSL.certificate, SSL.key, enc_cert.to_der, [], OpenSSL::PKCS7::BINARY)
return Certificate.new(reply.to_der, "application/x-pki-message")
end
Finally, here is my implementation using Node.js and node-forge:
function pkiOperationScepOperationHandler(req, reply) {
//
// |req.query.message| should contain a Base64 encoded PKCS#7 package.
// The SignedData portion is PKCS#7 EnvelopedData encrypted with the CA
// public key we gave the client in GetCACert. Once decrypted, we have
// ourselves the client's CSR.
//
if(!req.query.message) {
return reply('The CA could not validate the request').code(403);
}
const msgBuffer = new Buffer(req.query.message, 'base64');
let p7Message;
try {
p7Message = forge.pkcs7.messageFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(msgBuffer, 'binary')
)
);
const p7EnvelopedData = forge.pkcs7.messageFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(new Buffer(p7Message.rawCapture.content.value[0].value[0].value, 'binary'), 'binary')
)
);
p7EnvelopedData.decrypt(p7EnvelopedData.recipients[0], conf.serverConfig.caPrivateKey);
// p7EnvelopedData should contain a PKCS#10 CSR
const csrDataBuffer = new Buffer(p7EnvelopedData.content.getBytes(), 'binary');
const csr = forge.pki.certificationRequestFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(csrDataBuffer, 'binary')
),
true // computeHash
);
//
// Create a new cert based on the CSR and sign it
//
// See https://github.com/digitalbazaar/forge/issues/154
//
const signedCert = forge.pki.createCertificate();
signedCert.serialNumber = Date.now().toString();
signedCert.validity.notBefore = new Date();
signedCert.validity.notAfter = new Date();
signedCert.validity.notAfter.setFullYear(signedCert.validity.notBefore.getFullYear() + 1);
signedCert.setSubject(csr.subject.attributes);
signedCert.setIssuer(conf.serverConfig.caCert.subject.attributes);
signedCert.setExtensions([
{
name : 'keyUsage',
digitalSignature : true,
keyEncipherment : true,
}
]);
signedCert.publicKey = csr.publicKey;
signedCert.sign(conf.serverConfig.caPrivateKey);
const degenerate = forge.pkcs7.createSignedData();
degenerate.addCertificate(signedCert);
const enveloped = forge.pkcs7.createEnvelopedData();
// UPDATE 1
enveloped.recipients.push({
version: 0,
issuer: csr.subject.attributes,
serialNumber: signedCert.serialNumber,
encryptedContent: {
algorithm: forge.pki.oids.rsaEncryption,
key: csr.publicKey
}
});
enveloped.content = forge.asn1.toDer(degenerate.toAsn1());
enveloped.encryptedContent.algorithm = forge.pki.oids['des-EDE3-CBC'];
enveloped.encrypt();
const signed = forge.pkcs7.createSignedData();
signed.addCertificate(conf.serverConfig.caCert);
signed.addSigner({
key : conf.serverConfig.caPrivateKey,
certificate : conf.serverConfig.caCert,
digestAlgorithm : forge.pki.oids.sha1,
authenticatedAttributes : [
{
type : forge.pki.oids.contentType,
value : forge.pki.oids.data
},
{
type: forge.pki.oids.messageDigest
},
{
type: forge.pki.oids.signingTime,
},
]
});
signed.content = forge.asn1.toDer(enveloped.toAsn1());
signed.sign();
const signedDer = new Buffer(forge.asn1.toDer(signed.toAsn1()).getBytes(), 'binary');
return reply(signedDer).bytes(signedDer.length).type('application/x-pki-message');
} catch(e) {
req.log( ['error' ], { message : e.toString() } );
return reply('The CA could not validate the request').code(403);
}
}
Can anyone point out what I'm doing wrong here?
Update 1:
Updated code above to reflect my latest. Still not working, but I believe the recipient information is now correct. (See UPDATE 1 above)
Finally got this working (and on to the next SCEP related headache!):
Overview of problems in original code:
Recipient MUST be the cert that signed the request: p7Message.certificates[0]
SCEP defines some Authenticated attributes that MUST be present. Of those, transactionID and senderNonce from the original request must be sent back (senderNonce is sent back as recipientNonce)
The OIDs for the SCEP specific attributes are not currently supported by node-forge. This required a very simple hack (See PKCS#7 signed data and custom authenticatedAttributes / OIDs)
Updated working code:
Below is some updated & working code (note that there are still some missing checks that need to be implemented for validation/etc.)
function pkiOperationScepOperationHandler(req, reply) {
//
// |req.query.message| should contain a Base64 encoded PKCS#7 package.
// The SignedData portion is PKCS#7 EnvelopedData encrypted with the CA
// public key we gave the client in GetCACert. Once decrypted, we have
// ourselves the client's CSR.
//
if(!req.query.message) {
return reply('The CA could not validate the request').code(403);
}
try {
const msgBuffer = new Buffer(req.query.message, 'base64');
const p7Message = forge.pkcs7.messageFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(msgBuffer, 'binary')
)
);
// :TODO: Validate integrity
// :TODO: Validated signing
//
// The outter PKCS#7 signed data must contain authenticated
// attributes for transactionID and senderNonce. We will use these
// in our reply back as part of the SCEP spec.
//
const oids = forge.pki.oids;
let origTransactionId = p7Message.rawCapture.authenticatedAttributes.find( attr => {
const oid = forge.asn1.derToOid(attr.value[0].value);
return ('2.16.840.1.113733.1.9.7' === oid); // transactionID
});
if(!origTransactionId) {
return reply('Invalid request payload').code(403);
}
origTransactionId = origTransactionId.value[1].value[0].value; // PrintableString
let origSenderNonce = p7Message.rawCapture.authenticatedAttributes.find( attr => {
const oid = forge.asn1.derToOid(attr.value[0].value);
return ('2.16.840.1.113733.1.9.5' === oid); // senderNonce
});
if(!origSenderNonce) {
return reply('Invalid request payload').code(403);
}
origSenderNonce = origSenderNonce.value[1].value[0].value; // OctetString
const p7EnvelopedData = forge.pkcs7.messageFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(new Buffer(p7Message.rawCapture.content.value[0].value[0].value, 'binary'), 'binary')
)
);
// decrypt using our key
p7EnvelopedData.decrypt(p7EnvelopedData.recipients[0], conf.serverConfig.caPrivateKey);
// p7EnvelopedData should contain a PKCS#10 CSR
const csrDataBuffer = new Buffer(p7EnvelopedData.content.getBytes(), 'binary');
const csr = forge.pki.certificationRequestFromAsn1(
forge.asn1.fromDer(
forge.util.createBuffer(csrDataBuffer, 'binary')
),
true // computeHash
);
//
// Create a new cert based on the CSR and sign it
//
// See https://github.com/digitalbazaar/forge/issues/154
//
const signedCert = forge.pki.createCertificate();
signedCert.serialNumber = Date.now().toString();
signedCert.validity.notBefore = new Date();
signedCert.validity.notAfter = new Date();
// expires one year from now (client should contact us before then to renew)
signedCert.validity.notAfter.setFullYear(signedCert.validity.notBefore.getFullYear() + 1);
signedCert.setSubject(csr.subject.attributes);
signedCert.setIssuer(conf.serverConfig.caCert.subject.attributes);
// :TODO: Really, this should come from requested extensions in the CSR
signedCert.setExtensions([
{
name : 'keyUsage',
digitalSignature : true,
keyEncipherment : true,
critical : true,
}
]);
signedCert.publicKey = csr.publicKey;
signedCert.sign(conf.serverConfig.caPrivateKey);
req.log( ['trace' ], { message : 'Signed CSR certificate', cert : forge.pki.certificateToPem(signedCert) } );
const degenerate = forge.pkcs7.createSignedData();
degenerate.addCertificate(signedCert);
degenerate.sign();
const enveloped = forge.pkcs7.createEnvelopedData();
// Recipient is the original requester cert
enveloped.addRecipient(p7Message.certificates[0]);
enveloped.content = forge.asn1.toDer(degenerate.toAsn1());
enveloped.encryptedContent.algorithm = forge.pki.oids['des-EDE3-CBC']; // We set this in GetCACaps
enveloped.encrypt();
// Package up everything in PKCS#7 signed (by us) data
const signed = forge.pkcs7.createSignedData();
signed.addSigner({
key : conf.serverConfig.caPrivateKey,
certificate : conf.serverConfig.caCert,
digestAlgorithm : forge.pki.oids.sha1,
authenticatedAttributes : [
{
type : forge.pki.oids.contentType,
value : forge.pki.oids.data
},
{
type: forge.pki.oids.messageDigest
},
{
type: forge.pki.oids.signingTime,
},
{
name : 'transactionID',
type : '2.16.840.1.113733.1.9.7',
rawValue : forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.PRINTABLESTRING,
false,
origTransactionId
),
},
{
name : 'messageType',
type : '2.16.840.1.113733.1.9.2',
rawValue : forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.PRINTABLESTRING,
false,
'3' // CertRep
),
},
{
name : 'senderNonce',
type : '2.16.840.1.113733.1.9.5',
rawValue : forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OCTETSTRING,
false,
forge.util.createBuffer(forge.random.getBytes(16)).bytes()
),
},
{
name : 'recipientNonce',
type : '2.16.840.1.113733.1.9.6',
rawValue : forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.OCTETSTRING,
false,
origSenderNonce),
},
{
name : 'pkiStatus',
type : '2.16.840.1.113733.1.9.3',
rawValue : forge.asn1.create(
forge.asn1.Class.UNIVERSAL,
forge.asn1.Type.PRINTABLESTRING,
false,
'0' // SUCCESS
),
}
]
});
signed.content = forge.asn1.toDer(enveloped.toAsn1());
signed.sign();
const signedDer = new Buffer(forge.asn1.toDer(signed.toAsn1()).getBytes(), 'binary');
return reply(signedDer).bytes(signedDer.length).type('application/x-pki-message');
} catch(e) {
req.log( ['error' ], { message : e.toString() } );
return reply('The CA could not validate the request').code(403);
}
}
This took a few days to get right. Hopefully it can be of help to someone!

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

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

Resources