Generating a Signature for Subscription Offers - Xcode - Swift
Asked Answered
P

6

6

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

Parttime answered 29/3, 2019 at 9:55 Comment(0)
C
7

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

Crossstaff answered 29/3, 2019 at 17:33 Comment(2)
Thank you for reporting this example, I've already followed it, but unfortunately the result is not positiveParttime
Do you know something example with PHP?Parttime
N
4

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.

Noranorah answered 17/4, 2019 at 18:14 Comment(2)
This is missing the definition of the Signature classJacindajacinta
Signature class is just a DTO there is no logic in it.Sarasvati
D
1

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.

Domingadomingo answered 7/5, 2019 at 16:47 Comment(2)
I'm trying to install the library you reported, but I always get an errorsop/crypto-types 0.2.1 requires ext-gmp * -> the requested PHP extension gmp is missing from your system.Parttime
Yep, most cryptographic libraries require GMP (GNU Multiple Precision) PHP extension. You can install one by following steps from here: #40010697Domingadomingo
H
0

I created the following PHP class, using built in PHP functions, which worked for me. (No additional libraries required)

<?php

class GetSubscriptionSignatureService {

    // Function to generate the signature
    function generateSignature($appBundleID, $keyIdentifier, $productIdentifier, $offerIdentifier, $applicationUsername, $returnType="JSON") {
        
        // Specifiy the path to the key file
        
        $keyFile = "[KEY_FILE_PATH]/[KEY_FILE_NAME].p8";

        $nonce = strtolower($this->guidv4());
        $timestamp = time() * 1000;

        // Combine the parameters into a UTF-8 string with an invisible separator (\u{2063} between them

        $payload = implode("\u{2063}", array(
            $appBundleID,
            $keyIdentifier,
            $productIdentifier,
            $offerIdentifier,
            $applicationUsername,
            $nonce,
            $timestamp
        ));

        // Read the private key from the p8 file
        $keyPem = file_get_contents($keyFile);
        
        
        // Create a key resource from the .p8 private key content
        $privateKey = openssl_pkey_get_private($keyPem);

       // Sign the data using RSA-SHA256
        openssl_sign($payload, $signature,$privateKey, OPENSSL_ALGO_SHA256);
        

        // Free the private key resource
        openssl_free_key($privateKey);

        // Base64-encode the signature
        $signature = base64_encode($signature);

        if($returnType=="JSON"){
            //Return as JSON
            $responseArray = array(
                'signature' => $signature,
                'nonce' => $nonce,
                'timestamp' => $timestamp,
                'keyIdentifier' => $keyIdentifier
            );

            // Send the response as JSON
            header('Content-Type: application/json');
            
            return json_encode($responseArray);
        } else {
            //Return as a Class
            $signatureData = new stdClass();
            $signatureData->signature = $signature;
            $signatureData->nonce = $nonce;
            $signatureData->timestamp = $timestamp;
            $signatureData->keyIdentifier = $keyIdentifier;
            
            return $signatureData;
        }
    }

    // Function to generate a GUID (Globally Unique Identifier)
    function guidv4() {
        
        if (function_exists('com_create_guid') === true) {
            return trim(com_create_guid(), '{}');
        }

        $data = openssl_random_pseudo_bytes(16);
        assert(strlen($data) === 16);

        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
        
    }
}
?>

-- Instantiating the Class and calling the function code snippet

$appBundleID = '[YOUR_APP_ID]';
$keyIdentifier = "[YOUR_APPLE_KEY_ID]";
$productIdentifier = "[YOUR_PRODUCT_ID]";
$offerIdentifier = [YOUR_OFFER_DATA];
$applicationUsername = "[YOUR_APPLICATION_USERNAME]";

$getSubscriptionSignature = new GetSubscriptionSignatureService();
$signatureData = $getSubscriptionSignature->generateSignature($appBundleID, $keyIdentifier, $productIdentifier, $offerIdentifier, $applicationUsername);
    
Holothurian answered 22/11, 2023 at 15:37 Comment(0)
P
-1

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();

?>
Parttime answered 17/4, 2019 at 12:38 Comment(1)
You'd better not reinvent the wheel and use proper libraries for UUID and cryptographic operations. There's a great PHP lib for UUID github.com/ramsey/uuid, installed via composer as composer require ramsey/uuid. Also, you need ECDSA algorithm for the signature, and it is better done with a library, such as PHPECC or any other (I used this one packagist.org/packages/sop/crypto-bridge).Domingadomingo
P
-2

Result

I found this example online but unfortunately the result is not positive. Tutorial Example

Parttime answered 1/4, 2019 at 8:22 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.