-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from MaartenStaa/windows-store-support
Windows store support
- Loading branch information
Showing
4 changed files
with
267 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
namespace ReceiptValidator\WindowsStore; | ||
|
||
interface CacheInterface | ||
{ | ||
/** | ||
* Retrieve an item from the cache by key. If the key is not found, null | ||
* should be returned. | ||
* | ||
* @param string $key | ||
* @return mixed | ||
*/ | ||
public function get($key); | ||
|
||
/** | ||
* Store an item in the cache for a given number of minutes, where 0 minutes | ||
* means forever. | ||
* | ||
* @param string $key | ||
* @param mixed $value | ||
* @param int $minutes | ||
* @return void | ||
*/ | ||
public function put($key, $value, $minutes); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
namespace ReceiptValidator\WindowsStore; | ||
|
||
use DOMDocument; | ||
use Guzzle\Http\Client as GuzzleClient; | ||
use ReceiptValidator\RunTimeException; | ||
use RobRichards\XMLSecLibs\XMLSecEnc; | ||
use RobRichards\XMLSecLibs\XMLSecurityDSig; | ||
|
||
class Validator | ||
{ | ||
protected $cache; | ||
|
||
public function __construct(CacheInterface $cache = null) | ||
{ | ||
$this->cache = $cache; | ||
} | ||
|
||
/** | ||
* Validate the given receipt. | ||
* | ||
* @param string $receipt | ||
* @return bool | ||
* @throws RunTimeException | ||
*/ | ||
public function validate($receipt) | ||
{ | ||
// Load the receipt that needs to verified as an XML document. | ||
$dom = new \DOMDocument; | ||
if (@$dom->loadXML($receipt) === false) { | ||
throw new RunTimeException('Invalid XML'); | ||
} | ||
|
||
// The certificateId attribute is present in the document root, retrieve it. | ||
$certificateId = $dom->documentElement->getAttribute('CertificateId'); | ||
if (empty($certificateId)) { | ||
throw new RunTimeException('Missing CertificateId in receipt'); | ||
} | ||
|
||
// Retrieve the certificate from the official site. | ||
$certificate = $this->retrieveCertificate($certificateId); | ||
|
||
return $this->validateXml($dom, $certificate); | ||
} | ||
|
||
/** | ||
* Load the certificate with the given ID. | ||
* | ||
* @param string $certificateId | ||
* @return resource | ||
*/ | ||
protected function retrieveCertificate($certificateId) | ||
{ | ||
// Retrieve from cache if a cache handler has been set. | ||
$cacheKey = 'store-receipt-validate.windowsstore.'.$certificateId; | ||
$certificate = $this->cache !== null ? $this->cache->get($cacheKey) : null; | ||
|
||
if ($certificate === null) { | ||
$maxCertificateSize = 10000; | ||
|
||
// We are attempting to retrieve the following url. The getAppReceiptAsync website at | ||
// http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.store.currentapp.getappreceiptasync.aspx | ||
// lists the following format for the certificate url. | ||
$certificateUrl = 'https://go.microsoft.com/fwlink/?LinkId=246509&cid=' . $certificateId; | ||
|
||
// Make an HTTP GET request for the certificate. | ||
$client = new GuzzleClient($certificateUrl); | ||
$response = $client->get()->send(); | ||
|
||
// Retrieve the certificate out of the response. | ||
$certificate = $response->getBody(true); | ||
|
||
// Write back to cache. | ||
if ($this->cache !== null) { | ||
$this->cache->put($cacheKey, $certificate, 3600); | ||
} | ||
} | ||
|
||
return openssl_x509_read($certificate); | ||
} | ||
|
||
/** | ||
* Validate the receipt contained in the given XML element using the | ||
* certificate provided. | ||
* | ||
* @param DOMDocument $dom | ||
* @param resource $certificate | ||
* @return bool | ||
*/ | ||
protected function validateXml(DOMDocument $dom, $certificate) | ||
{ | ||
$secDsig = new XMLSecurityDSig; | ||
|
||
// Locate the signature in the receipt XML. | ||
$dsig = $secDsig->locateSignature($dom); | ||
if ($dsig === null) { | ||
throw new RunTimeException('Cannot locate receipt signature'); | ||
} | ||
|
||
$secDsig->canonicalizeSignedInfo(); | ||
$secDsig->idKeys = array('wsu:Id'); | ||
$secDsig->idNS = array( | ||
'wsu' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', | ||
); | ||
|
||
if (!$secDsig->validateReference()) { | ||
throw new RunTimeException('Reference validation failed'); | ||
} | ||
|
||
$key = $secDsig->locateKey(); | ||
if ($key === null) { | ||
throw new RunTimeException('Could not locate key in receipt'); | ||
} | ||
|
||
$keyInfo = XMLSecEnc::staticLocateKeyInfo($key, $dsig); | ||
if (!$keyInfo->key) { | ||
$key->loadKey($certificate); | ||
} | ||
|
||
return $secDsig->verify($key) == 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
<?php | ||
|
||
use ReceiptValidator\WindowsStore\CacheInterface; | ||
use ReceiptValidator\WindowsStore\Validator; | ||
|
||
/** | ||
* @group library | ||
*/ | ||
class WindowsValidatorTest extends PHPUnit_Framework_TestCase | ||
{ | ||
/** | ||
* @dataProvider receiptProvider | ||
*/ | ||
public function testValidate($receipt) | ||
{ | ||
$validator = new Validator; | ||
$this->assertTrue($validator->validate($receipt), 'Receipt should validate successfully'); | ||
} | ||
|
||
/** | ||
* @dataProvider receiptProvider | ||
*/ | ||
public function testValidateWithCache($receipt) | ||
{ | ||
$validator = new Validator(new DummyCache); | ||
$this->assertTrue($validator->validate($receipt), 'Receipt should validate successfully'); | ||
} | ||
|
||
public function testValidateFails() | ||
{ | ||
$this->setExpectedException('ReceiptValidator\RunTimeException', 'Invalid XML'); | ||
|
||
$validator = new Validator; | ||
$validator->validate('foo bar'); | ||
} | ||
|
||
public function receiptProvider() | ||
{ | ||
return array( | ||
// App receipt | ||
array( | ||
'<Receipt Version="1.0" ReceiptDate="2012-08-30T23:10:05Z" '. | ||
'CertificateId="b809e47cd0110a4db043b3f73e83acd917fe1336" '. | ||
'ReceiptDeviceId="4e362949-acc3-fe3a-e71b-89893eb4f528">'. | ||
'<AppReceipt Id="8ffa256d-eca8-712a-7cf8-cbf5522df24b" '. | ||
'AppId="55428GreenlakeApps.CurrentAppSimulatorEventTest_z7q3q7z11crfr" '. | ||
'PurchaseDate="2012-06-04T23:07:24Z" LicenseType="Full" />'. | ||
'<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>cdiU06eD8X/w1aGCHeaGCG9w/kWZ8I099rw4mmPpvdU=</DigestValue>'. | ||
'</Reference>'. | ||
'</SignedInfo>'. | ||
'<SignatureValue>SjRIxS/2r2P6ZdgaR9bwUSa6ZItYYFpKLJZrnAa3zkMylbiWjh9oZGGng2p6/gtBHC2dSTZlLbqny'. | ||
'sJjl7mQp/A3wKaIkzjyRXv3kxoVaSV0pkqiPt04cIfFTP0JZkE5QD/vYxiWjeyGp1dThEM2RV811sRWvmEs/hHhVxb32e'. | ||
'8xCLtpALYx3a9lW51zRJJN0eNdPAvNoiCJlnogAoTToUQLHs72I1dECnSbeNPXiG7klpy5boKKMCZfnVXXkneWvVFtAA1'. | ||
'h2sB7ll40LEHO4oYN6VzD+uKd76QOgGmsu9iGVyRvvmMtahvtL1/pxoxsTRedhKq6zrzCfT8qfh3C1w=='. | ||
'</SignatureValue>'. | ||
'</Signature>'. | ||
'</Receipt>', | ||
), | ||
// Product receipt | ||
array( | ||
'<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/TDicidIPLBjTbc'. | ||
'ZylYZdGPkMvAIc3/1mdLMZYJc+EXG9IsE9L74LmJ0OqGH5WjGK/UexAXxVBWDtBbDI2JLOaBevYsyy+4hLOcTXDSUA4tX'. | ||
'wPa2Bi+BRoUTdYE2mFW7ytOJNEs3jTiHrCK6JRvTyU9lGkNDMNx9loIr+mRks+BSf70KxPtE9XCpCvXyWa/Q1JaIyZI7l'. | ||
'lCH45Dn4SKFn6L/JBw8G8xSTrZ3sBYBKOnUDbSCfc8ucQX97EyivSPURvTyImmjpsXDm2LBaEgAMADg=='. | ||
'</SignatureValue>'. | ||
'</Signature>'. | ||
'</Receipt>' | ||
), | ||
); | ||
} | ||
} | ||
|
||
class DummyCache implements CacheInterface | ||
{ | ||
protected $cache = array(); | ||
|
||
public function get($key) | ||
{ | ||
return isset($this->cache[$key]) ? $this->cache[$key] : null; | ||
} | ||
|
||
public function put($key, $value, $minutes) | ||
{ | ||
$this->cache[$key] = $value; | ||
} | ||
} |