1

I am trying to verify receipt from Windows Store using PHP.

I am using following code:

    <?php
    include('Crypt/RSA.php');
    include('File/X509.php');
    $xml_str ='<Receipt Version="1.0" ReceiptDate="2012-08-30T23:08:52Z" CertificateId="b809e47cd0110a4db043b3f73e83acd917fe1336" ReceiptDeviceId="4e362949-acc3-fe3a-e71b-89893eb4f528">
        <ProductReceipt Id="6bbf4366-6fb2-8be8-7947-92fd5f683530" ProductId="Product1" PurchaseDate="2012-08-30T23:08:52Z" ExpirationDate="2012-09-02T23:08:49Z" ProductType="Durable" AppId="55428GreenlakeApps.CurrentAppSimulatorEventTest_z7q3q7z11crfr" />
        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
                <Reference URI="">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                    <DigestValue>Uvi8jkTYd3HtpMmAMpOm94fLeqmcQ2KCrV1XmSuY1xI=</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>TT5fDET1X9nBk9/yKEJAjVASKjall3gw8u9N5Uizx4/Le9RtJtv+E9XSMjrOXK/TDicidIPLBjTbcZylYZdGPkMvAIc3/1mdLMZYJc+EXG9IsE9L74LmJ0OqGH5WjGK/UexAXxVBWDtBbDI2JLOaBevYsyy+4hLOcTXDSUA4tXwPa2Bi+BRoUTdYE2mFW7ytOJNEs3jTiHrCK6JRvTyU9lGkNDMNx9loIr+mRks+BSf70KxPtE9XCpCvXyWa/Q1JaIyZI7llCH45Dn4SKFn6L/JBw8G8xSTrZ3sBYBKOnUDbSCfc8ucQX97EyivSPURvTyImmjpsXDm2LBaEgAMADg==</SignatureValue>
        </Signature>
    </Receipt>';

    if( !$xml = simplexml_load_string( $xml_str ) )
    {
        echo 'Unable to load XML string<br />';
    }
    else
    {
        print 'XML String loaded successfully<br />';
    }


    $ch = curl_init();

    //set the url, number of POST vars, POST data
    curl_setopt($ch,CURLOPT_URL, "https://lic.apps.microsoft.com/licensing/certificateserver/?cid=".$xml["CertificateId"]);


    curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $responseFromServer = curl_exec($ch);
    //echo $responseFromServer;
    echo '<br/>';

    $x509 = new File_X509();
    $cert=$x509->loadX509($responseFromServer);
    print_r($cert);
    echo $x509->getPublicKey();

    echo '<br/>';
    //close connection
    curl_close($ch);

  $signatureInfo2='<SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <DigestValue>Uvi8jkTYd3HtpMmAMpOm94fLeqmcQ2KCrV1XmSuY1xI=</DigestValue>
            </Reference>
        </SignedInfo>';
$data     = $xml->Signature->SignatureValue;
echo $data;
echo '<br/>';
$dom = new DOMDocument();
$dom->loadXML($signatureInfo2);
$canonicalized = $dom->C14N(TRUE, FALSE);

echo $canonicalized;
echo '<br/>';
$rsa = new Crypt_RSA();
$key = $x509->getPublicKey();

$key1 = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnK+P74KmRbczKst4ztFx 4wVDceo+2U1xJzaS5dlUns1UAPSitkZb66FyoWDFFHSacPrcZtZqov1uw/UDmE6t XvNxi4VgvSEYfzkpkmLdHpIFSwfonMkR93baWHCebLKVNobj3+CPzXNOjrl5TLA/ TFOFIPSAQ9h0gwRKroRkaMVeuGLhB+OuOaAdeC5RGstPiWZZCmf5lYcf7Hc0gX63 WtV/wpHO0joJ00jN3fw5zuQysFdlmJ/u4v6wanuP6KeiKkDKz6R8npvUp8votMYl DAPtSMJF9IbNILxzOsw8MEzA4k2qWwsvS55jMeuaDKueoYbEMnSxJqrqvJVWFAxMywIDAQAB';
if($rsa->loadKey($key1))
{ // public key
    $rsa->setPublicKey();
    $rsa->setHash('sha256');    
    echo $rsa->verify($canonicalized, $data) ? 'verified' : 'unverified';
}
?>

I am following this link. But I am always getting "bad" as output.

Can somebody tell me what I am doing wrong here?

Thanks

Community
  • 1
  • 1
