Using Encrypted PayPal Buttons (EWP) and IPN

If you want to accept PayPal payments on your site you have a variety of options. What I consider to be the ideal solution is a pairing of their IPN service with encrypted buttons. This allows your customers/clients to pay you securely without having to host an SSL cert on your site. Using encrypted buttons we can dynamically generate the amount values without having to worry about them being tampered with. Sure you could always verify the amount later, but why bother when there’s an easily solution around that, plus who really needs the hassle? This guide will walk you step-by-step from the question of “how?” to setting up a sophisticated payment solution.

Getting things set up

Before we can do any coding we first need to set up our environment. In order to get anywhere you’re going to need at least merchant account with PayPal, so if you don’t have one you’ll want to sign up for free. Next, before we even bother logging into paypal let’s set up the certificates we’re going to need. First we’re going to generate our private key certificate and use that to create the public key cert. For more information on public key cryptography check out part 3 of my cryptography series. Using paypal’s handy guide we can just copy and paste this code into a *nix shell to get a fresh new private key cert:

openssl genrsa -out my-prvkey.pem 1024

Then follow that up with:

openssl req -new -key my-prvkey.pem -x509 -days 365 -out my-pubcert.pem

OpenSSL should come standard with most distros, even OSX has it installed by default, but if it’s missing for some reason check your repos (synaptic, YaST, portage, etc.). If you still can’t find it for some reason you could always build from source. Now your’re set to rock! What’s that you say? You don’t have access to a linux/unix/BSD/OSX box or shell? If for some reason you’re completely isolated in a windows world, fear not! OpenSSL does provided source for your Redmond restrictions. Although you could build from source it’s probably easier to just grab one of the binary installers. All the commands will be the same.

Now that our certificates are generated we’re going to want to log into paypal and get things set up on that end. After logging in to your account go to My Account -> Profile -> More Options -> Selling Preferences -> Encrypted Payment Settings. You should now be at a page that looks like this:
paypal1
First download the paypal public certificate and keep it with the two you generated. I recommend creating a folder called “keys” for these three files. Make sure this folder is set to chmod 750 or more restrictive if possible on your web server. Next upload your public certificate (my-pubcert.pem in this example) to paypal with the “Add” button. Note the Cert ID that was generated and jot that down for later. Our last stop on paypal is to enable IPN. Click the “Back to Profile Summary” link towards the top of the page. Here you can copy down your “Secure Merchant Account ID” if you want. This value is often used in regular buttons to link purchases to your account without having to expose your e-mail address, but since we’ll be encrypting the contents of our button it doesn’t really matter which value you use. Now back under “Selling Preferences” click on the link for “Instant Payment Notification Preferences” You should now see a screen like this:
paypal2
Type in the address of your callback script including the ‘http://’ I usually go with something like “http://www.example.com/ipn.php” or cb.php, the name of the script doesn’t matter, and if you decide you want to change the location/name down the road you can always edit the settings later. Finally make sure that “Receive IPN messages (Enabled)” is selected. Paypal allows you to stop message delivery should you need to take the script down for maintenance yet still accept payments. Payments will build up in a queue and when you switch the delivery option back to “Enabled” the queue will fire off against your updated script like nothing ever happened. At this point you’re really done, but I like to make one more change. If you click back to the profile settings you’ll see one called “Website Payment Preferences”. Here you can set up a number of preferences, including a URL that users will be sent to when the finish payment, but the one we’re interested in is a little further down and called “Encrypted Website Payments”. If you turn this one it will block any button that isn’t encrypted. It’s not strictly necessary, but I feel a little better turning it on. Of course the biggest threat really is that someone would insert a rogue button somewhere and start sending you free money… but I digress. That should do it for the paypal side of things. Let’s move onto the code!

Button Generation

To create out encrypted buttons we’re going to write a function that will take an associative array and spit out the glob of encrypted text that gets passed as the value for the hidden field named “encrypted” in our paypal form. Without further adieu let’s take a look at this code and then dissect it:

