Validate RRSIG with PHP using openssl - php

I'm trying to do a RRSIG validation, I'm trying to use the openssl lib in PHP. But I'm having a problem to pass the public key to the openssl_verify function.
This is a base code,
using the Net/DNS2 library to do a DNS query with DNSSEC option.
and get the DNSKEY and RRSIG.
<?php
require_once 'Net/DNS2.php';
$r = new Net_DNS2_Resolver(array('nameservers' => array('127.0.0.1')));
$r->dnssec = true;
try {
$result = $r->query('ip4afrika.nl', 'DNSKEY');
} catch(Net_DNS2_Exception $e) {
echo "::query() failed: ", $e->getMessage(), "\n";
die(); //
}
// print_r($result->answer);
$public_key_bin = base64_decode( $result->answer[0]->key ) ;
$public_key_str = $result->answer[0]->key; //echo $public_key_str; die();
// $public_key_res = openssl_x509_parse($public_key_bin);
$public_key_res = openssl_x509_read($public_key_str);
// $public_key_res = openssl_pkey_get_public($public_key_str);
while ($msg = openssl_error_string()) echo $msg . PHP_EOL;
I get this error messages,
when using:
$public_key_res = openssl_x509_read($public_key_str);
PHP Warning: openssl_x509_read(): supplied parameter cannot be
coerced into an X509 certificate! in /src/Net_DNS2-1.4.3/i.php on line
34 PHP Stack trace: PHP 1. {main}() /src/Net_DNS2-1.4.3/i.php:0 PHP
2. openssl_x509_read() /src/Net_DNS2-1.4.3/i.php:34 error:0906D06C:PEM routines:PEM_read_bio:no start line
so i tried adding the BEGIN/END headers
$public_key_str = '-----BEGIN CERTIFICATE-----' . PHP_EOL . $result->answer[0]->key . PHP_EOL . '-----END CERTIFICATE-----' ;
And got this error messages,
error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag
error:0D07803A:asn1 encoding routines:ASN1_ITEM_EX_D2I:nested asn1 error
error:0906700D:PEM routines:PEM_ASN1_read_bio:ASN1 lib
So it seems I'm feeding the function the wrong format, I'm still googling but any help would be welcome.
Eventually I like to validate the signature with:
openssl_verify($data, $signature, $public_key_res, 'RSA-SHA256');

Short answer:
If you just need the capability in PHP, you can just use https://github.com/metaregistrar/php-dnssec-validator .
Long Answer:
The reason you can't load the KEY data is because it's in a slightly different format. According to rfc3110:
Field Size
----- ----
exponent length 1 or 3 octets (see text)
exponent as specified by length field
modulus remaining space
Whereas RSA public keys are a bit more complex -- aside from the exponent and modulus, you need to prefix it with the correct OID as such (2nd answer).
After that the process is a bit gnarly:
Get the RRSIG record to get the signature and key tag (to determine which key to use)
Use the public keys from the correct DNSKEY RR to verify the signature against.
There's also a process described here (in python)

PHP_EOL is platform specific, not sure what platform you're testing this code on but try replacing the constant with '\n' explicitly. See if that helps.

Related

Decrypt a signature using openssl_public_decrypt