Ashwani K
  • 7,880
  • 19
  • 63
  • 102
  • Maybe try doing base64_decode on `$xml->Signature->SignatureValue`? – neubert Sep 16 '13 at 19:58
  • I have tried it, but it is not working. Actually my openssl.php in eclipse does not have OPENSSL_ALGO_SHA256. May this can be an issue. I am sure how to get OPENSSL_ALGO_SHA256 in openssl.php in Core Libraries of project in eclipse. – Ashwani K Sep 17 '13 at 11:07
  • Oh... in that case maybe try phpseclib - a pure PHP RSA implementation. Here's an example of how to use it with sha256: http://pastebin.com/4698PPnQ – neubert Sep 17 '13 at 14:41
  • Thanks Neubert, but this is also not working. I have updated the question with ur suggestion. – Ashwani K Sep 17 '13 at 16:55
  • Try doing `$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);` before the signature verification. – neubert Sep 17 '13 at 18:26
  • Also, `openssl_get_publickey(openssl_x509_read($responseFromServer))` can be replaced with phpseclib too. eg. `$x509->loadX509($responseFromServer); $x509->getPublicKey();` assuming you've done `include('File/X509.php')` – neubert Sep 17 '13 at 18:27
  • Updated as suggested, still no success. I am using php 5.4.10. – Ashwani K Sep 17 '13 at 18:53
  • `base64_encode($xml_str)` probably isn't necessary. That said, thinking about it, maybe try xmlseclib (https://code.google.com/p/xmlseclibs/). XML, in particular, requires cananoclization, which phpseclib doesn't do. ie. an XML signature should be the same regardless of how you indented the tags. Although that might not be without problems either as that one uses openssl and not phpseclib.. :/ – neubert Sep 17 '13 at 19:06
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/37546/discussion-between-ashwani-k-and-neubert) – Ashwani K Sep 17 '13 at 19:16
  • As u suggested, I saw XMlseclibs code and modified the code in the question. However even now I am getting "unverified" as result. – Ashwani K Sep 17 '13 at 21:36
  • What if you try http://php.net/manual/en/domnode.c14n.php ? – neubert Sep 18 '13 at 15:24
  • @AshwaniK did you solve your problem? I also had to use phpseclib, yet no luck so far – Tertium Apr 25 '14 at 12:29
  • No :(, did not get it working – Ashwani K May 13 '14 at 06:50

2 Answers2

3

I managed to verify WP8 IAP receipt using xmlseclibs library. Windows Store is not much different.

Also, you need php curl enabled.

do {
    $doc = new DOMDocument();

    $xml = $_POST['receipt_data']; // your receipt xml here!

    // strip unwanted chars - IMPORTANT!!!
    $xml = str_replace(array("\n","\t", "\r"), "", $xml);
    //some (probably mostly WP8) receipts have unnecessary spaces instead of tabs
    $xml = preg_replace('/\s+/', " ", $xml);
    $xml = str_replace("> <", "><", $xml);

    $doc->loadXML($xml);
    $receipt = $doc->getElementsByTagName('Receipt')->item(0);
    $certificateId = $receipt->getAttribute('CertificateId');

    $ch = curl_init("https://lic.apps.microsoft.com/licensing/certificateserver/?cid=$certificateId");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

    $publicKey = curl_exec($ch);
    $errno = curl_errno($ch);
    $errmsg = curl_error($ch);
    curl_close($ch);

    if ($errno != 0) {
        $verifyFailed = true;
        break;
    }

    // Verify xml signature
    require('./xmlseclibs.php');
    $objXMLSecDSig = new XMLSecurityDSig();
    $objDSig = $objXMLSecDSig->locateSignature($doc);
    if (!$objDSig) {
        $verifyFailed = true;
        break;
    }
    try {
        $objXMLSecDSig->canonicalizeSignedInfo();
        $retVal = $objXMLSecDSig->validateReference();
        if (!$retVal) {
            throw new Exception("Error Processing Request", 1);
        }
        $objKey = $objXMLSecDSig->locateKey();
        if (!$objKey) {
            throw new Exception("Error Processing Request", 1);
        }
        $key = NULL;
        $objKeyInfo = XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig);
        if (! $objKeyInfo->key && empty($key)) {
            $objKey->loadKey($publicKey);
        }
        if (!$objXMLSecDSig->verify($objKey)) {
            throw new Exception("Error Processing Request", 1);
        }
    } catch (Exception $e) {
        $verifyFailed = true;
        break;
    }

    $productReceipt = $doc->getElementsByTagName('ProductReceipt')->item(0);
    $prodictId = $productReceipt->getAttribute('ProductId');
    $purchaseDate = $productReceipt->getAttribute('PurchaseDate');
} while(0);