function paypal_encrypt($hash) {
	global $base;
	//certs
	$priv = "$base/keys/my-prvkey.pem";
	$pub = "$base/keys/my-pubcert.pem";
	$paypalCert = "$base/keys/paypal_cert.pem";
	//Cert ID
	$cid = "XXXXXXXX"; 
	
	$openssl = "/usr/bin/openssl";
	
	$errors = array();
	if (!file_exists($priv)) {
		$errors[] = "error: private cert $priv not found\n";
	}
	if (!file_exists($pub)) {
		$errors[] = "error: public cert $pub not found\n";
	}
	if (!file_exists($paypalCert)) {
		$errors[] = "error: paypal cert $paypalCert not found\n";
	}
	if (!file_exists($openssl)) {
		$errors[] = "error: openssl $openssl not found\n";
	}
	if ($errors) return implode("\n",$errors);

	//assign a couple extra variables
	$hash['cert_id'] = $cid;
	$hash['bn']= 'SubvenioComputing_BuyNow_WPS_US';
	
	foreach ($hash as $key => $val) {
		if ($val == "") continue;
		$data .= "$key=$val\n";
	}

	$cmd = "($openssl smime -sign -signer $pub -inkey $priv -outform der -nodetach -binary <<_eof_\n$data\n_eof_\n) | $openssl smime -encrypt -des3 -binary -outform pem $paypalCert";

	exec($cmd, $encrypted, $error);

	if (!$error) {
		return implode("\n",$encrypted);
	} else {
		return "ERROR: encryption failed";
	}
}

Most of the details you don’t really need to concern yourself with. First it sets up the certificates, which as I mentioned before I’ve put in a folder called “keys”. I’m using a global variable here called $base that you would set up outside your function. I set it to the absolute path of the script so that I’m always sure I have the right files. Then that Cert ID you jotted down? Plug that value in for the $cid. Next we do a little error checking to make sure all the files exist and the path to openssl is valid. If we pass the test we add a couple values to the hash, first is that Cert ID and then the “build notation” variable. The format is: <Company>_<Service>_<Product>_<Country>. You should change the Company and Country and if you’re not doing the standard buy now button the other options for Product are: AddToCart, Donate, Subscribe, BuyGiftCertifcate and ShoppingCart. Then the boring part of actually doing the encryption.

I tend to use a standard config file and I toss the function in there with the rest, but you can throw it anywhere you want. To use our new function we can create a little something like this:

<?php
require_once('config.php'); //file with out function
$form = array('cmd' => '_xclick',
	      'business' => 'paypallogin@email.com',
	      'lc' => 'US',
	      'custom' => 1,
	      'invoice' => '1',
	      'currency_code' => 'USD',
	      'no_shipping' => '1',
	      'item_name' => '1',
	      'item_number' => '1',
	      'amount' => '99.99'
);

$encrypted = paypal_encrypt($form);
?>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_s-xclick">
<input type="image"
src="https://www.paypal.com/en_US/i/btn/btn_buynowCC_LG.gif" border="0"
name="submit" alt="Make payments with PayPal - it's fast, free and secure!">
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif"
width="1" height="1">
<input type="hidden" name="encrypted" value="<?php echo $encrypted ?>" />
</form>

And that should do it, a perfectly encrypted button. Obviously you probably would be calling this as part of a fuller script and populate those values with meaningful information, if you were just going to use static data you’d be better of just using a hosted button. The business field is the one that will accept either your Secure Merchant ID or plain ol’ login details. You can use any of the standard PayPal variables a couple that I’d like to highlight for what we’re doing are invoice and custom. These two variables are so-called “pass through” variables. That is, PayPal doesn’t do anything with them, but they’ll show back up in your IPN reply that we’ll be covering next. You can store anything you really want in them, but be warned the custom field is limited to 256 characters and invoice tops out at 127. You’ll probably also want to add return and cancel variables that will bring the user back to your site. At this state our button should be up and working perfectly fine. A user could complete the transaction and paypal would probably send you an e-mail. We want more though so next we’ll get our callback script online.

IPN callback script

Letting customers pay with PayPal buttons is all well and good, but if you send them off to this third party site you’re left with clean up. Did their payment get authorized? Is it an e-check and still processing? Did they click the button to pay and then decide not to? Normally you’d be completely in the dark at this point. The other option seems to be setting up an SSL cert on your site and creating some e-commerce solution to handle all the transactions. We know better though, with a little more coding we can get all that information back to us and then preform whatever actions necessary from changing an order status to packaging/shipping or marking your invoice as payment received.

I’ll give you the skeleton script (based on the PayPal example), but it won’t actually do anything other than acknowledge receiving the IPN message, it’s up to you to figure out what your site needs to do with this information.

<?php
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';

foreach ($_POST as $key => $value) {
	  $value = urlencode(stripslashes($value));
	  $req .= "&$key=$value";
}

