1. 程式人生 > >使用OpenSSL做RSA簽名驗證 支付寶移動快捷支付 的server異步通知

使用OpenSSL做RSA簽名驗證 支付寶移動快捷支付 的server異步通知

.com cond 排序 一個 string url base64編碼 sig const

因為業務須要。我們須要使用支付寶移動快捷支付做收款。支付寶給了我們《移動快捷支付應用集成接入包支付接口》見支付寶包《WS_SECURE_PAY_SDK》。

支付寶給的serverdemo僅僅有Java、C#、PHP三種,而我們server端使用的是C++。

這當中就涉及到接收支付寶的server異步通知。為了確保接收到的server異步通知來至支付寶,我們就必須驗證支付寶的簽名。

坑爹的是,原來PC端使用MD5做簽名,預計支付寶考慮到移動端的風險更高,於是改用RSA做移動快捷支付應用的簽名。這無疑添加了我們遷移到移動端的開發成本。

支付寶文檔中說明是使用openssl,我們這邊就決定使用openssl做rsa簽名驗證。

因為第一次使用openssl做RSA驗證簽名,我們碰到了各種坑,為了避免其它項目也碰到類似問題。分享例如以下:


首先要說明的是RSA簽名和簽名驗證的過程。

RSA簽名的過程(支付寶操作)例如以下:對須要簽名的字符串按key的字母升序排序,使用=和&連接,形成一個簽名字符串。

對該字符串做摘要(能夠使用MD5或者SHA1,支付寶使用的是SHA1),然後對摘要字符串(即接口中的hash參數)使用支付寶私鑰做RSA加密,獲得加密字符串,即為簽名字符串(放在sign中),設置sign_type=RSA。

這樣,就完畢了發送字符串的簽名。

RSA簽名驗證的過程(我們第三方企業操作)例如以下:接收到發送過來的字符串(假設字符串沒有做url decode解碼。須要做url decode解碼)。拆分為key、value對,依照支付寶的文檔。依據key的字母升序排序,使用=和&鏈接,獲得被簽名字符串。

被簽名字符串做SHA1摘要算法,獲得SHA1摘要字符串。假設sign_type=RSA。先將sign字段做base64解碼,然後使用支付寶公鑰做RSA解密。得到SHA1摘要字符串。比較兩個SHA1摘要字符串,假設SHA1摘要字符串一致,則簽名驗證成功。

特別說明的是:支付寶的公鑰字符串為以-----BEGIN PUBLIC KEY-----\n開始,以\n-----END PUBLIC KEY-----\n結束,中間的字符串須要每64個字符換行一次,即為:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRA
FljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQE
B/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5Ksi
NG9zpgmLCUYuLkxpLQIDAQAB
-----END PUBLIC KEY-----

理論說完了,再解釋一下使用的函數吧。


驗證簽名函數為:int verifyAlipayNotify(const std::string& recvString, const std::string& alipayPublicKey);

recvString為接收的字符串,未做urldecode。

alipayPublicKey為本地內存中存儲的支付寶公鑰,已經保證包括特殊說明的條件。


使用的openssl函數例如以下:

int RSA_verify(int type, const unsigned char *m, unsigned int m_length,
const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);

type 使用何種摘要算法,這裏因為使用的是SHA1算法,填寫NID_sha1

m 摘要字符串

m_length 摘要字符串長度

sigbuf 支付寶返回的簽名,已經做了base64解碼

siglen 支付寶返回的簽名長度,這裏應該為128

rsa openssl的RSA密鑰結構體,這裏由支付寶公鑰轉化而來的

返回值:負數為運行錯誤,0為簽名驗證失敗(預計是有黑客攻擊你),1為簽名驗證成功


verifyAlipayNotify代碼例如以下:

#include <openssl/rsa.h>
#include <openssl/sha.h>
#include <openssl/md5.h>
#include <openssl/rand.h>
#include <openssl/objects.h>
#include <openssl/pem.h>
#include <openssl/bio.h>

#include <string>
#include <map>

#include "urlcodec.h"
#include "base64.h"

struct ltstr
{
	bool operator()(std::string s1, std::string s2) const{return (s1.compare(s2) < 0);}
};

