Implementing In-App Payment



The webOS In-App Payment framework allows you to embed a store directly in your app and sell additional app content and functionality. If not locked within the app, additional app content can be downloaded from 3rd party servers. This content can include such items as media files, game levels, and game playing aids and tools. Your app can use HP payment APIs to securely process user payments and provide receipt verification.

Any app that has items registered for sale at HP's developer portal can make use of the In-App Payment framework. Users can purchase items after an initial payment setup configuration.

The webOS SDK contains In-App Payment functionality which the PDL (Palm Development Library) also makes available to PDK (C/C++) apps. Equivalent sets of APIs are provided for both JavaScript and PDK apps.

Off-Device Content

Once an item has been purchased, and it is not on-device, webOS In-App Payment allows 3rd-party apps to download content from a non-HP content hosting server. For this purpose, webOS In-App Payment provides a secure purchase verification process. Developers should follow this process to ensure that a user has legitimately purchased an item.

Saleable Content

Developers are allowed to sell a wide variety of products including:

Product Types

Products are classified as one of three types:

  1. Perishable - Can be purchased multiple times, but each item purchased can only be used once. However, webOS Payment Services does not track usage; it us up to developers to implement logic for handling this.

  2. Non-Perishable - Can only be purchased once, but can be used multiple times indefinitely. Unlike perishables, non-perishables are transferable. For example, if a user purchases a new game level on a phone, and they then install the same game on a TouchPad using the same HP webOS account, they will then have that same level on both devices.

  3. Subscriptions - Can be purchased multiple times with each item purchased usable for a specific period of time.

In the first release, perishable and non-perishable items are supported, but subscriptions are not.

Sale Criteria

In the initial release, items for sale must meet the following criteria:

User Payment

If a user does not have a configured payment option the first time they attempt to purchase an item, a Payment Setup UI is displayed that steps them through the process of configuring one. This occurs automatically, nothing is required of developers for this to happen. You only need to note the return value from the purchase item API which could indicate the user declined to configure a payment option.

Payment Options

In the initial release, the following types of payment are allowed:


In this section:


Basic Process

The following are the basic steps 3rd-party developers should follow:

  1. Register app at the Developer Portal.

  2. Register associated app items for purchase at the Developer Portal.

  3. When app runs, dynamically retrieve app items for sale (getAvailableItems) from HP's Item Catalog Server and list them in app store.

  4. Process the purchase of user-requested items (purchaseItem) via HP's Payment Server which returns digital receipt. When the user first purchases an item, their payment options are configured in a one-time operation through an HP app that automatically appears. If a payment method has been configured, a confirmation UI will appear.

  5. Deliver on-device purchased items. If items are off-device then:

    1. Send request for content along with signed digital receipt to 3rd-party Content Server (CS).

    2. CS has option to employ HP-provided verification process to ensure receipt is valid.

    3. If CS is happy purchase is legitimate, it downloads content to device app.

Note

To test your app in the SDK environment, your device's Palm Profile email address must be the same as the email address associated with your dev portal account.

Developer Responsibilites

The In App framework communicates with HP's App Catalog infrastructure and Payment services on 3rd party developers' behalf. Developers, however, are responsible for the following:

In summary, while In-App purchase collects payment, developers must provide any additional functionality, including unlocking built-in features or downloading content from their own servers.

Tracking Perishable Items

As mentioned earlier, perishable items can be purchased multiple times, but should be used only once. webOS Payment Services does not track this, however, and it us up to developers to manage usage. Developers can do this using the globally-unique receipt ID associated with each purchased item. One possibility is to store a db8 data object for each item that would look something like this:

 {
     "receiptId" : int,
     "consumed"  : boolean
 }

Once used, the consumed flag would be set to true.


In-App Payment Service

The In-App Payment framework consists of one component accessible to device apps:

 com.palm.service.payment

All in-app purchase functionality is implemented via API calls to this JavaScript service accessible on the device bus.

Calling from JavaScript

Developers have the same options for calling the In-App Payment service as they do for calling other device services:

For API reference information, see In-App Payment Service APIs.

Calling from C/C++

The PDL (Palm Development Library) provides APIs that wrap calls to com.palm.service.payment. This gives C/C++ apps equivalent functionality to JavaScript apps. See the PDK In-App Payment API for more information.



Purchase Verification for Off-Device Content

When a user purchases a 3rd-party app item, the app receives a digital receipt from an HP Payment server.