// post back to PayPal system to validate
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen ('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30);

if ($fp) {
	fputs ($fp, $header . $req); //actually send data to paypal
	require_once('config.php'); //pull in config file (initialize db/variables/etc)
	// assign posted variables to local variables
	$status = $_POST['payment_status'];
	$payment = --$_POST['mc_gross'];
	$i = escape_data($_POST);
	$error = array();
	while (!feof($fp)) {
		$res = fgets ($fp, 1024);
		if (strcmp ($res, "VERIFIED") == 0) {
			if ($status == "Completed") {
				//check if a pending transaction just completed
				$query = "SELECT * FROM `table` WHERE `trans`='$i[txn_id]'";
				if($result = mysqli_query($dbc,$query)) $row = mysqli_fetch_assoc($result);
				if ($row['pending']) {
					//update pending status to completed/paid
				} else {
					//add instant payments to database
				}
				//do global things you need to for processing payment completion
			} elseif ($status == "Pending") {
				//add entry into database with pending status, include txn_id
			} else {
				//various status events that mean you didn't get paid (Failed/Denied/Refunded/Voided/etc)
			}
		}
		else if (strcmp ($res, "INVALID") == 0) {
			// log for manual investigation
			$message = "INVALID IPN\n";
			
			foreach ($_POST as $key => $value){
				$message .= $key . " = " .$value ."\n";
			}
			error_log ($message, 0); // Send to system log
		}
	}
	fclose ($fp);
}
?>

First off we need to let PayPal know we got their notification so we open a socket back up to them and post the data back adding the cmd for validating the message. We fire this off at the very beginning because PayPal expects a response within 30 seconds. I don’t even pull in my config file until after posting so setting up the database connection and initializing variables doesn’t take up precious time. Speaking of which, after that is all set up, the first thing I do is pull in my generic config file which includes the escape_data() function I use right after that. You may think that the data is safe to use coming from a security conscious company such as PayPal. That’s exactly the kind of thinking that leads to security breaches. I also clean the data into a single letter array because typing $_POST over and over gets annoying pretty fast.

There will be two possible responses to your validation: VALID or INVALID. This is in regards to your attempts to validate the IPN message and has nothing to do with the actual transaction. For this example we just log an error to the system, you may want to e-mail it, or do something else (i.e., SMS your webmaster). On the other hand if your validation went though properly you can start dealing with all that POSTed data you originally got back. I cached the status message into a simple variable that we can test against. There’s really only 3/4 messages we’re interested in. 4 total, but two basically mean the same thing, you didn’t get no money so in effect it’s only 3: Success, Pending, Failed. Denied is more of a fine grained version of Failed. Think of it this way, your credit card may fail because you typed in the wrong number, but if it’s denied you didn’t have enough credit to cover the charge and the card issuer won’t let you make the purchase.

That’s it in a nutshell. I gave you a little hint about how to you may want to think about dealing with pending transactions, stash the transaction ID in a field in your database. I recommend not using the column in a boolean manner (if column has value it’s pending, if empty it’s not). You should probably consider storing the transaction ID for every transaction, and setting up a particular meaning for “pending” such as a column called “pending” that then would be boolean (1 == pending; 0 == not pending) or slightly more complex logic of a “paid” column (0 == not paid; 1 == pending; 2 == paid, etc.) There’s really no end to what you can do with the information you get back. Update databases, e-mail, SMS, IM, it’s all about what makes sense for your site.

Closing

By now you should be able to leverage the power that can be had by combining two of PayPals features with some solid code. Without having to get into the esoterica of the full XML API you can build a really robust payment processing system. As a bonus you don’t have to pay those high prices for an SSL certificate or having to set up your server to use them. Also when testing your site I highly recommend getting a developer account from PayPal, again the account is free. You’ll be able to create as many fake users as you want to, sellers and buyers of any account type (even the ones you normally have to pay for) and play around with all the various settings. During testing you simply change all the calls from www.paypal.com to www.sandbox.paypal.com then to go live pull out the .sandbox and you’ll know everything works. There’s even a handy IPN Simulator that you can use to send your callback script various types of possible replies to make sure it handles the data properly. One thing I like to do is use the tool along with error_log() calls and then set up a terminal with tail -f /var/log/apache2/error_log to get around the inability to do “print debugging”. This will save you from sending debug messages to a file or e-mail. Of course, while testing in the sandbox you’ll need to keep your certs/cert_id’s correct for that environment and remember to switch them for the live values later. I’d like to thank Stellar Web Solutions for their article that I originally ran across over a year ago and used to base my function on.

Leave a Reply

You must be logged in to post a comment.