问题
I wanted to ask if someone has already implemented the new Offers for the inapp-subscription (auto renewal), the difficulty in creating server-side the system to create this signature using the p8 key with php if possible. I found this on the Apple documentation, I'm not sure understanding it: https://developer.apple.com/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers
回答1:
Here's a walkthrough from RevenueCat: iOS Subscription Offers
The post contains much more detail, but the signature generation is:
import json
import uuid
import time
import hashlib
import base64
from ecdsa import SigningKey
from ecdsa.util import sigencode_der
bundle_id = 'com.myapp'
key_id = 'XWSXTGQVX2'
product = 'com.myapp.product.a'
offer = 'REFERENCE_CODE' # This is the code set in ASC
application_username = 'user_name' # Should be the same you use when
# making purchases
nonce = uuid.uuid4()
timestamp = int(round(time.time() * 1000))
payload = '\u2063'.join([bundle_id,
key_id,
product,
offer,
application_username,
str(nonce), # Should be lower case
str(timestamp)])
# Read the key file
with open('cert.der', 'rb') as myfile:
der = myfile.read()
signing_key = SigningKey.from_der(der)
signature = signing_key.sign(payload.encode('utf-8'),
hashfunc=hashlib.sha256,
sigencode=sigencode_der)
encoded_signature = base64.b64encode(signature)
print(str(encoded_signature, 'utf-8'), str(nonce), str(timestamp), key_id)
This is just a proof of concept. You will want this on your server and perhaps have some logic to determine, for a given user, if the requested offer is appropriate.
Once you’ve generated the signature, nonce and timestamp send these along with the key_id
back to your app where you can create an SKPaymentDiscount
.
Disclaimer: I work at RevenueCat. We support Subscription Offers out of the box with our SDK, no code-signing required: https://www.revenuecat.com/2019/04/25/signing-ios-subscription-offers
回答2:
I used to have problems with subscription offers, but this issue on GitHub helped me to make it work. I installed Sop CryptoBridge library (composer require sop/crypto-bridge
) and it finally worked for my iOS app client. Here is my working PHP code:
use Sop\CryptoBridge\Crypto;
use Sop\CryptoEncoding\PEM;
use Sop\CryptoTypes\AlgorithmIdentifier\Hash\SHA256AlgorithmIdentifier;
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\SignatureAlgorithmIdentifierFactory;
use Sop\CryptoTypes\Asymmetric\PrivateKeyInfo;
// you can copy your p8 file contents here or just use file_get_contents()
$privateKeyPem = <<<'PEM'
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
PEM;
// load private key
$privateKeyInfo = PrivateKeyInfo::fromPEM(PEM::fromString($privateKeyPem));
// you can also load p8 file like this
// PrivateKeyInfo::fromPEM(PEM::fromFile($pathToP8));
// combine the parameters
$appBundleId = 'com.github.yaronius'; // iOS app bundle ID
$keyIdentifier = 'A1B2C3D4E5'; // Key ID from AppStore Connect
$productIdentifier = 'product_identifier';
$offerIdentifier = 'offer_identifier';
$applicationUsername = 'username'; // same as in the iOS app
$nonce = 'db6ba7a6-9ec2-4504-bcb9-c0dfbdc3d051'; // some UUID in lowercase
$timestamp = time() * 1000; // milliseconds! as stated in the docs
$dataArray = [
$appBundleId,
$keyIdentifier,
$productIdentifier,
$offerIdentifier,
$applicationUsername,
$nonce,
$timestamp
];
$data = implode("\u{2063}", $dataArray);
// signature algorithm
$algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
$privateKeyInfo->algorithmIdentifier(),
new SHA256AlgorithmIdentifier()
);
// generate signature
$signature = Crypto::getDefault()->sign($data, $privateKeyInfo, $algo);
// encode as base64 encoded DER
$encodedSignature = base64_encode($signature->toDER());
// send signature to your app
echo $encodedSignature;
Keep in mind a few things:
- as a delimiter you have to use PHP Unicode codepoint, i.e.
"\u{2063}"
. Using'\u2063'
did not work for me. $nonce
is lowercase UUID$timestamp
is in milliseconds (i.e.time() * 1000
).
And it should work like a charm.
回答3:
I can confirm that this is working:
<?php
use Ramsey\Uuid\Uuid;
class ItunesSignatureGenerator {
private $appBundleID = 'your.bundle.id';
private $keyIdentifier = 'ZZZZZZZ';
private $itunesPrivateKeyPath = '/path/to/the/file.p8;
/**
* @see https://developer.apple.com/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers
*
* @param $productIdentifier
* @param $offerIdentifier
*
* @return Signature
*/
public function generateSubscriptionOfferSignature($productIdentifier, $offerIdentifier)
{
$nonce = strtolower(Uuid::uuid1()->toString());
$timestamp = time() * 1000;
$applicationUsername = 'username';
$message = implode(
"\u{2063}",
[
$this->appBundleID,
$this->keyIdentifier,
$productIdentifier,
$offerIdentifier,
$applicationUsername,
$nonce,
$timestamp
]
);
$message = $this->sign($message);
return new Signature(
base64_encode($message),
$nonce,
$timestamp,
$this->keyIdentifier
);
}
private function sign($data)
{
$signature = '';
openssl_sign(
$data,
$signature,
openssl_get_privatekey('file://' . $this->itunesPrivateKeyPath),
OPENSSL_ALGO_SHA256
);
return $signature;
}
}
We had a issue on the client side because the device was connected on 2 different itunes account, one regular and one sandbox. It was creating a invalid signature error that didn't make sens. We disconnect the regular account and just use the sandbox account and everything is working.
回答4:
I am trying to create something in php, but it seems impractical, maybe spotting something in the encoding?
<?php
class Key_offer {
var $appBundleId;
var $keyIdentifier;
var $productIdentifier;
var $offerIdentifier;
var $applicationUsername;
var $nonce;
var $timestamp;
function __construct() {
// Setting Data
$this->appBundleId = 'bundle-app-id';
$this->keyIdentifier = '0123456789';
$this->productIdentifier = $_POST["productIdentifier"] ?? "";
$this->offerIdentifier = $_POST["offerIdentifier"] ?? "";
$this->applicationUsername = $_POST["usernameHash"] ?? ""; // usare lo stesso anche nella chiamata che si effettua da Xcode
$this->nonce = strtolower( $this->gen_uuid() ); // genera UUID formato 4;
$this->timestamp = time(); // get timeStump
}
function rsa_sign($policy, $private_key_filename) {
$signature = "";
// load the private key
$fp = fopen($private_key_filename, "r");
$priv_key = fread($fp, 8192);
fclose($fp);
$pkeyid = openssl_get_privatekey($priv_key);
// compute signature
openssl_sign($policy, $signature, $pkeyid);
// free the key from memory
openssl_free_key($pkeyid);
return $signature;
}
function gen_uuid() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
// 16 bits for "time_mid"
mt_rand( 0, 0xffff ),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand( 0, 0x0fff ) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand( 0, 0x3fff ) | 0x8000,
// 48 bits for "node"
mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
);
}
function get() {
$text = utf8_encode($this->appBundleId.'\u2063'.$this->keyIdentifier.'\u2063'.$this->productIdentifier.'\u2063'.$this->offerIdentifier.'\u2063'.$this->applicationUsername.'\u2063'.$this->nonce.'\u2063'.$this->timestamp);
$signature0 = $this->rsa_sign($text, "key.pem"); // SubscriptionKey_43PF4FTV2X.p8
$signature = hash('sha256', $signature0);
$array = array(
'lowUUid' => $this->nonce,
'timeStump' => $this->timestamp,
'identifier' => $this->offerIdentifier,
'keyid' => $this->keyIdentifier,
'signature' => base64_encode($signature)
);
return json_encode($array);
}
}
$obj = new Key_offer();
echo $obj->get();
?>
回答5:
I found this example online but unfortunately the result is not positive. Tutorial Example
来源:https://stackoverflow.com/questions/55414749/generating-a-signature-for-subscription-offers-xcode-swift