Since a device implicitly trusts HP, there is no need to verify a receipt within a device app. However, the same is not true for non-HP servers hosting device app content. In this case, it may be necessary for the content server to verify the receipt and ensure its authenticity. If the verification process that HP provides is then followed and completed successfully, its authenticity is guaranteed.

Receipt signing uses a X.509/PKI infrastructure. Receipts are signed using HP's private keys associated with the public keys published in the certificates. By verifying the signature against the included signing certificate, and by verifying the certificate chain, it is possible to determine the signature's authenticity, and, by extension, the purchase.

Receipt Structure

An In-App purchase receipt is a PKCS#7 cryptographically signed container containing the following elements:

Purchase Verification Process

The verification process involves a certificate chain, ensuring all certificates in the chain are valid, and verifying the signature such that if the process succeeds, the receipt is considered authentic. This process allows developers to examine the receipt content to determine the item purchased and uniquely identify the transaction.


Notes:


  1. Open the receipt and access its internal fields.

    This step requires you to open the receipt's PKCS#7 container and access its internal fields. Decoding ASN.1 structures can be cumbersome. We recommend using a cryptographic library with support for PKCS#7. Fortunately, such libraries are available in many programming languages as open source add-ons. For example:

    For an example of using BouncyCastle for this process, see Appendix A: Sample Receipt Verification Java Code.

  2. Verify the receipt signature.

    Use your cryptographic library of choice to:

    • Extract the signed data
    • Extract the signature
    • Identify the signer
    • Construct the certificate chain

    Your library might also contain code that allows it to verify the receipt's digital signature.

    Since the exact verification process is library and language specific, it is not within this document's scope. Please refer to X.509 Certificate standards for further information.

    Note on OCSP and CRL

    It is important to turn on OCSP (Online Certificate Status Protocol) and CRL(Certificate Revocation List) checks when verifying the signature. If a signing key is compromised, HP will revoke its certificate - purchases signed with compromised certificates should not be trusted. The device can always get a new receipt - signed with a new key - if the original receipt signing certificate is compromised. Such revocation can be detected with OCSP and CRL extensions.

  3. Verify the certificate domain.

    Certificates for HP In-App purchases have the domain "palmws.com" and are rooted in Verisign's root CA (Certificate Authority). Usually, the X.500 Subject DN contains the following component:
     CN=rcpt-signer*.palmws.com,OU=HP webOS Cloud Services,....
    

    If the domain matches and the signing certificate is the Verisign root CA, the X.509 certificate chain is valid.

    For testing In-App purchases in the Dev Portal sandbox environment, ths CN will be used:

CN=receipt.testing.palmws.com,...

  1. Extract the receipt content.

    Once the receipt is determined to be valid, its contents can be extracted and interpreted.

    A JSON object parser library such as JacksonMapper by codehaus.org is recommended to make this task easier.


PDK In-App Payment API

All the PDK APIs block. Developers need to be aware that API calls could take awhile to return and code their apps accordingly. While waiting, internal thread management continues.

The PDK APIs for in-app payments parallel those for JavaScript apps: they can be used to get available items, purchase items, and check pending purchases and item information.

The PDK accesses com.palm.service.payment for 4 calls:

These are wrappers for the same calls the SDK allows JavaScript developers to make. For each of these service calls, the PDK returns the service response. It is up to the caller to parse this for themselves. The other PDK calls are used to help convert these responses to JSON-formatted strings or free the responses' allocated memory.

Note: As with all PDK APIs, developers need to include PDL.h to access them.


Call Summary Description
PDL_FreeItemCollection Frees a PDL_ItemCollection struct.
PDL_FreeItemInfo Frees a PDL_ItemInfo struct.
PDL_FreeItemReceipt Frees a PDL_ItemReceipt struct.
PDL_GetAvailableItems Returns a list of all items available to this application.
PDL_GetItemCollectionJSON Returns a JSON formatted string for the values contained in a PDL_GetItemCollectionJSON struct.
PDL_GetItemInfo Returns information about a specific purchase item.
PDL_GetItemJSON Returns a JSON formatted string for the values contained in a PDL_ItemInfo struct.
PDL_GetItemReceiptJSON Returns a JSON formatted string for the values contained in a PDL_ItemReceipt struct.
PDL_GetPendingPurchaseInfo Gets information about a pending purchase.
PDL_PurchaseItem Purchases an in-app item.



Appendix A: Sample Receipt Verification Java Code

