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:
-
Digital content - Digital books, magazines, photos, artwork, game levels, game characters, and so on.
-
Functionality - Products that unlock or expand app features. For example, you could ship a game with multiple smaller games for sale.
-
Services - One-time services such as voice transcription. Each time the service is used constitutes a separate purchase.
Product Types
Products are classified as one of three types:
-
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.
-
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.
-
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:
-
Only digital content or services consumed within an application are allowed. Real-world goods and services are not.
-
Intermediary digital currency for eventual item purchase is not allowed.
-
Products for purchase should not relate to gambling. Simulated gambling where no actual exchange of money takes place is okay.
-
Subscriptions are not supported.
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:
-
MasterCard or Visa credit card.
-
Operator billing - In the initial release, only AT&T in the United States is supported.
-
Promo Codes are not supported.
In this section:
-
In-App Payment Service APIs (Separate document.)
Basic Process
The following are the basic steps 3rd-party developers should follow:
-
Register app at the Developer Portal.
-
Register associated app items for purchase at the Developer Portal.
-
When app runs, dynamically retrieve app items for sale (getAvailableItems) from HP's Item Catalog Server and list them in app store.
-
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.
-
Deliver on-device purchased items. If items are off-device then:
-
Send request for content along with signed digital receipt to 3rd-party Content Server (CS).
-
CS has option to employ HP-provided verification process to ensure receipt is valid.
-
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:
-
Tracking products for sale and delivery.
-
Designing and implementing store presentation.
-
Deciding how their app delivers purchased products to end-users.
-
Managing how application specific payments are confirmed and tracked.
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:
- Mojo apps can use serviceRequest.
- Enyo apps can use PalmService.
- JavaScript (Enyo and Mojo) apps and services can use the Foundation library's PalmCall.
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:
- A signed data field containing the bytes to be signed. This includes the receipt information encoded as a string.
- A digital signature of the signed data.
- A signer information field, used to uniquely identify the signing certificate.
- An X.509 certificate chain used to sign the purchase.
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:
-
This process applies to the base64-encoded "signedReceipt" field returned from the getItemInfo, getPendingPurchaseInfo, and purchaseItem calls. The receipt object is information provided for the on-device app.
-
To see an example of implementing this process in code, see Appendix A: Sample Receipt Verification Java Code.
-
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:
- Java: BouncyCastle crypto and mail libraries
- C (Linux / UNIX / CygWIn): OpenSSL C-API
For an example of using BouncyCastle for this process, see Appendix A: Sample Receipt Verification Java Code.
-
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.
-
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,...
-
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:
- PDL_GetAvailableItems
- PDL_GetItemInfo
- PDL_GetPendingPurchaseInfo
- PDL_PurchaseItem
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 //** @returnReceiptVerifier.VerificationResult
containing verification //** result and receipt contents. //** @throwsReceiptVerificationException
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. //** @returntrue
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, ornull
if not successfully verified. //** public String getReceiptContent() { return receiptContent; } } }