I have a mobile app with some inapp items that users can purchase. Once users buy some inapp product the app sends the JSON receipt to my server for online verification against my Google developer public key (stored in the server).
The app sends signature and data (aka the receipt) to the server:
$signature = 'E2dxlmSe0d45eJpN4FKSUxNPYXM5A1zohpVL60Hd+5jd43j4YMhBlVRLwFeDaBKZnkJ39rYYesWoOu8Z5ysczAIiQO7Myko7UJYVYKvB5GqM8a0iEDjCdCpSRSqLUmaEHKwUJFfjcgw1K5L2gM/m3u8l7Jy25IB+HFVIikO50jiy8SMRh7S+s6PgEAXqG6K6vTpuTC5ECweuQ45VTdb0jNyWOzEW/I1nA5fAB/mmp5j3B6k7nN81NMh/3oUJHba/wWGlbkWtItmDU6/jMdpd1CVViNBhKe0ktwnSRz3XF607/AfZM6JteOKhC6TquWhVNuWpKJWdJbP7Q+RVS0YKog==';
$data = '{"orderId":"GPA.xxxx-xxxx-xxxx-xxxxx","packageName":"xxx.xxx.xxx","productId":"xxx","purchaseTime":1508881024560,"purchaseState":0,"purchaseToken":"didpmjkaldaddakgfabdohdj.AO-J1Ozqb8hZAa-_FLd-sQJgXhwruU3tVEYU0sqhlgXHb8I9wI35xDeQFgFI0Zpoaurw4Ry7zahymvge1U0WlEqqvvAKvwAo0Wk1MtawzAiqVdy2RTvwFGo"}';
Here's the PHP code I'm using for signature verification:
$pkey = "...";
$apkey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($pkey, 64, "\n")."-----END PUBLIC KEY-----";
$pubkeyid = openssl_get_publickey($apkey);
$ok = openssl_verify($data, $signature, $pubkeyid);
openssl_free_key($pubkeyid);
echo $ok;
and of course it doesn't work. The OpenSSL function returns 0 (instead of 1 for OK). According to the online documentation https://developer.android.com/google/play/billing/billing_integrate.html
the thing I have to check is INAPP_PURCHASE_DATA which is receipt. Here an example from the doc:
'{
"orderId":"GPA.1234-5678-9012-34567",
"packageName":"com.example.app",
"productId":"exampleSku",
"purchaseTime":1345678900000,
"purchaseState":0,
"developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
"purchaseToken":"opaque-token-up-to-1000-characters"
}'
Which is exactly what my app sends. Now, since signature verification requires bit perfect data, how I'm supposed to "send" such string to the OpenSSL function? On the doc the data has line breaks and indentation, mine is the very same JSON structure but recorded as a plain string without line breaks nor indentation. It's the same data JSON-wise but very different from a cryptographic signature verification point of view. Can someone please explain how to do it?
Solved the issue:
The data has to be a plain string with no indentation nor line breaks
signature has to be base64 decoded before passing it to the OpenSSL function
So instead of this:
$ok = openssl_verify($data, $signature, $pubkeyid);
I have to do this:
$ok = openssl_verify($data, base64_decode($signature), $pubkeyid);
Related
I am attempting to hash and sign user data on iOS (14.4), send that to my server, and have the server verify the hash and the signature with a previously uploaded public key (sent on keypair generation during user creation). It seems a number of people have run into issues with this, but all of the answers I've been able to find are very old, don't factor in using Apple's Secure Enclave, or revolve around signing and verifying on the same iOS device.
The general workflow is: User creates an account on iOS, and a random keypair is created on the device with the private key remaining in the Secure Enclave, while the public key is converted to ASN.1 format, PEM encoded and uploaded to the server. When the user later signs data, the data is JSONEncoded, hashed with sha512, and signed by their private key in the Secure Enclave. This is then packaged into a base64EncodedString payload, and sent to the server for verification. The server first verifies the hash using openssl_digest and then checks the signature using openssl_verify.
I have been unable to get the openssl_verify method to successfully verify the signature. I have also attempted using the phpseclib library (to get more insight into why the verification fails) without success. I understand phpseclib uses the openssl library if it is available, but even if this is disabled, phpseclib's internal verification fails because the resulting values after modulus do not match. Interestingly, phpseclib converts the public key to what looks like PKCS8 formatting with a large amount of padding.
It appears the public key is being parsed and loaded properly by openssl, as a proper reference is being created prior to verification. However, since the private key is opaque (residing in the Secure Enclave) I don't have a way to externally "check" how the signatures themselves are generated/encoded or if the same signature would be created outside of the iOS device. I'm wondering if I have an encoding error, or if external verification is possible with keys generated in the Secure Enclave.
iOS Public Key Upload method- I am using CryptoExportImportManager which converts the raw bytes to DER, adds the ASN.1 header, and adds the BEGIN and END key tags.
public func convertPublicKeyForExport() -> String?
{
let keyData = SecKeyCopyExternalRepresentation(publicKey!, nil)! as Data
let keyType = kSecAttrKeyTypeECSECPrimeRandom
let keySize = 256
let exportManager = CryptoExportImportManager()
let exportablePEMKey = exportManager.exportECPublicKeyToPEM(keyData, keyType: keyType as String,
keySize: keySize)
return exportablePEMKey
}
An example of what one of the public keys looks like after upload
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf16tnH8YPjslaacdtdde4wRQs0PP
zj/nWgBC/JY5aeajHhbKAf75t6Umz6vFGBsdgM/AFMkeB4n2Qi96ePNjFg==
-----END PUBLIC KEY-----
let encoder = JSONEncoder()
guard let payloadJson = try? encoder.encode(["user_id": "\(user!.userID)", "random_id": randomID])
else
{
onCompletion(nil, NSError())
print("Failed creating data")
return
}
let hash = SHA512.hash(data: payloadJson)
guard let signature = signData(payload: payloadJson, key: (user?.userKey.privateKey)!) else
{
print("Could not sign data payload")
onCompletion(nil, NSError())
return
}
let params = Payload(
payload_hash: hash.hexString,
payload_json: payloadJson,
signatures: ["user": [
"signature": signature.base64EncodedString(),
"type": "ecdsa-sha512"
]]
)
let encoding = try? encoder.encode(params).base64EncodedString()
The sign data function is pretty close to Apple's documentation code, but I'm including it for reference
private func signData(payload: Data, key: SecKey) -> Data?
{
var error: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(key,
SecKeyAlgorithm.ecdsaSignatureMessageX962SHA512,
payload as CFData, &error)
else
{
print("Signing payload failed with \(error)")
return nil
}
print("Created signature as \(signature)")
return signature as Data
}
I actually stumbled upon the solution while doing additional research and experimentation while writing this question. The problem of course had nothing to do with the keys or algorithms, and everything to do with the way Apple hashes data objects.
I had discovered a similar problem when trying to determine why my hashes were not matching on the server-side vs the ones created on the iOS device. The user JSONEncoded data is hashed and signed as a base64Encoded data object, but unknown to me (and not in any documentation I could discover) iOS decodes the Data object and hashes the raw object, and re-encodes it (since this is opaque code it's possible this is not precisely accurate, but the result is the same). Therefore when checking the hash on the user data, I had to first base64decode the object, and then perform the hash. I had assumed that Apple would sign the encoded object as is (in order to not contaminate its integrity), but in fact, when Apple creates the digest before signing, it hashes the decoded raw object and creates a signature on the raw object.
Therefore the solution was to again base64decode the object before sending it to the openssl_verify function.
Checking the hash on the server
public function is_hash_valid($payload) {
$server_payload_hash = openssl_digest(base64_decode($payload["payload_json"]), "SHA512");
$client_payload_hash = $payload["payload_hash"];
if ($client_payload_hash != $server_payload_hash) {
return false;
}
return true;
}
Verifying the signature on the server
function is_signature_valid($data, $signature, $public_key) {
$public_key = openssl_get_publickey($public_key);
$ok = openssl_verify(base64_decode($data), base64_decode($signature), $public_key, "SHA512");
if ($ok === 1) {
return true;
} else {
return false;
}
}
After discovering this, and verifying that openssl_verify and phpseclib's verify function worked correctly, I almost considered deleting the question entirely but realized that if I had discovered a question similar to this in my research, it might have saved me a good deal of time. Hopefully to anyone else that has a similar issue, this will prove helpful.
I would like to achieve something I thought was pretty straighforward:
A) My VB.NET client (ideally targetting Framework 4.0) sends a text string to my Apache/PHP Server via an HTTPS POST request.
B) My Server responds with a Signature of that text string.
Private key used by the Server is always the same, and public key used by Client is already embeded within the source code.
After investigating and reading through a lot of documentation, I came up with the following strategy and have two questions:
Is my strategy efficient?
The code provided below does not work (.VerifyData returns FALSE). What am I missing?
Strategy
Server Side
Apache/PHP: Because that is the only server language I am familiar
with, but I could switch if recommended.
OpenSSL: Because I use PHP
PEM files: Because I use OpenSSL
RSA key size is 2048 bits: Recommended minimum in 2019
Algorythm is SHA256: Because everyone seems to use that one
Header text/plain, UTF8: Why not?
Client Side
VB.Net Framework 4.0 Client Profile: Because I want to maximise legacy (VSTO 2013)
System.Security.Cryptography.RSACryptoServiceProvider
PEM public key info loaded via XML string
HTTPS should be TLS1.0 or higher: Because I target .Net Framework 4.0 (TLS1.1 is recommended if possible)
Source Code
Server Side (generate .pem key files, only once)
<?
// Create new Keys Pair
$new_key_pair = openssl_pkey_new(array(
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
));
//Save Private Key
openssl_pkey_export($new_key_pair, $private_key_pem, "my passphrase to protect my private key; add random characters like $, ?, #, & or ! for improved security");
file_put_contents('private_key.pem', $private_key_pem);
//Save Public Key
$details = openssl_pkey_get_details($new_key_pair);
$public_key_pem = $details['key'];
file_put_contents('public_key.pem', $public_key_pem);
?>
Server Side (target of POST requests)
<?
header('Content-Type: text/plain; charset=utf-8');
// Verify connection is secure
if(empty($_SERVER['HTTPS']) || $_SERVER['HTTPS']=="off")
{
echo "Unauthorized Access";
exit;
}
// Data to Sign
$data = base64_decode(file_get_contents('php://input'));
//Load Private Key
$private_key_pem = openssl_pkey_get_private('file:///path/protected/by/dotHtaccess/private_key.pem', "my passphrase to protect my private key; add random characters like $, ?, #, & or ! for improved security");
//Create Signature
openssl_sign($data, $signature, $private_key_pem, OPENSSL_ALGO_SHA256);
echo base64_encode($signature);
?>
Client Side
imports System.Diagnostics
Sub mySignatureTest()
Dim oURI As Uri = New Uri("https://www.example.com/targetpage.php")
Dim sData As String = "myStringToSign"
Dim sResponse As String
'# Get POST request Response
Using oWeb As New System.Net.WebClient()
Try
System.Net.ServicePointManager.SecurityProtocol = System.Net.ServicePointManager.SecurityProtocol And Not System.Net.ServicePointManager.SecurityProtocol.Ssl3 'Override defautl Security Protocols: Prohibit SSL3
oWeb.Encoding = Encoding.UTF8
Debug.Print("SSL version is " & System.Net.ServicePointManager.SecurityProtocol.ToString)
Debug.Print("Sending " & sData)
Debug.Print("To " & oURI.ToString)
Debug.Print("Encoding is " & oWeb.Encoding.ToString)
sResponse = oWeb.UploadString(oURI, "POST", Convert.ToBase64String(Encoding.UTF8.GetBytes(sData)))
Debug.Print("Server reponse = " & sResponse)
Catch ex As Exception
MsgBox("Connection with server failed: " & ex.ToString, vbCritical + vbOKOnly, "Add-In")
End Try
End Using
'#Verify RSA SHA256 Signature
Dim sDataToSign As String = sData
Dim sSignatureToVerify As String = sResponse
Using myRSA As New System.Security.Cryptography.RSACryptoServiceProvider
'XML format obtain from PEM file by hand copy/paste here: https://superdry.apphb.com/tools/online-rsa-key-converter
myRSA.FromXmlString("<RSAKeyValue><Modulus>SomeLongBase64StringHere</Modulus><Exponent>SomeShortBase64StringHere</Exponent></RSAKeyValue>")
Debug.Print("Signature verification = " & myRSA.VerifyData(Encoding.UTF8.GetBytes(sDataToSign), _
System.Security.Cryptography.CryptoConfig.MapNameToOID("SHA256"), _
Convert.FromBase64String(sSignatureToVerify)).ToString)
End Using
End Sub
Once question 2) has been resolved, I hope we can let this code evolve so that it provides a good but yet simple example of how to implement .NET Client verification of OpenSSL Server signature. I was unable to locate a simple and clear example on the Internet.
This can typically be used for licensing purposes where a Server would provide a file containing an expiration date as well as a signature of that date, in order for the Client App to confirm this expiration date was not altered for example by the Owner of the Client computer.
The following PHP function verifies that the $data string was signed using $key to create the $signature:
<?php
$result = openssl_verify( $data , $signature , $key , OPENSSL_ALGO_SHA1 );
?>
Is there an equivalent PHP function where I can get the original $data string if I have the correct $signature and $key
I think OP had a misconception about the signing process that I understand because I had it too.
The short answer is: no, there is no such PHP function, because it is impossible.
The digital signature is not just the original message ($data) encrypted with a Private Key.
It's the original message hashed into a "message digest" and then encrypted using a Private Key.
If you were to decrypt the signature with the matching public key, you would actually obtain the "message digest".
However there is no way to obtain the original message from the "message digest" because the hashing process is one way by design.
Verifying the signature is the process of hashing $data with the same hashing function and comparing it to the "message digest" obtained from decrypting the signature with the Public Key.
I guess that's what php function openssl_verify does.
This page helped me understand:
http://www.youdzone.com/signature.html
$data IS the original $data string, that's what the openssl_verify call does - verify that $data is unchanged, using $signature and $key
If you're asking if a $signature can be used to generate the data, then no - even if you have the private key, you still cannot recreate the data.
Imagine if you have a physical document (like a Will), which is signed, and a signature card. While the signature card ($key)allows you to verify the signature is authentic ($signature), the signature cannot be used to recreate the original content of the document (say, for example, if ink is spilled across the top of the document, but not across the signature line) - even if you're the original signer of the document. It can only be used to authenticate THAT document (Will) was, in fact, signed (legally binding).
Digital signatures are a bit more complicated from a math perspective, and provide some subtly different mechanisms for authentication, but in this case the result is the same - a signature cannot be used to recreate an original document, even if you have the signature card ($key), and even if you have the private key.
I'm making Android Application with In-App Purchases. On Android Developer Center page I see that I must verify purchase data (json) with signature. I trying to use PHP tool from Google Code for this, but validation failed. First fail be that this library want from me not json (as I understand), but some plain text with fields, joined with : and |. It split this plain string to get packageName and validate it too. I commented this part of code, because next part more interesting:
$result = openssl_verify($responseData, base64_decode($signature),
$this->_publicKey, self::SIGNATURE_ALGORITHM);
//openssl_verify returns 1 for a valid signature
if (0 === $result) {
return false;
} else if (1 !== $result) {
require_once 'RuntimeException.php';
throw new AndroidMarket_Licensing_RuntimeException('Unknown error verifying the signature in openssl_verify');
}
where $responseData is my purchase json, self::SIGNATURE_ALGORITHM is OPENSSL_ALGO_SHA1, $this->_publicKey is:
$key = self::KEY_PREFIX . chunk_split($publicKey, 64, "\n") . self::KEY_SUFFIX;
$key = openssl_get_publickey($key);
if (false === $key) {
require_once 'InvalidArgumentException.php';
throw new AndroidMarket_Licensing_InvalidArgumentException('Please pass a Base64-encoded public key from the Market portal');
}
$this->_publicKey = $key;
where public key is base64 public key, like described:
Note:To find the public key portion of this key pair, open your application's
details in the Developer Console, then click on Services & APIs, and look at the
field titled Your License Key for This Application.
But such verification is fail. I read that API 3 is new (Dec 2012), and many other articles and tutorials isn't correspond to it. What I need to change to correct this verification?
This code using SHA1, but on Android Developer Center page (first link) described that public key is RSA with X.509... Any ideas?
UPD: While trying to make server always say 'purchase is ok' and add all purchases to database, find that this error is my fail. I take json to server in base64, since on server i base64_decode it in two different places, so I breaking it. This library works in part of code that use openssl to validate json. Previos version, as I understand, just validate package name; this may be easy rewrited to read productId from json.
In an attempt to follow some of the security guidelines for in-app purchase here:
http://developer.android.com/guide/market/billing/billing_best_practices.html
I am trying to do signature validation on a server instead of in the app iteself. I would ideally like to use the php openssl libraries and it looks like code such as the following should work:
<?php
// $data and $signature are assumed to contain the data and the signature
// fetch public key from certificate and ready it
$fp = fopen("/src/openssl-0.9.6/demos/sign/cert.pem", "r");
$cert = fread($fp, 8192);
fclose($fp);
$pubkeyid = openssl_get_publickey($cert);
// state whether signature is okay or not
$ok = openssl_verify($data, $signature, $pubkeyid);
if ($ok == 1) {
echo "good";
} elseif ($ok == 0) {
echo "bad";
} else {
echo "ugly, error checking signature";
}
// free the key from memory
openssl_free_key($pubkeyid);
?>
I replace signature with the base64 decoded signature string in the app purchase bundle and the use the data from the same bundle. The public key needs to be in PEM format and I added the BEGIN and END tokens and some line breaks.
My problem is that I can not get this PHP code to successfully verify the data/signature and I do not know what needs to change to get it to work correctly.
If I use openssl, create a private and public key, create a signature for the same data using sha1 and run it through the above php code, it works fine and validate successfully.
Here is how I use OpenSSL:
openssl genrsa -out private.pem
openssl rsa -in private.pem -pubout -out public.pem
then I use the private.pem and some php code to generate a signature:
...
openssl_sign($data, $signature, $pkeyid);
...
Does anyone have any working sample php code with server side validation of in-app signatures?
I could just run the equivalent java code that is in the sample application, and that seems to work ok, but I would like to use php directly if possible.
I've written a library for verifying Android Market licensing responses and it's available on Google Code.
It just takes a few lines of PHP to verify a license, and the formatting of keys and OpenSSL stuff is taken care of for you.
Is it possible to use cURL in your PHP script, rather than the stuff built into PHP streams. I've used them before, and have found the problems more rare, and the error messages more verbose.