The following Java code performs the verification steps outlined in Purchase Verification for Off-Device Content.

This code uses the open source Bouncy Castle's crypto and messaging libraries, as well as Java's own standard APIs for PKI and X.509 Certificate verification and handling.


//
// Sample Main.java class (replace with your own)
// ====================================================================================
//

import com.palmws.inapp.receiptverification.ReceiptVerificationException;
import com.palmws.inapp.receiptverification.ReceiptVerifier;

public class Main {

    public static void main(String[] args) {

        try {
            ReceiptVerifier verifier = new ReceiptVerifier(true);

            String receiptB64 = "MIAGCSqGS.........."; // Insert receipt here
            ReceiptVerifier.VerificationResult result = verifier.verifyReceipt(receiptB64);
            if (result.getVerificationStatus()) {
                System.out.println("Success! PKCS#7 validated");
                System.out.println("Receipt content: " + result.getReceiptContent());
            } else {
                System.out.println("failed");
            }
        } catch (ReceiptVerificationException rve) {
            rve.printStackTrace();
        }
    }
}

//
// ReceiptVerificationException.java
// ====================================================================================
//

package com.palmws.inapp.receiptverification;package com.palmws.inapp.receiptverification;

/**
 * Generic exception class for receipt verification. Encompasses underlying exception causes.
 */
public class ReceiptVerificationException extends Exception {

    public ReceiptVerificationException(Throwable cause) {
        super(cause);
    }
}

//
// ReceiptVerifier.java
// ====================================================================================
// 

package com.palmws.inapp.receiptverification;

import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.util.encoders.Base64;

//******************************************************************************************
//**
//** ReceiptVerifier
//**
//** This class implements the receipt verification logic for HP webOS verification purchases.
//**
//******************************************************************************************* 
public class ReceiptVerifier {

    private CertificateFactory cfact;
    private PKIXParameters trustedPkixParams;
    private VerificationResult failedVerificationResult = new VerificationResult();
    private Logger logger;
    private boolean testingReceipts;

    //******************************************************************************************
    //**
    //**  setupTrustedRootsAndRevocationChecks
    //**
    //******************************************************************************************
    private void setupTrustedRootsAndRevocationChecks() throws ReceiptVerificationException {

        try {
            //**
            //** Load the JDK's cacerts keystore file
            //**
            String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
            FileInputStream is = new FileInputStream(filename);
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            String password = "changeit";
            keystore.load(is, password.toCharArray());

            //**
            //**  This class retrieves the most-trusted CAs from the keystore
            //**
            trustedPkixParams = new PKIXParameters(keystore);

            //**
            //** Enable CRL checking
            //**
            trustedPkixParams.setRevocationEnabled(true);

            //**
            //** Enable OCSP checking - enable in conjunction to PKIXParameters.setRevocationEnabled(true).
            //**
            Security.setProperty("ocsp.enable", "true");

            // Success
            return;
        } catch (Exception e) {
            throw new ReceiptVerificationException(e);
        }
    }

    //******************************************************************************************
    //**
    //**  checkPalmWSCertificateDomain
    //**
    //******************************************************************************************
    private boolean checkPalmWSCertificateDomain(X509Certificate cert) {
        String name = cert.getSubjectX500Principal().getName(X500Principal.RFC2253);
        int cnIdx = name.indexOf("CN=");
        int commaIdx = name.indexOf(",");
        String cnName = name.substring(cnIdx+3, commaIdx);

        if (testingReceipts) {
            if (cnName.startsWith("receipt")  && cnName.endsWith(".palmws.com")) {
                return true;
            }
        } else {
            if (cnName.startsWith("rcpt-signer")  && cnName.endsWith(".palmws.com")) {
                return true;
            }
            logger.log(Level.INFO, "Failed certificate name verification.");
        }
        return false;
    }

    //**
    //**
    //** Constructor.
    //**
    //** @param inProduction - set to "true" to verify production receipts,
    //** "false" for testing or sandbox environment receipts.
    //**
    //** @throws "ReceiptVerificationException" if a problem occurs.
    //**
    public ReceiptVerifier(boolean inProduction) throws ReceiptVerificationException {
        try {
            setupTrustedRootsAndRevocationChecks();
            cfact = CertificateFactory.getInstance("X.509");
            logger = Logger.getLogger(this.getClass().getName());
            testingReceipts = !inProduction;
        } catch (Exception e) {
            throw new ReceiptVerificationException(e);
        }
    }

