使用OpenSSL做RSA簽名驗證 支付寶移動快捷支付 的server異步通知
因為業務須要。我們須要使用支付寶移動快捷支付做收款。支付寶給了我們《移動快捷支付應用集成接入包支付接口》見支付寶包《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異步通知