I'm trying to verify an external call to one of our endpoints, this endpoint is triggered by a third party, we receive a transaction data and a signature based on that transaction information, with that, we need to decrypt the signature and compare the result to verify the authenticity.
I'm trying to use openssl_public_decrypt to decrypt the signature using the provider's public key.
This is how I'm trying:
$signature = 'GcTtinhU0YgwGbZPtBwLdh+zdEe0w0W95TFPggeHMCjeDUBWgZfCZ6ZDRUk7DfT5BkKsbAi8/4o60Krcwz1JMdRjmsPf7vj33heVIB2PZJaf8eFR1jijLIsyl4vgH7BbbQ2I6kk6IcYXYWPVAHYRWxl1pJwOyNxZPr49fdW+hcw2zbpkEmj2114QBSiV6eHLowVYKLvpuiT8zLc6DN/wVzCYBuR/cg+CPHgYMeWFsuvu9J46hm6Hij00E68ldYAqVwImlmHPqfqvdEItg3Oi0ac4tXH2nCNgLPHcyU/H32NzTYC9iT1YZkoInqsU6Qv64vbU9lSMS91EQBEa5UQkUg==';
$pubKey = openssl_pkey_get_public('file://path/to/public.pem');
if( openssl_public_decrypt(base64_decode($signature), $data, $pubKey)){
echo $data;
}else{
echo 'Error';
}
I don't get any error but the $data value is not what I expect, is something like this
v_~�#&�W��q�&Ș�uQ���֔�
I'm sure I'm missing something but I can't find out what is it, due to the $data value looks like is encrypted.
The result that I expect from the decrypt is 167619085f7ed94026e357930b18dc011971f226c898ef7551cdf6ec9ad694cf this is the result of the following code
$canonical = 'c328e942-8be8-4104-abbe-048254f893dc|9687|2874.30|52409|BP1381|550bd8439cd1f41691671cdd4e8c6ae6';
$hashed = hash('sha256', $canonical);
That last part is how the provider generates the signature.
For the given example, canonic form is as follows:
cec4b9bf-5a39-4bd7-bc8b826ebc18208d|Internal_0005|12|39679|BP7610|947d589a40dece13c28f2b63c41ae451
We sign the response by hashing the canonic form with SHA-256 and encrypting the
resulting bytes with our private key.
RSA_ENCRYPT(SHA256(canonicForm), privkey.key)
To verify the payload, you must recalculate the canonic form and apply SHA-256 to the
result. The resulting value must be compared with the result of decrypting the signature
parameter with our public key.
Any hint would be appreciated.
perhaps post the public key and some valid test data so we can test ourselves?
anyway, v_~�#&�W��q�&Ș�uQ���֔� could be a valid signature, remember that SHA256 is 256 random bits, it's binary data, not ascii data, not hex, and not printable. SHA256 is also exactly 32 bytes long (256 bits, and 1 byte is 8 bits, and 256/8 is 32 bytes), so if you run var_dump(strlen($data)) after decryption, it should print 32, if it does not print 32, it implies they're using a padding scheme, try checking the strlen of both OPENSSL_PKCS1_PADDING and OPENSSL_NO_PADDING , when you get the correct padding scheme, strlen($data) after decryption should be int(32)
but my best guess is:
$signature = 'GcTtinhU0YgwGbZPtBwLdh+zdEe0w0W95TFPggeHMCjeDUBWgZfCZ6ZDRUk7DfT5BkKsbAi8/4o60Krcwz1JMdRjmsPf7vj33heVIB2PZJaf8eFR1jijLIsyl4vgH7BbbQ2I6kk6IcYXYWPVAHYRWxl1pJwOyNxZPr49fdW+hcw2zbpkEmj2114QBSiV6eHLowVYKLvpuiT8zLc6DN/wVzCYBuR/cg+CPHgYMeWFsuvu9J46hm6Hij00E68ldYAqVwImlmHPqfqvdEItg3Oi0ac4tXH2nCNgLPHcyU/H32NzTYC9iT1YZkoInqsU6Qv64vbU9lSMS91EQBEa5UQkUg==';
$canonical = 'c328e942-8be8-4104-abbe-048254f893dc|9687|2874.30|52409|BP1381|550bd8439cd1f41691671cdd4e8c6ae6';
$pubKey = openssl_pkey_get_public('file://path/to/public.pem');
if( openssl_public_decrypt(base64_decode($signature), $data, $pubKey)){
echo "signature decryption success! ";
if(hash_equals(hash("sha256",$canonical,true),$data)){
echo "checksum verification success!";
} else{
echo "checksum verification failed (after decryption was successful..)";
}
}else{
echo 'checksum decryption error';
}
but again, experiment with both
if( openssl_public_decrypt(base64_decode($signature), $data, $pubKey, OPENSSL_PKCS1_PADDING)){
and
if( openssl_public_decrypt(base64_decode($signature), $data, $pubKey, OPENSSL_NO_PADDING)){
1 of them is probably correct (and when it is correct, var_dump(strlen($data)) should print int(32) )

My php script does not decrypt return crypt in Sagepay Form version 3.00

I have moved my website to a new hosting provider where my Sagepay Form v3 script which receives the encrypted response is now failing.
At the previous hosting provider the script was working (php version there was 5.5.9) and the new hosting offers a choice from 5.4 to 6. At the first hosting provider the php version for a long time earlier was 5.2 (or maybe it was 5.3) and when they finally enforced a change to 5.5 it wrecked a lot of things in my website scripts which resulted in a very difficult period trying to fix them, which I achieved in the end.
One of those things was that the decrypt failed, just like it is doing again now. In that case I eventually fixed it by changing the decrypt line from:
$Decoded = DHclassInFunc::decryptAes($crypt,$EncryptionPassword);
to:
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
I had tried many other variations but only this last one worked.
So now the problem is back and I am completely at a loss. I have tried all of the previous variations but nothing works. Also the various php versions on offer at my new host.
My (LONG) question on the previous occasion was also posted here: see Website to Sagepay submit encryption code was working but now fails after server php upgrade
Can anyone suggest why this is failing this time round and what I can do to fix it?
EDIT 14/12/18 More info after investigation plus I am including more explanation and the full code from the two relevant scripts ------------------------
I made no progress and the website orders have had to be manually managed while the Sagepay return was not working. Now I have a little time so I am trying again.
I have now found that if I remove this line (below) on the "completed.php" page (the url to which the Sagepay response is directed) the script does not hang; however it is because it is that line which causes a fatal error.
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
Without the line and the resultant error the scipt is able to move on and call the the following page ("return.php") which then displays payment result information to the customer and also does other actions (such as sending the full order detail to our local - not on the internet - database).
However with the line removed the crypt in the url is not processed and therefore there are no values in the result variables which the completed.php page forwards to the return.php page.
This means that the $status variable is empty; in the return.php page this is evaluated as an error and therefore the customer is shown a message which says that there was an error and that no payment was taken - which is incorrect.
The lack of a "success" status value also means that the order in the web mysql database is not flagged as confirmed.
I had tried many other variations of the line to no avail (although the one given here worked before the website was moved to a new host).
The line does of course invoke the function in the class "DHclassInFunc" which is located in the functions.php file.
I am enclosing below the active code from of the two files, completed.php and functions.php
As far as I can work out the root problem is that the line
$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);
does not receive any value in 'crypt' and so there is no string for the function.php decrypt routine to work on resulting in the fatal error when that function is called: "PHP Fatal error: Class 'SagepayApiException' not found in /redacted/redacted/redacted.com/www/redacted/protx/functions.php on line 208"
I have added a line in the functions.php code as below:
echo '$strIn' . " string in with # should be here?";
in order to try to expose the value which is being passed to the function but it simple prints the name of the var, not the values from the url content which is in the address bar of the completed.php page when it receives the response from Sagepay - eg:
https://www.redacted.com/redacted/protx/completed.php?crypt=#ad6721a09c786829cd839586df0fe047ea0f0e9c791ddfe5d55b7175881aa4609ccfb4768a8b84dd9f259614d0edf0f03254a1967279693509e72190c8248cd56d1cefa713592f84eca4e8d7477ac89c9dd783b350a21766500c1c91fde3dbe5deb7887bea0e5c07e58274dec93224729f265730a4aecf5cf9c7216dad2b5eecc4d128e6c8389c1c9d5d297b7a10ccb53e37eae5b7a996a308c10f2d0edc0b41b6b38c6e56375a6421d110a0a3fe40cdfa2daa2fa6e0bf767204d209aa300d9f907ea686ee9a9dcc0992c14c325123ab53d7885bc6dc66eebf3c341002034fbce6277ccc6fbb8734c3cdab58dcd294d0a3a4430c7b091beed81fd97cadbf24b9149f9541e5d8e8c45a4e267fc0d14222c45963fe847ec12a9fedf05eba2a78caf769825046584b112d353d92d38aedc3cb086fc0c8250e20ef975dc377438b7c3a34c96cacba9ed1670b2af1bcd0945a5a0424c0532f23b0a6662db8198a2368d60ee3785f07826005593292154abe06abf55ff1d461b714e1fb53b5da3db1f21eb6b01169a2cf78d872de5ac96e41e088a7bf1e6f88aa8cc5c6b4bfd5d82f63
Regarding whether it might be a unicode / iso issue I can't see why this would result in an empty value in $strIn since this has not yet been processed at all, merely captured (or not?).
COMPLETED.php ---------------------
<?php
include "functions.php";
$Decoded = DHclassInFunc::decryptAes($_GET['crypt'],$EncryptionPassword);
$values = getToken($Decoded);
$VendorTxCode = $values['VendorTxCode'];
$Status = $values['Status'];
$VPSTxID = $values['VPSTxId'];
$TxAuthNo = $values['TxAuthNo'];
$AVSCV2 = $values['AVSCV2'];
$Amount = $values['Amount'];
// protocol 2.22 fields
$AddressResult = $values[ 'AddressResult' ];
$PostCodeResult = $values[ 'PostCodeResult' ];
$CV2Result = $values[ 'CV2Result' ];
$GiftAid = $values[ 'GiftAid' ];
$VBVSecureStatus = $values[ '3DSecureStatus' ];
$CAVV = $values[ 'CAVV' ];
// DH my all-in-one details var
$ResultDetails = $ResultDetails . "Vendor Code: " . $VendorTxCode . " - ";
$ResultDetails = $ResultDetails . "Status: " . $Status . " - ";
$ResultDetails = $ResultDetails . "VPS Transaction ID: " . $VPSTxID . " - ";
$ResultDetails = $ResultDetails . "Auth Num: " . $TxAuthNo . " - ";
$ResultDetails = $ResultDetails . "AVS / CV2 response: " . $TxAuthNo . " - ";
$ResultDetails = $ResultDetails . "Amount: " . $Amount . " - ";
$ResultDetails = $ResultDetails . "Address Result: " . $AddressResult . " - ";
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";
$ResultDetails = $ResultDetails . "CV2 Result: " . $CV2Result . " - ";
$ResultDetails = $ResultDetails . "GiftAid Result: " . $GiftAid . " - ";
$ResultDetails = $ResultDetails . "3DSecure Status: " . $VBVSecureStatus . " - ";
$ResultDetails = $ResultDetails . "CAVV Result: " . $CAVV . " - ";
$FindHyphen = strpos($VendorTxCode,'-');
$LastIdChar = $FindHyphen;
$MyOrderID = substr($VendorTxCode,0,$LastIdChar);
$StatusSave = $Status;
echo ' <FORM METHOD="POST" FORM NAME="GoToReturn" ACTION="../MXKart/return.php">'."\n";
echo ' <input type="hidden" name="response_code" value= "';
echo $Status;
echo '">'."\n";
echo ' <input type="hidden" name="order_number" value= "';
echo $MyOrderID;
echo '">'."\n";
echo ' <input type="hidden" name="secretword" value= "';
echo $secret_word;
echo '">'."\n";
//echo addslashes($ResultDetails);
echo ' <input type="hidden" name="response_reason_text" value= "';
echo $ResultDetails;
echo '">'."\n";
echo ' <input type="hidden" name="amount" value= "';
echo $Amount;
echo '">'."\n";
echo ' <input type="hidden" name="force" value= "';
echo $VendorTxCode;
echo '">'."\n";
$msg = "<br><strong>Getting payment result.... </strong> <br><br><h2 style=\"color:green;\">PLEASE WAIT AT THIS PAGE - do not close the page or move on. <br>There can be a delay of up to a minute so please be patient.</h2>";
echo $msg."\n";
echo '</FORM>'."\n";
echo '<script language="javascript">'."\n";
echo 'document.forms[0].submit();'."\n";
echo '</script>'."\n";
?>
FUNCTIONS.php ---------------------
<?
$VendorName="redacted";
$EncryptionPassword="redacted"; // LIVE server destination
//************ NEW CRYPT STUFF COPIED FRON SAGEPAY KIT util.php
//DH added class definition as shown in stackoverflow page - trying to fix error when run, on line static private function etc
class DHclassInFunc{
/**
* PHP's mcrypt does not have built in PKCS5 Padding, so we use this.
*
* #param string $input The input string.
*
* #return string The string with padding.
*/
static protected function addPKCS5Padding($input)
{
$blockSize = 16;
$padd = "";
// Pad input to an even block size boundary.
$length = $blockSize - (strlen($input) % $blockSize);
for ($i = 1; $i <= $length; $i++)
{
$padd .= chr($length);
}
return $input . $padd;
}
/**
* Remove PKCS5 Padding from a string.
*
* #param string $input The decrypted string.
*
* #return string String without the padding.
* #throws SagepayApiException
*/
static protected function removePKCS5Padding($input)
{
$blockSize = 16;
$padChar = ord($input[strlen($input) - 1]);
/* Check for PadChar is less then Block size */
if ($padChar > $blockSize)
{
throw new SagepayApiException('Invalid encryption string');
}
/* Check by padding by character mask */
if (strspn($input, chr($padChar), strlen($input) - $padChar) != $padChar)
{
throw new SagepayApiException('Invalid encryption string');
}
$unpadded = substr($input, 0, (-1) * $padChar);
/* Chech result for printable characters */
if (preg_match('/[[:^print:]]/', $unpadded))
{
throw new SagepayApiException('Invalid encryption string');
}
return $unpadded;
}
/**
* Encrypt a string ready to send to SagePay using encryption key.
*
* #param string $string The unencrypyted string.
* #param string $key The encryption key.
*
* #return string The encrypted string.
*/
static public function encryptAes($string, $key)
{
// AES encryption, CBC blocking with PKCS5 padding then HEX encoding.
// Add PKCS5 padding to the text to be encypted.
$string = self::addPKCS5Padding($string);
// Perform encryption with PHP's MCRYPT module.
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
/**
* Decode a returned string from SagePay.
*
* #param string $strIn The encrypted String.
* #param string $password The encyption password used to encrypt the string.
*
* #return string The unecrypted string.
* #throws SagepayApiException
*/
static public function decryptAes($strIn, $password)
{
echo '$strIn' . " string in with # should be here?";
$strIn = htmlspecialchars($strIn, ENT_COMPAT,'utf-8', true);
// HEX decoding then AES decryption, CBC blocking with PKCS5 padding.
// Use initialization vector (IV) set from $str_encryption_password.
$strInitVector = $password;
// Remove the first char which is # to flag this is AES encrypted and HEX decoding.
$hex = substr($strIn, 1);
// Throw exception if string is malformed
if (!preg_match('/^[0-9a-fA-F]+$/', $hex))
{
//DH added section to print result of decryption onto page for debugging
//$hex = "pseudo hex";
//echo "throw error at line 188";
// echo $hex;
throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);
// Perform decryption with PHP's MCRYPT module.
$stringReturn = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, $strIn, MCRYPT_MODE_CBC, $strInitVector);
return self::removePKCS5Padding($string);
}
}
/* The getToken function. **
** NOTE: A function of convenience that extracts the value from the "name=value&name2=value2..." VSP reply string **
** Works even if one of the values is a URL containing the & or = signs. */
function getToken($thisString) {
// List the possible tokens
$Tokens = array(
"Status",
"StatusDetail",
"VendorTxCode",
"VPSTxId",
"TxAuthNo",
"Amount",
"AVSCV2",
"AddressResult",
"PostCodeResult",
"CV2Result",
"GiftAid",
"3DSecureStatus",
"CAVV" );
// Initialise arrays
$output = array();
$resultArray = array();
// Get the next token in the sequence
for ($i = count($Tokens)-1; $i >= 0 ; $i--){
// Find the position in the string
$start = strpos($thisString, $Tokens[$i]);
// If it's present
if ($start !== false){
// Record position and token name
$resultArray[$i]->start = $start;
$resultArray[$i]->token = $Tokens[$i];
}
}
// Sort in order of position
sort($resultArray);
// Go through the result array, getting the token values
for ($i = 0; $i<count($resultArray); $i++){
// Get the start point of the value
$valueStart = $resultArray[$i]->start + strlen($resultArray[$i]->token) + 1;
// Get the length of the value
if ($i==(count($resultArray)-1)) {
$output[$resultArray[$i]->token] = substr($thisString, $valueStart);
} else {
$valueLength = $resultArray[$i+1]->start - $resultArray[$i]->start - strlen($resultArray[$i]->token) - 2;
$output[$resultArray[$i]->token] = substr($thisString, $valueStart, $valueLength);
}
}
// Return the ouput array
return $output;
}
// Randomise based on time
function randomise() {
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
?>
I am much in need of help to fix the problem in respect of the code or whether I am making mistakes in how I try to expose the value of the seemingly empty string, and therefore jumping to the wrong conclusion.
EDIT TUESDAY 18/12/18 ---------------
I have made some progress in that I have discovered the reason that the $_GET in the page "completed.php" was not obtaining any value at all from the page url in the return reply sent by Sagepay.
It was because the hosting platform's default php server settings only accepted up to 512 characters in the url; I was was able to change this to 2000 characters (see later comments) which fixed part the problem; the fatal error was gone but the decrypt still failed. However I could now debug because the functions now had data to work with and I could trace values in different parts of the script.
Unfortunately I am now completely at a loss in understanding the debug outputs - because in the first place I don't understand the decrypt functions at all despite searching for help.
The output seems to be reasonable as far as the decrypt line
$hex = substr($strIn, 1);
in "functions.php" which yields the content of the incoming crypt after the "#" has been stripped off.
But once the script moves to the line
$strIn = pack('H*', $hex);
it goes wrong because the output if the variable content now is littered with 'garbage' characters. I don't understand how the 'pack' works but I assume that the characters should all remain readable and therefore this is an encoding problem.
Link to image of a screenshot of the characters
The characters which display as a question mark within a black diamond in the image linked to above seem to be some of these these -
e? g!xh)̓G]/|CՖ'#]Ws܀͝Y?Ig#uQ*ߎ#KѦ
when I capture the text with a quick select-and-copy and then pass into a text editor.
But I don't know if the garbage characters are confined to those which are inserted by the 'pack' function and therefore the encoding mismatch is limited to just the function, rather than being an overall encoding issue with the submission and return data to and from Sagepay.
Unfortunately I am pretty bemused after a long long process of messing (trying everything) with encodings since the website move to the new host, changing scripts, headers, explicit encoding statements, script file encodings, php.ini encoding declarations, Mysql database encodings etc from the old (largely) ISO to utf_8. Mostly just trying to get rid of anomalous characters which are actually visible to users on the website. Squish one and you get a different one in its place.
So now my head throbs at the thought of tracking down how to deal with this if it is purely a encoding problem. Sagepay has told me that Form 3 is compatible with unicode but I know that this is contradicted by other advice I have received, and indeed my own experience having been through previous php and sagepay version changes at my old hosting provider.
There is no way that the website and database fundamentals are going to get changed back to ISO but if it is the case that I have to somehow let Sagepay alone dine on ISO, how can I do this most easily - what are the essentials?
The submit to Sagepay works just fine under utf-8, but do I have to change this to submit in ISO before I can specify ISO for the return which is where the real problem lies? And how best to force this ISO anyway - just for Sagepay - given that encoding often seem not to 'stick', a battleground of web technologies influencing the encoding.
On the other hand it would be great if it was just the 'pack' function that's out of kilter; and if there is an easy way or place to fix that. Can anyone advise please.
As implemented in the Omnipay Sage Pay driver (Omnipay Common v3.x) https://github.com/thephpleague/omnipay-sagepay/blob/master/src/Message/Form/CompleteAuthorizeRequest.php#L47
$crypt = $_GET['crypt'];
// Remove the leading '#' and decrypt the remainder into a query string.
$hexString = substr($crypt, 1);
// Last minute check to make sure we have data that looks sensible.
if (! preg_match('/^[0-9a-f]+$/i', $hexString)) {
throw new \Exception('Invalid "crypt" parameter; not hexadecimal');
}
// Decrypt the crypt string.
$queryString = openssl_decrypt(
hex2bin($hexString),
'aes-128-cbc',
$yourEncryptionKey,
OPENSSL_RAW_DATA,
$yourEncryptionKey
);
// Parse ...&VPSTxId={AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}&...
// into an array of values.
parse_str($queryString, $data);
var_dump($data);
/*
array(17) {
["VendorTxCode"]=>
string(19) "your-original-unique-id"
["VPSTxId"]=>
string(38) "{AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}"
["Status"]=>
string(2) "OK"
["StatusDetail"]=>
string(40) "0000 : The Authorisation was Successful."
["TxAuthNo"]=>
string(6) "376048"
["AVSCV2"]=>
string(24) "SECURITY CODE MATCH ONLY"
["AddressResult"]=>
string(10) "NOTMATCHED"
["PostCodeResult"]=>
string(10) "NOTMATCHED"
["CV2Result"]=>
string(7) "MATCHED"
["GiftAid"]=>
string(1) "0"
["3DSecureStatus"]=>
string(10) "NOTCHECKED"
["CardType"]=>
string(4) "VISA"
["Last4Digits"]=>
string(4) "0006"
["DeclineCode"]=>
string(2) "00"
["ExpiryDate"]=>
string(4) "1220"
["Amount"]=>
string(5) "99.99"
["BankAuthCode"]=>
string(6) "999777"
}
*/
PHP 7 no longer supports the older encryption/decryption functions that the official Sage Pay library (and many plugins based off that old code) use. Use openssl functions instead.
Everything returned in $data will be ASCII (it will return well-defined IDs and codes only, and no user-entered data). I don't believe it will contain any extended ASCII characters, so can be treated as UTF-8 without any conversion if desired.