    //******************************************************************************************
    //**
    //**  verifyReceipt
    //**
    //**  Receives a Base64-encoded receipt as parameter, and returns an object
    //**  containing a receipt verification result. Receipt content is only included
    //**  if receipt successfully verified.
    //**  @param receiptB64 Base64-encoded receipt
    //**  @return ReceiptVerifier.VerificationResult containing verification
    //**  result and receipt contents.
    //**  @throws ReceiptVerificationException if a problem occurs.
    //**
    //********************************************************************************************* 
    public VerificationResult verifyReceipt(String receiptB64) throws ReceiptVerificationException {
        try {
            //**
            //** Receipt data
            //**
            byte[] receiptData = Base64.decode(receiptB64);
            logger.log(Level.INFO, "Receipt size: " + Integer.toString(receiptData.length));

            //**
            //** Validate the signature
            //**
            CMSSignedData s = new CMSSignedData(receiptData);
            CertStore certs = s.getCertificatesAndCRLs("Collection", "SUN");
            SignerInformationStore signers = s.getSignerInfos();
            boolean verified = false;

            ArrayList<Certificate> certPath = new ArrayList<Certificate>();

            for (Iterator i = signers.getSigners().iterator(); i.hasNext(); ) {
              SignerInformation signer = (SignerInformation) i.next();
              logger.log(Level.INFO, signer.getSID().toString());
              Collection<? extends Certificate> certCollection = certs.getCertificates(signer.getSID());
              if (!certCollection.isEmpty()) {
                X509Certificate cert = (X509Certificate) certCollection.iterator().next();
                logger.log(Level.INFO, cert.getSubjectX500Principal().toString());
                if ((signer.verify(cert.getPublicKey(), "SunRsaSign")) &&
                        (checkPalmWSCertificateDomain(cert))) {
                  verified = true;

                  //** 
                  //** Reconstruct certificate path
                  //**
                  certPath.add(cert);

                  boolean finish = false;

                  while (!finish) {
                      X509CertSelector csel = new X509CertSelector();
                      csel.setSubject(cert.getIssuerX500Principal());
                      certCollection = certs.getCertificates(csel);

                      if (!certCollection.isEmpty()) {
                          cert = (X509Certificate) certCollection.iterator().next();
                          logger.log(Level.INFO, cert.getSubjectX500Principal().toString());

                          if (cert.getIssuerX500Principal().equals(cert.getSubjectX500Principal())) {
                              logger.log(Level.INFO, "Root cert reached.");
                              finish = true;
                          } else {
                              certPath.add(cert);
                          }
                      } else {
                          logger.log(Level.INFO, "Last cert in chain");
                          finish = true;
                      }
                  }
                }
              }
            }

            //**
            //** Now validate the certificate path to root
            //** first build the chain...
            //**
            List<X509Certificate> certList = Arrays.asList(certPath.toArray(new X509Certificate[0]));
            logger.log(Level.INFO, "Cert list size: " + certList.size());
            CertPath cpath = cfact.generateCertPath(certList);
            try {
                CertPathValidator certVal = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
                certVal.validate(cpath, trustedPkixParams);
                // Success
            } catch (Exception e) {
                // Failed to validate path
                e.printStackTrace();
                verified = false;
            }

            if (verified) {

                //** 
                //** Recover content
                //**
                CMSProcessable signedContent = s.getSignedContent() ;
                byte[] originalContent  = (byte[]) signedContent.getContent();
                String recoveredContent = new String(originalContent);
                VerificationResult successfulVerificationResult = new VerificationResult();
                successfulVerificationResult.verified = true;
                successfulVerificationResult.receiptContent = recoveredContent;
                logger.log(Level.INFO, "Successfully verified receipt and extracted content");
                return successfulVerificationResult;
            } else {
                logger.log(Level.INFO, "Failed to verify receipt");
                return failedVerificationResult;
            }
        } catch (Exception e) {
            throw new ReceiptVerificationException(e);
        }
    }

    //**
    //** Class containing the receipt verification result.
    //**
    public class VerificationResult {

        private boolean verified = false;
        private String receiptContent = null;

        //**
        //** Contains the verification status.
        //** @return true for verified, false for failed verification.
        //**
        public boolean getVerificationStatus() {
            return verified;
        }

        //**
        //** Return the receipt content, if successfully verified.
        //** @return String containing receipt content if verified, or null if not successfully verified.
        //**
        public String getReceiptContent() {
            return receiptContent;
        }
    }

}