int verifyString(const std::string& signString, const std::string& sign, const std::string& alipayPublicKey)
{
	//獲得支付寶的簽名字節串
	char szSign[128];
	unsigned long szSignLen = 128;
	bool decodeResult = CBase64::Decode(sign, (unsigned char*)szSign, &szSignLen);//CBase::Decode是Base64解碼函數,您能夠在網上隨便下載一個
	if(!decodeResult)
	{
		return -1;
	}

	//獲得SHA1摘要字符串
	unsigned char sha1Origin[20];
	SHA1((unsigned char*)signString.c_str(), signString.size(), sha1Origin);

	//由支付寶公鑰內存字符串轉化為openssl的RSA結構
	BIO* memBIO = NULL;
	memBIO = BIO_new(BIO_s_mem());
	int bioWriteLen = BIO_write(memBIO, alipayPublicKey.c_str(), alipayPublicKey.length());
	RSA* rsa = PEM_read_bio_RSA_PUBKEY(memBIO, NULL, NULL, NULL);
    if(NULL == rsa)
    {
        return -2;
    }
	//簽名驗證
	int verifyResult = RSA_verify(NID_sha1, sha1Origin, SHA_DIGEST_LENGTH, (unsigned char*)szSign, szSignLen, rsa);
	return verifyResult;
}

int verifyAlipayNotify(const std::string& alipayNotifyData, const std::string& alipayPublicKey)
{
	std::string strAlipayNotifyData = alipayNotifyData;
	std::string sign;
	std::map<std::string, std::string, ltstr> omap;
	std::string::size_type pos = strAlipayNotifyData.find("&");
	while(std::string::npos != pos)
	{
		std::string one = strAlipayNotifyData.substr(0, pos);
		std::string::size_type subpos = one.find("=");
		if(std::string::npos != subpos)
		{
			std::string key = one.substr(0, subpos);
			if("sign_type" != key && "sign" != key)
			{
				std::string value = one.substr(subpos+1);
				std::string newValue = UrlDecode(value);//UrlDecode是URL解碼函數。您能夠在網上隨便下載一個
				omap.insert(std::make_pair(key, newValue));
			}
			else if("sign" == key)
			{
				sign = UrlDecode(one.substr(subpos+1));
			}
		}

		strAlipayNotifyData = strAlipayNotifyData.substr(pos + 1);
		pos = strAlipayNotifyData.find("&");
	}
	std::string::size_type subpos = strAlipayNotifyData.find("=");
	if(std::string::npos != subpos)
	{
		std::string key = strAlipayNotifyData.substr(0, subpos);
		if("sign_type" != key && "sign" != key)
		{
			std::string value = strAlipayNotifyData.substr(subpos+1);
			std::string newValue = UrlDecode(value);
			omap.insert(std::make_pair(key, newValue));
		}
		else if("sign" == key)
		{
			sign = UrlDecode(strAlipayNotifyData.substr(subpos+1));
		}
	}

	//獲得支付寶被簽名字符串
	std::string signString = "";
	std::map<std::string, std::string, ltstr>::iterator itr = omap.begin();
	for(; itr != omap.end(); ++itr)
	{
		signString += itr->first;
		signString += "=";
		signString += itr->second;
		signString += "&";
	}
	if(!signString.empty())
	{
		signString.erase(signString.length() - 1);
	}

	return verifyString(signString, sign, alipayPublicKey);
}

有時候,你本地存儲的公鑰是沒有包括頭尾的,如

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRAFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQEB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5KsiNG9zpgmLCUYuLkxpLQIDAQAB

為此。提供一個函數支持轉化為完整公鑰的函數:

std::string completeAlipayPublicKey(std::string strPublicKey)
{
	int nPublicKeyLen = strPublicKey.size();      //strPublicKey為base64編碼的公鑰字符串
	for(int i = 64; i < nPublicKeyLen; i+=64)
	{
		if(strPublicKey[i] != ‘\n‘)
		{
			strPublicKey.insert(i, "\n");
		}
		i++;
	}
	strPublicKey.insert(0, "-----BEGIN PUBLIC KEY-----\n");
	strPublicKey.append("\n-----END PUBLIC KEY-----\n");
	return strPublicKey;
}

最後,測試代碼例如以下:
int main(int argc, char **argv)
{

	std::string strPublicKey = "**********";
	std::string strAlipayData = "**********";
	std::string strCompletePublicKey = completeAlipayPublicKey(strPublicKey);
	int result = verifyAlipayNotify(strAlipayData, strCompletePublicKey);
	if(1 == result)
	{
		printf("verify sign ok!\n");
	}
	else if(0 == result)
	{
		printf("mock alipay notify data");
	}
	else
	{
		printf("error\n");
	}
	return 0;
}






使用OpenSSL做RSA簽名驗證 支付寶移動快捷支付 的server異步通知