openssl_dh_compute_key returns false

I'm trying to compute the shared secret for ECDH (Elliptic Curve Diffie Hellman) using PHP.
Assume I have someone's public key.
$clientPublickey = "BOLcHOg4ajSHR6BjbSBeX_6aXjMu1V5RrUYXqyV_FqtQSd8RzdU1gkMv1DlRPDIUtFK6Nd16Jql0eSzyZh4V2uc";
I generate my keys using OpenSSL.
exec('openssl ecparam -genkey -name prime256v1 -noout -out example-ecc.pem');
$private = openssl_pkey_get_private("file://example-ecc.pem");
Using the following code, I get false as value for $sharedSecret.
$sharedSecret = openssl_dh_compute_key(base64_decode($clientPublickey), $private);
openssl_error_string() doesn't return any error.
I've var_dumped openssl_pkey_get_details($private) and I verified it was created properly.
How do I use openssl_dh_compute_key()?
https://www.openssl.org/docs/manmaster/crypto/DH_compute_key.html describes that function as being for (non-EC) Diffie Hellman. You would need ECDH_compute_key (which I don't know if PHP exposes).
Though https://wiki.openssl.org/index.php/Elliptic_Curve_Diffie_Hellman recommends using the EVP_PKEY wrappers instead of the low level routines. But, again, I can't say what support PHP has.
Quite an old post, but still..
I think your public key is not formatted in Base64, but in URL-safe-Base64.
See: https://en.wikipedia.org/wiki/Base64#URL_applications
Furthermore, you can create the keys also from PHP now. Using openssl_pkey_new().
See: https://www.php.net/manual/en/function.openssl-pkey-new.php
openssl_dh_compute_key() supports ECDH only in PHP 8.1.0, with OpenSSL 3.0.0. Before PHP 8.1.0, openssl_dh_compute_key() always returned false.
Starting from PHP 7.3, there is openssl_pkey_derive() which derives a shared secret from a set of public key and private key. It works either with DH or EC keys. The example code given for this function is simpler than the example given for openssl_dh_compute_key(), which generate a public/private DH keypair using the command line.
$private = openssl_pkey_get_private("-----BEGIN PRIVATE KEY-----
MIICJgIBADCCARcGCSqGSIb3DQEDATCCAQgCggEBAJLxRCaZ933uW+AXmabHFDDy
upojBIRlbmQLJZfigDaSA1f9YOTsIv+WwVFTX/J1mtCyx9uBcz0Nt2kmVwxWuc2f
VtCEMPsmLsVXX7xRUFLpyX1Y1IYGBVXQOoOvLWYQjpZgnx47Pkh1Ok1+smffztfC
0DCNt4KorWrbsPcmqBejXHN79KvWFjZmXOksRiNu/Bn76RiqvofC4z8Ri3kHXQG2
197JGZzzFXHadGC3xbkg8UxsNbYhVMKbm0iANfafUH7/hoS9UjAVQYtvwe7YNiW/
HnyfVCrKwcc7sadd8Iphh+3lf5P1AhaQEAMytanrzq9RDXKBxuvpSJifRYasZYsC
AQIEggEEAoIBAGwAYC2E81Y1U2Aox0U7u1+vBcbht/OO87tutMvc4NTLf6NLPHsW
cPqBixs+3rSn4fADzAIvdLBmogjtiIZoB6qyHrllF/2xwTVGEeYaZIupQH3bMK2b
6eUvnpuu4Ytksiz6VpXBBRMrIsj3frM+zUtnq8vKUr+TbjV2qyKR8l3eNDwzqz30
dlbKh9kIhZafclHfRVfyp+fVSKPfgrRAcLUgAbsVjOjPeJ90xQ4DTMZ6vjiv6tHM
hkSjJIcGhRtSBzVF/cT38GyCeTmiIA/dRz2d70lWrqDQCdp9ArijgnpjNKAAulSY
CirnMsGZTDGmLOHg4xOZ5FEAzZI2sFNLlcw=
-----END PRIVATE KEY-----
");
$public = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----
MIICJDCCARcGCSqGSIb3DQEDATCCAQgCggEBAJLxRCaZ933uW+AXmabHFDDyupoj
BIRlbmQLJZfigDaSA1f9YOTsIv+WwVFTX/J1mtCyx9uBcz0Nt2kmVwxWuc2fVtCE
MPsmLsVXX7xRUFLpyX1Y1IYGBVXQOoOvLWYQjpZgnx47Pkh1Ok1+smffztfC0DCN
t4KorWrbsPcmqBejXHN79KvWFjZmXOksRiNu/Bn76RiqvofC4z8Ri3kHXQG2197J
GZzzFXHadGC3xbkg8UxsNbYhVMKbm0iANfafUH7/hoS9UjAVQYtvwe7YNiW/Hnyf
VCrKwcc7sadd8Iphh+3lf5P1AhaQEAMytanrzq9RDXKBxuvpSJifRYasZYsCAQID
ggEFAAKCAQAiCSBpxvGgsTorxAWtcAlSmzAJnJxFgSPef0g7OjhESytnc8G2QYmx
ovMt5KVergcitztWh08hZQUdAYm4rI+zMlAFDdN8LWwBT/mGKSzRkWeprd8E7mvy
ucqC1YXCMqmIwPySvLQUB/Dl8kgau7BLAnIJm8VP+MVrn8g9gghD0qRCgPgtEaDV
vocfgnOU43rhKnIgO0cHOKtw2qybSFB8QuZrYugq4j8Bwkrzh6rdMMeyMl/ej5Aj
c0wamOzuBDtXt0T9+Fx3khHaowjCc7xJZRgZCxg43SbqMWJ9lUg94I7+LTX61Gyv
dtlkbGbtoDOnxeNnN93gwQZngGYZYciu
-----END PUBLIC KEY-----
");
echo bin2hex(openssl_pkey_derive($public, $private));
For other PHP 7 releases, there are pure PHP implementations like phpecc/phpecc, which can generate a shared encryption key using code similar to the following one. (It's one of the library examples.)
use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Primitives\GeneratorPoint;
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
use Mdanter\Ecc\Util\NumberSize;
// ECDSA domain is defined by curve/generator/hash algorithm,
// which a verifier must be aware of.
$adapter = EccFactory::getAdapter();
$generator = EccFactory::getNistCurves()->generator384();
$useDerandomizedSignatures = true;
$derPub = new DerPublicKeySerializer();
$pemPub = new PemPublicKeySerializer($derPub);
$pemPriv = new PemPrivateKeySerializer(new DerPrivateKeySerializer($adapter, $derPub));
# These .pem and .key are for different keys
$alicePriv = $pemPriv->parse(file_get_contents(__DIR__ . '/../tests/data/openssl-secp256r1.pem'));
$bobPub = $pemPub->parse(file_get_contents(__DIR__ . '/../tests/data/openssl-secp256r1.1.pub.pem'));
$exchange = $alicePriv->createExchange($bobPub);
$shared = $exchange->calculateSharedKey();
echo "Shared secret: " . gmp_strval($shared, 10).PHP_EOL;
# The shared key is never used directly, but used with a key derivation function (KDF)
$kdf = function (GeneratorPoint $G, \GMP $sharedSecret) {
$adapter = $G->getAdapter();
$binary = $adapter->intToFixedSizeString(
$sharedSecret,
NumberSize::bnNumBytes($adapter, $G->getOrder())
);
$hash = hash('sha256', $binary, true);
return $hash;
};
$key = $kdf($generator, $shared);

InApp Billing Verifying Order on Web Server PHP

I'm using a simple PHP script to verify Android order to parse download for the customer.
$receipt = $_GET['purchaseData'];
$billInfo = json_decode($receipt,true);
$signature = $_GET['dataSignature'];
$public_key_base64 = "xxxxxxxxxxxxxxxx";
$key = "-----BEGIN PUBLIC KEY-----\n".
chunk_split($public_key_base64, 64,"\n").
'-----END PUBLIC KEY-----';
$key = openssl_get_publickey($key);
$signature = base64_decode($signature);
//$result = openssl_verify($billInfo, $signature, $key);
$result = openssl_verify($receipt, $signature, $key);
if (0 === $result) {
echo "0";
} else if (1 !== $result) {
echo "1";
} else {
echo "Hello World!";
}
//added the var_dump($result); as asked by A-2-A
var_dump($result);
result is 0int(0)
I made a real order through the App after I published it and when trying to validate the order I get "0" as result.
I tried direct HTTP access
https://domain.com/thankyou.php?purchaseData={"packageName":"com.example.app","orderId":"GPA.1234-5678-1234-98608","productId":"product","developerPayload":"mypurchasetoken","purchaseTime":1455346586453,"purchaseState":0,"developerPayload":"mypurchasetoken","purchaseToken":"ggedobflmccnemedgplmodhp...."}&dataSignature=gwmBf...
I'm keeping the first of the question because my result is still a guess. After further investigation I think it's the signature not being read in a nice clean way as sent by google.
The signature=gwmBfgGudpG5iPp3L0OnepNlx while the browser is reading it as ƒ ~®v‘¹ˆúw
How is it possible to let it be read in the right way?
To verify the signature you want to make sure of the following:
INAPP_PURCHASE_DATA is not mutated in any way. Any encoding or escaping changes will result in a invalid verification. The best way to ensure it gets to your server intact is to base64 encoded it.
INAPP_DATA_SIGNATURE also must remain intact, it should already base64 encoded so sending that to your server should not be a problem.
openssl_verify expects both data and signature arguments to be in their raw state, so base64 decode before verifying.
It also takes signature_alg as the last argument, in this case sha1WithRSAEncryption should work as should the default, but if in doubt try a few other sha1 algorithms to see which ones work.
My best guess why it's not working for you right now is that you're not receiving the INAPP_PURCHASE_DATA on your server in the same condition that it was received on the app. This Stackoverflow question had the same problem.

How to calculate wsse nonce?

I am getting an error while trying to send a soap request (soapCall) to the server.
Fatal error: Uncaught SoapFault exception: [ns1:InvalidSecurity] An error was discovered processing the <wsse:Security> header
I need to send the ws-security header
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken-1" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:Username>userID</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">passwd</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ZTQ3YmJjZmM1ZTU5ODg3YQ==</wsse:Nonce>
<wsu:Created>2013-07-05T19:55:36.458Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
After a lot of research I think the issue I got is the nonce didnt meet the requirement. As I am making up the soap header looks like the example I got. The only unknown element is to calculating this nonce...
From the example nonce I got, its a set of 24 numbers + alphabet + special character
Something like this
ZTQ3YmJjZmM1ZTU5ODg3YQ==
But however, I am not too sure how do you calculate the wsse nonce from php...is there any standard?
the code I had
$nonce = sha1(mt_rand());
Result
dabddf9dbd95b490ace429f7ad6b55c3418cdd58
which is something completely different than the example...and I believe this is the reason why this code is not working.
So I am doing more research and now I am using this
$NASC = substr(md5(uniqid('the_password_i_am _using', true)), 0, 16);
$nonce = base64_encode($NASC);
Result
NzJlMDQ4OTAyZWIxYWU5ZA==
Now, it looks similar to the example but I still getting that error showed from the beginning.
Can someone give me a hand please?
some further testing with soapUI.
same userID and passwd, set the passwordtype to passwordtext
and it is working.
is anyone know how do the soapUI calculate the nonce? or have any idea how soapUI passing the ws-security?
try something like this
string usn = "MyUsername";
string pwd = "MyPassword";
DateTime created = DateTime.Now.ToUniversalTime();
var nonce = getNonce();
string nonceToSend = Convert.ToBase64String(Encoding.UTF8.GetBytes(nonce));
string createdStr = created.ToString("yyyy-MM-ddTHH:mm:ssZ");
string passwordToSend = GetSHA1String(nonce + createdStr + pwd);
and functions:
protected string getNonce()
{
string phrase = Guid.NewGuid().ToString();
return phrase;
}
protected string GetSHA1String(string phrase)
{
SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
byte[] hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(phrase));
string test = Convert.ToString(hashedDataBytes);
return Convert.ToBase64String(hashedDataBytes);
}
As uniqid() is based on a Pseudo-Random Number Generator, it does not provide enough entropy. Siehe Insufficient Entropy For Random Values
$nonce = base64_encode( bin2hex( openssl_random_pseudo_bytes( 16 ) ) );
If you don't have the OpenSSL module try this fallback to mcrypt_create_iv() see:
https://github.com/padraic/SecurityMultiTool/blob/master/library/SecurityMultiTool/Random/Generator.php
Microsoft defines the WS-Security nonce as:
The nonce is 16 bytes long and is passed along as a base64 encoded value.
The following PHP code generates a code that follows the Microsoft .Net WS-Security Standard:
$prefix = gethostname();
$nonce = base64_encode( substr( md5( uniqid( $prefix.'_', true)), 0, 16));
Some testing with no $prefix was successful, but the production version of this code uses the $prefix with no authentication problems encountered so far. The original version of this nonce code came from the following library (with a modification to the number of characters to return in substr):
http://code.ronoaldo.net/openemm/src/e25a2bad5aa7/webservices/WSSESoapClient.php#cl-267

Categories