if ($verifyFailed) {
    // invalid receipt
} else {
    // valid receipt
}

If you are dealing with WP8 in-app purchase, the CertificateId in the xml may look like this A656B9B1B3AA509EEA30222E6D5E7DBDA9822DCD. But the following link gives 404 error:

https://lic.apps.microsoft.com/licensing/certificateserver/?cid=A656B9B1B3AA509EEA30222E6D5E7DBDA9822DCD

So you cannot get the certificate with curl. Instead, replace the curl part with this:

    $publicKey = <<<EOT
-----BEGIN CERTIFICATE-----
MIIDFDCCAgCgAwIBAgIQrih3cQuSeL1CgpLFusfJsTAJBgUrDgMCHQUAMB8xHTAb
BgNVBAMTFElhcFJlY2VpcHRQcm9kdWN0aW9uMB4XDTEyMDIxNzAxMTYyNFoXDTM5
MTIzMTIzNTk1OVowHzEdMBsGA1UEAxMUSWFwUmVjZWlwdFByb2R1Y3Rpb24wggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb0CeltVqOOIJiwNGgAr7Z0K4r
AYsHCa1oSFPJXtokz134bi2neJ8bHIKAnT0kwa3xViUxwp3+OZd2t2PshDv0ucZ5
dus6WCnuAw/MHVAodgLQMqYiKeM7VTIi3S1s3iV/66Y8KP7jH3CmE2XCXOQae+bQ
UuyGsTit0ScU7+MofODoNhvONs54n/K1WVnct2wWBpn8GGAS+l2mzOF0jXbMSjtz
7wuK77GeydG+x9paLuHIyCso7tjOqv/lvol5IIX0VnC5G2vC6dWR6MkNL5FzLXns
SuQgoYEUZXPlXJhsmv6oyyenaP0PpYJZcCLLVi1L2hcVo8B2DIEg3I3t8ch/AgMB
AAGjVDBSMFAGA1UdAQRJMEeAEHGLK3BRpCWDa2vU50kI73ehITAfMR0wGwYDVQQD
ExRJYXBSZWNlaXB0UHJvZHVjdGlvboIQrih3cQuSeL1CgpLFusfJsTAJBgUrDgMC
HQUAA4IBAQC4jmOu0H3j7AwVBvpQzPMLBd0GTimBXmJw+nruE+0Hh/0ywGTFNE+K
cQ21L4v+IuP8iMh3lpOcPb23ucuaoNSdWi375/KxrW831dbh+goqCZP7mWbxpnSn
FnuV+R1VPsQjdS+0tg5gjDKNMSx/2fH8krLAkidJ7rvUNmtEWMeVNk0/ZM/ECino
bMSSwbqUuc9Qql9T1epe+xv34a6eek+m4W0VXnLSuKhQS5jdILsyeJWHROZF5mrh
3DQuS0Ll5FzKmJxHf0hyXAo03SSA+x3JphAU4oYbkE9nRTU1tR6iq1D9ZxfQmvzm
IbMfyJ/y89PLs/ewHopSK7vQmGFjfjIl
-----END CERTIFICATE-----
EOT;

(The certificate is from MSDN sample code. I converted it to PEM format.)

It now works XD!

Stackia
  • 2,110
  • 17
  • 23
  • Thank you! Was struggling with the .cer file MSDN provided, for the second kind of receipt the WinPhone 8 store can provide (i.e. certificate ID beginning `A656B9B...`). – Jamie Dexter Sep 10 '14 at 08:50
  • Perfect dude. Thank you. Why do you use do and while(0)?? – JuliSmz Sep 24 '19 at 15:04
  • @JuliSmz a common pattern to simulate 'goto' https://stackoverflow.com/questions/2314066/do-whilefalse – Stackia Sep 25 '19 at 03:25
0

Your $canonicalized should only content the receipt with "Signature" element (and all it's child nodes) removed. after that, you can do a base64_encode(hash('sha256', $canonicalized,TRUE)) to see if match the DigestValue before go for verify stage.

Besides the above issue,the receipt & cert you are using is outdated, even the "offical sample" from Microsoft: http://code.msdn.microsoft.com/wpapps/In-app-purchase-receipt-c3e0bce4 also fails to verify the signature using their c# example code.

I spend days to find a working receipt and cert online but end up no luck =(

Elf Yang
  • 1
  • 1
  • 1
    ops, i missed out that you need a $dom->preserveWhiteSpace = false; before $dom->loadXML to make the DigestValue match – Elf Yang Oct 09 '13 at 10:10