In the command line, I can verify that certificate is issued by trusted CA by typing
openssl verify mycert.pem
How do I do same with PHP's OpenSSL library? PHP has an openssl_verify function which takes many extra parameters:
data , string $signature , mixed $pub_key_id
How do I repeat that simple command line operation with corresponding PHP function?
This is pretty easy with phpseclib, a pure PHP X509 implementation. eg.
<?php
include('File/X509.php');
$x509 = new File_X509();
$x509->loadCA('...');
$x509->loadX509('...');
echo $x509->validateSignature() ? 'valid' : 'invalid';
?>
See http://phpseclib.sourceforge.net/x509/compare.html#verify for more info
I'm not sure what is your cert but I found this function openssl_x509_checkpurpose.
http://php.net/manual/en/function.openssl-x509-checkpurpose.php
http://www.php.net/manual/en/openssl.cert.verification.php
openssl_x509_checkpurpose($cert, $purpose, $cainfo, $untrustedfile);
$cainfo is array with path to CA files.
In PHP the openssl_verify function is not used to verify that a certificate is issued by a trusted CA but used to verify that a signature is the right one for some data...
EDIT : How to verify CA with PHP :
You can't only verify that subject and issuer name are matching, so by only using OpenSSL in Php it doesnt seem like totally possible
check this out:
Verify SMTP in PHP
<?php
$server = "smtp.gmail.com"; // Who I connect to
$myself = "my_server.example.com"; // Who I am
$cabundle = '/etc/ssl/cacert.pem'; // Where my root certificates are
// Verify server. There's not much we can do, if we suppose that an attacker
// has taken control of the DNS. The most we can hope for is that there will
// be discrepancies between the expected responses to the following code and
// the answers from the subverted DNS server.
// To detect these discrepancies though, implies we knew the proper response
// and saved it in the code. At that point we might as well save the IP, and
// decouple from the DNS altogether.
$match1 = false;
$addrs = gethostbynamel($server);
foreach($addrs as $addr)
{
$name = gethostbyaddr($addr);
if ($name == $server)
{
$match1 = true;
break;
}
}
// Here we must decide what to do if $match1 is false.
// Which may happen often and for legitimate reasons.
print "Test 1: " . ($match1 ? "PASSED" : "FAILED") . "\n";
$match2 = false;
$domain = explode('.', $server);
array_shift($domain);
$domain = implode('.', $domain);
getmxrr($domain, $mxhosts);
foreach($mxhosts as $mxhost)
{
$tests = gethostbynamel($mxhost);
if (0 != count(array_intersect($addrs, $tests)))
{
// One of the instances of $server is a MX for its domain
$match2 = true;
break;
}
}
// Again here we must decide what to do if $match2 is false.
// Most small ISP pass test 2; very large ISPs and Google fail.
print "Test 2: " . ($match2 ? "PASSED" : "FAILED") . "\n";
// On the other hand, if you have a PASS on a server you use,
// it's unlikely to become a FAIL anytime soon.
// End of maybe-they-help-maybe-they-don't checks.
// Establish the connection
$smtp = fsockopen( "tcp://$server", 25, $errno, $errstr );
fread( $smtp, 512 );
// Here you can check the usual banner from $server (or in general,
// check whether it contains $server's domain name, or whether the
// domain it advertises has $server among its MX's.
// But yet again, Google fails both these tests.
fwrite($smtp,"HELO $myself\r\n");
fread($smtp, 512);
// Switch to TLS
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);
stream_context_set_option($smtp, 'ssl', 'capture_peer_cert', true);
stream_context_set_option($smtp, 'ssl', 'cafile', $cabundle);
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);
$opts = stream_context_get_options($smtp);
if (!isset($opts["ssl"]["peer_certificate"]))
$secure = false;
else
{
$cert = openssl_x509_parse($opts["ssl"]["peer_certificate"]);
$names = '';
if ('' != $cert)
{
if (isset($cert['extensions']))
$names = $cert['extensions']['subjectAltName'];
elseif (isset($cert['subject']))
{
if (isset($cert['subject']['CN']))
$names = 'DNS:' . $cert['subject']['CN'];
else
$secure = false; // No exts, subject without CN
}
else
$secure = false; // No exts, no subject
}
$checks = explode(',', $names);
// At least one $check must match $server
$tmp = explode('.', $server);
$fles = array_reverse($tmp);
$okay = false;
foreach($checks as $check)
{
$tmp = explode(':', $check);
if ('DNS' != $tmp[0]) continue; // candidates must start with DNS:
if (!isset($tmp[1])) continue; // and have something afterwards
$tmp = explode('.', $tmp[1]);
if (count($tmp) < 3) continue; // "*.com" is not a valid match
$cand = array_reverse($tmp);
$okay = true;
foreach($cand as $i => $item)
{
if (!isset($fles[$i]))
{
// We connected to www.example.com and certificate is for *.www.example.com -- bad.
$okay = false;
break;
}
if ($fles[$i] == $item)
continue;
if ($item == '*')
break;
}
if ($okay)
break;
}
if (!$okay)
$secure = false; // No hosts matched our server.
}
if (!$secure)
die("failed to connect securely\n");
print "Success!\n";
// Continue with connection...
?>
Related
I'm facing a problem with RFC2253 non-compliant X509SubjectName and X509IssuerName in the signed XML by Chilkat PHP Extension, CkXmlDSigGen.
The original certificate subject contains the "2.5.4.97" DN:
Certificate subject example
The original certificate issuer contains the "2.5.4.97" DN:
Certificate issuer example
Both are translated to the signed XML as "OrganizationID" DN. And this cause a problem on the validator system side - e.g. "Signature error: X509IssuerName '..., OrganizationID=xyz, ...' is not RFC 2253 compliant."
Is there any possibility to keep original certificate subject / issuer?
My current code:
<?php
include("chilkat_9_5_0.php");
$success = true;
$xmlToSign = new CkXml();
$xmlToSign->LoadXmlFile('wrk/xml01.xml');
$gen = new CkXmlDSigGen();
$gen->put_SigLocation('soapenv:Envelope|soapenv:Body|trn:prfData');
$gen->put_SigLocationMod(0);
$gen->put_SigId('pSignature');
$gen->put_SigNamespacePrefix('ds');
$gen->put_SigNamespaceUri('http://www.w3.org/2000/09/xmldsig#');
$gen->put_SignedInfoCanonAlg('EXCL_C14N');
$gen->put_SignedInfoDigestMethod('sha256');
$object1 = new CkXml();
$object1->put_Tag('xades:QualifyingProperties');
$object1->AddAttribute('Target','#pSignature');
$object1->AddAttribute('xmlns:xades','http://uri.etsi.org/01903/v1.3.2#');
$object1->UpdateAttrAt('xades:SignedProperties',true,'Id','SignedProperties');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningTime','TO BE GENERATED BY CHILKAT');
$object1->UpdateAttrAt('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:CertDigest|ds:DigestMethod',true,'Algorithm','http://www.w3.org/2001/04/xmlenc#sha256');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:CertDigest|ds:DigestValue','TO BE GENERATED BY CHILKAT');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:IssuerSerial|ds:X509IssuerName','TO BE GENERATED BY CHILKAT');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:IssuerSerial|ds:X509SerialNumber','TO BE GENERATED BY CHILKAT');
$gen->AddObject('',$object1->getXml(),'','');
$gen->AddSameDocRef('signedData','sha256','','','');
$gen->AddObjectRef('SignedProperties','sha256','EXCL_C14N','','http://uri.etsi.org/01903#SignedProperties');
$cert = new CkCert();
$success = $cert->LoadPfxFile('path_to_cert.p12','passphrase');
if ($success != true) {
print $cert->lastErrorText() . "\n";
exit;
}
$gen->SetX509Cert($cert,true);
$gen->put_KeyInfoType('X509Data');
$gen->put_X509Type('IssuerSerial,SubjectName,Certificate');
$sbXml = new CkStringBuilder();
$xmlToSign->GetXmlSb($sbXml);
$gen->put_Behaviors('IndentedSignature,ForceAddEnvelopedSignatureTransform,DnReverseOrder ');
$success = $gen->CreateXmlDSigSb($sbXml);
if ($success != true) {
print $gen->lastErrorText() . "\n";
exit;
}
$success = $sbXml->WriteFile('wrk/signedXml.xml','utf-8',false);
?>
i want to validate email address is valid or not through smtp
I can't validate emails in codeigniter.
this is Error
fwrite(): send of 34 bytes failed with errno=10054 An existing
connection was forcibly closed by the remote host.
This method check whether the email actually exist or not
i have got two same error in this code in different lines
function isValidEmail($email){
$result=false;
# BASIC CHECK FOR EMAIL PATTERN WITH REGULAR EXPRESSION
if(!preg_match('/^[_A-z0-9-]+((\.|\+)[_A-z0-9-]+)*#[A-z0-9-]+(\.[A-z0-9-]+)*(\.[A-z]{2,4})$/',$email))
return $result;
# MX RECORD CHECK
list($name, $domain)=explode('#',$email);
if(!checkdnsrr($domain,'MX'))
return $result;
# SMTP QUERY CHECK
$max_conn_time = 30;
$sock='';
$port = 25;
$max_read_time = 5;
$users=$name;
# retrieve SMTP Server via MX query on domain
$hosts = array();
$mxweights = array();
getmxrr($domain, $hosts, $mxweights);
$mxs = array_combine($hosts, $mxweights);
asort($mxs, SORT_NUMERIC);
#last fallback is the original domain
$mxs[$domain] = 100;
$timeout = $max_conn_time / count($mxs);
# try each host
while(list($host) = each($mxs)) {
#connect to SMTP server
if($sock = fsockopen($host, $port, $errno, $errstr, (float) $timeout)){
stream_set_timeout($sock, $max_read_time);
break;
}
}
# did we get a TCP socket
if($sock) {
$reply = fread($sock, 2082);
preg_match('/^([0-9]{3}) /ims', $reply, $matches);
$code = isset($matches[1]) ? $matches[1] : '';
if($code != '220') {
# MTA gave an error...
return $result;
}
# initiate smtp conversation
$msg="HELO ".$domain;
fwrite($sock, $msg."\r\n");
$reply = fread($sock, 2082);
# tell of sender
$msg="MAIL FROM: <".$name.'#'.$domain.">";
fwrite($sock, $msg."\r\n");
$reply = fread($sock, 2082);
#ask of recepient
$msg="RCPT TO: <".$name.'#'.$domain.">";
fwrite($sock, $msg."\r\n");
$reply = fread($sock, 2082);
#get code and msg from response
preg_match('/^([0-9]{3}) /ims', $reply, $matches);
$code = isset($matches[1]) ? $matches[1] : '';
if($code == '250') {
#you received 250 so the email address was accepted
$result=true;
}elseif($code == '451' || $code == '452') {
#you received 451 so the email address was greylisted
#_(or some temporary error occured on the MTA) - so assume is ok
$result=true;
}else{
$result=false;
}
#quit smtp connection
$msg="quit";
fwrite($sock, $msg."\r\n");
# close socket
fclose($sock);
}
return $result;
}
$email='test1221s#gmail.com';
if(isValidEmail($email))
echo "**** EMAIL EXISTS ****";
else
echo "**** NOT A VALID EMAIL ****";
This method check whether the email actually exist or not
You can't do that, and the other side doesn't want to talk to you anymore because you tried.
In the past, crawlers would harvest email addresses by asking the server if user#domain.com exists. Then user1#domain.com. Then user2#domain.com, etc.
They would then end up with a list of valid users to spam.
Since then, mail servers have become much less open and "chatty" and won't respond to these queries and will in fact ban your address after a number of failures.
All you can do is send the actual email and handle the bounce if it's un-deliverable. If you insist on "checking" email addresses to see if they're good, you'll find that you soon have a blacklisted IP address.
I'm trying to extract song title from live mp3 streams using SC protocol. The php script works fine with some IPs and ports, however with some IPs and ports I cannot get required headers from the response to determine the meta-block frequency, therefore I cannot find the location of the song title in the stream. Here's my code:
<?php
while(true)
{
//close warning messages (re-open for debugging)
error_reporting(E_ERROR | E_PARSE);
//create and connect socket with the parameters entered by the user
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
echo "Establishing connection to the given adress...\n";
$fp = fsockopen($argv[1], $argv[2], $errno, $errstr, 10);
if($fp)
{
echo "Connection established.\n";
$result = socket_connect($sock, $argv[1], $argv[2]);
//prepare request
$request = "GET / HTTP/1.1\r\n";
$request .= "Icy-MetaData: 1\r\n\r\n";
//send request
socket_write($sock,$request,strlen($request));
//set sentinel boolean value's initial value
$headers = true;
//put the segment to be parsed into a string variable
$l = socket_read($sock,2048);
$meta = "";
$streamurl = "";
$checkContentType = false;
//Parsing metadata frequency and streamurl from response's headers.
foreach(preg_split("/((\r?\n)|(\r\n?))/", $l) as $line)
{
if(!(strpos($line, "metaint:") === false))
{
$meta = $line;
}
if(!(strpos($line, "icy-url:") === false))
{
$streamurl = $line;
}
if(!strpos($line, "audio/mpeg") === false)
{
$checkContentType = true;
}
}
echo $l;
//Checking if the content of the stream is mpeg or not
if($checkContentType)
{
$pos = strpos($meta, ":");
$interval = intval(substr($meta,$pos+1));
$pos = strpos($streamurl, ":");
$streamurl = substr($streamurl, $pos+1);
$flag = false;
//initialize bytecount to 0
$bytecount = 0;
//Extracting song title using SC protocol
while($headers)
{
$l = socket_read($sock,PHP_NORMAL_READ);
$bytecount++;
if($bytecount == $interval )
{
$headers = false;
$flag = true;
}
if($flag)
{
$len = ord($l);
}
}
//Determining length variable
$len = $len * 16;
$string = socket_read($sock,$len);
$pos2 = strpos($string, "'") + 1;
$pos3 = strpos($string, ";",$pos2) -1;
$songtitle = substr($string, $pos2, ($pos3-$pos2));
//Formatting the log entry
$finalstr = "[".date("c")."]"."[".$streamurl."]".$songtitle."\n";
echo "logged".$finalstr;
//finalize connection
socket_close($sock);
//Writing the requested info to a log file
file_put_contents("log.txt", $finalstr,FILE_APPEND | LOCK_EX);
//waiting 5 minutes
echo "Logging next entry in five minutes. \n";
sleep(300);
}
else
{
echo "Content of the stream is not suitable.\n";
exit;
}
}
else
{
echo "Unable to connect to the given ip and port.\n Exiting...\n";
socket_close($sock);
exit;
}
}
?>
I've never tried to access shoutcast programatically but I've run streaming audio servers in the past. There are actually two different flavours of shoutcast server and I would guess your program is trying to talk to one and these broken servers are the other type.
From the post READING SHOUTCAST METADATA FROM A STREAM:
Turns out that SHOUTcast and Icecast (two of the most popular server
applications for streaming radio) are supposed to be compatible, but
the response message from each server is slightly different.
Full details about the shoutcast protocol: Shoutcast Metadata Protocol
I am working on below things:
Generate CSR(Certificate Signing Request)
Upload SSL Certificates
To generate SSL certificate I am using something like:
$privkey = openssl_pkey_new();
$csr = openssl_csr_new($dn, $privkey);
$sscert = openssl_csr_sign($csr, null, $privkey, $days);
openssl_csr_export($csr, $csrout);
openssl_pkey_export($privkey, $pkeyout, $_POST['password']);
openssl_pkey_export_to_file($privkey, "<path/to/store/server.key>");
openssl_csr_export_to_file($csr, "/tmp/".<domain-name>.".csr");
Now using that CSR request, I am able to generate(domain-name.cer),(DigitalCert.cer).
Now once I upload this(.cer) certificates, I need to verify those certificates.
Reason: Someone generated these certificates on say "a.com" and tries to upload on "b.com". this should not happen, so I want to validate the uploaded SSL certificates.
In PHP, we have
$ok = openssl_verify($data, $signature, $pubkeyid);
but i am not able to get what things would be treated as $data, $signature and $pubkeyid based on the above certificate generation process.
Check this out:
Verify SMTP in PHP
<?php
$server = "smtp.gmail.com"; // Who I connect to
$myself = "my_server.example.com"; // Who I am
$cabundle = '/etc/ssl/cacert.pem'; // Where my root certificates are
// Verify server. There's not much we can do, if we suppose that an attacker
// has taken control of the DNS. The most we can hope for is that there will
// be discrepancies between the expected responses to the following code and
// the answers from the subverted DNS server.
// To detect these discrepancies though, implies we knew the proper response
// and saved it in the code. At that point we might as well save the IP, and
// decouple from the DNS altogether.
$match1 = false;
$addrs = gethostbynamel($server);
foreach($addrs as $addr)
{
$name = gethostbyaddr($addr);
if ($name == $server)
{
$match1 = true;
break;
}
}
// Here we must decide what to do if $match1 is false.
// Which may happen often and for legitimate reasons.
print "Test 1: " . ($match1 ? "PASSED" : "FAILED") . "\n";
$match2 = false;
$domain = explode('.', $server);
array_shift($domain);
$domain = implode('.', $domain);
getmxrr($domain, $mxhosts);
foreach($mxhosts as $mxhost)
{
$tests = gethostbynamel($mxhost);
if (0 != count(array_intersect($addrs, $tests)))
{
// One of the instances of $server is a MX for its domain
$match2 = true;
break;
}
}
// Again here we must decide what to do if $match2 is false.
// Most small ISP pass test 2; very large ISPs and Google fail.
print "Test 2: " . ($match2 ? "PASSED" : "FAILED") . "\n";
// On the other hand, if you have a PASS on a server you use,
// it's unlikely to become a FAIL anytime soon.
// End of maybe-they-help-maybe-they-don't checks.
// Establish the connection
$smtp = fsockopen( "tcp://$server", 25, $errno, $errstr );
fread( $smtp, 512 );
// Here you can check the usual banner from $server (or in general,
// check whether it contains $server's domain name, or whether the
// domain it advertises has $server among its MX's.
// But yet again, Google fails both these tests.
fwrite($smtp,"HELO $myself\r\n");
fread($smtp, 512);
// Switch to TLS
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);
stream_context_set_option($smtp, 'ssl', 'capture_peer_cert', true);
stream_context_set_option($smtp, 'ssl', 'cafile', $cabundle);
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);
$opts = stream_context_get_options($smtp);
if (!isset($opts["ssl"]["peer_certificate"]))
$secure = false;
else
{
$cert = openssl_x509_parse($opts["ssl"]["peer_certificate"]);
$names = '';
if ('' != $cert)
{
if (isset($cert['extensions']))
$names = $cert['extensions']['subjectAltName'];
elseif (isset($cert['subject']))
{
if (isset($cert['subject']['CN']))
$names = 'DNS:' . $cert['subject']['CN'];
else
$secure = false; // No exts, subject without CN
}
else
$secure = false; // No exts, no subject
}
$checks = explode(',', $names);
// At least one $check must match $server
$tmp = explode('.', $server);
$fles = array_reverse($tmp);
$okay = false;
foreach($checks as $check)
{
$tmp = explode(':', $check);
if ('DNS' != $tmp[0]) continue; // candidates must start with DNS:
if (!isset($tmp[1])) continue; // and have something afterwards
$tmp = explode('.', $tmp[1]);
if (count($tmp) < 3) continue; // "*.com" is not a valid match
$cand = array_reverse($tmp);
$okay = true;
foreach($cand as $i => $item)
{
if (!isset($fles[$i]))
{
// We connected to www.example.com and certificate is for *.www.example.com -- bad.
$okay = false;
break;
}
if ($fles[$i] == $item)
continue;
if ($item == '*')
break;
}
if ($okay)
break;
}
if (!$okay)
$secure = false; // No hosts matched our server.
}
if (!$secure)
die("failed to connect securely\n");
print "Success!\n";
// Continue with connection...
?>
This works for me
$crt_md5=exec('openssl x509 -noout -modulus -in /path/to/domain.crt/ | openssl md5 | sed "s/^.* //"');
$key_md5=exec('openssl rsa -noout -modulus -in /path/to/server.key | openssl md5 | sed "s/^.* //"');
if($crt_md5 != $key_md5){
echo 'BAD';
}
else{
echo "GOOD";
}
sed "s/^.* //" - will remove (stdin)= thing from the output, so that
you get exact md5 string
this is how i do it...
system('openssl x509 -noout -modulus -in '.$crt.' | openssl md5', $crt_md5);
system('openssl rsa -noout -modulus -in '.$key.' | openssl md5', $key_md5);
if($crt_md5 != $key_md5){
echo 'BAD';
}
Try openssl_x509_check_private_key( $crt, $key ) it returns boolean
ref http://php.net/manual/en/function.openssl-x509-check-private-key.php
WARNING: openssl_x509_check_private_key will not work for some case.
Example:
SSL certificate like this:
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
xxxx
xxxx
This certificate does not end with -----END CERTIFICATE----- , but it can still pass the check of this function. It will return true to tell you that it is correct, but it is not actually. If you upload this certificate to your application, such as Nginx , Nginx will tell you an error.
This doesn't seem to be an error that only appears in PHP. If you check with the openssl function on the command line, it will tell you the same result.
So I think the best way is that you need to check whether the paragraphs of the certificate are complete.
After confirming that the format is correct, use this function to verify the certificate and private key.
i'm trying android in app billing v3 verifying on my remote php server.
but, it seems something is wrong at my codes.
i think this openssl_verify function is problem.
result is always failed!
i can't find what first parameter to verify with openssl_verify. actually, i 'm confuse what's reasonable format to place at first parameter :(
could you help me to solve it?
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key); // original // failed
belows full test codes.
<?php
$responseCode = 0;
$encoded='{
"orderId":"12999763169054705758.1111111111111",
"packageName":"com.xxx.yyy",
"productId":"test__100_c",
"purchaseTime":1368455064000,
"purchaseState":0,
"purchaseToken":"tcmggamllmgqiabymvcgtfsj.AO-J1OwoOzoFd-G-....."
}';
$data = json_decode($encoded,true);
$signature = "tKdvc42ujbYfLl+3sGdl7RAUPlNv.....";
$publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2kMri6mE5+.....";
$key = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($publicKey, 64, "\n") . "-----END PUBLIC KEY-----";
$key = openssl_get_publickey($key);
if (false === $key) {
exit("error openssl_get_publickey");
}
var_dump($key);
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key); // original // failed
//$result = openssl_verify($data, base64_decode($signature), $key); // failed
//$result = openssl_verify($encoded, base64_decode($signature), $key); // failed
//$result = openssl_verify(base64_decode($data["purchaseToken"]), base64_decode($signature), $key); // failed
//$result = openssl_verify(base64_decode($signature),$data["purchaseToken"], $key,OPENSSL_ALGO_SHA512 ); // failed
if ($result == 1) {
echo "good";
} elseif ($result == 0) {
echo "bad";
} else {
echo "error";
}
echo($result);
thanks :)
You are passing the wrong $data value into openssl_verify(). This value should be the full JSON string you get from Google Play, not the purchase token inside it. It is important that the JSON string is untouched, as even if you were to add a space or newlines to it, the signature would no longer work.
All you need to do in your code above is to change this line:
$result = openssl_verify($data["purchaseToken"], base64_decode($signature), $key);
to
$result = openssl_verify($data, base64_decode($signature), $key);
And you should get a success, assuming you're using the correct public key and the JSON purchase string is valid. I'm pretty sure your JSON string is not the original string from Google however, as the ones from Google do not contain newlines. It will be one long line of JSON text. Make sure that's what you are passing to openssl_verify().