nodejs 如何通過API 證書(權威CA頒發)下載敏感資訊加密公鑰證書?
在服務商平臺的API介面中,有部分介面在傳參時,需要對引數中的敏感資訊進行RSA加密(如:小微商戶申請入駐、小微商戶修改結算資訊等)。在這些介面的引數加密說明中,是這樣註明的:
加密方法詳見敏感資訊加密方法說明(該md檔案中的變數PUBLIC_KEY_FILENAME是表示平臺證書,即為證書及其序列號獲取方法說明PDF文件中1.1.5小節中的”加密後的證書內容encrypt_certificate.ciphertext"解密後的明文。) |
因此,我們在對敏感資訊加密前,需要先獲得ciphertext,然後對ciphertext進行解密,解密出來的明文就是加密敏感資訊所需的加密公鑰了,將該公鑰儲存為檔案就是公鑰證書啦(該公鑰證書有效期為5年,不過微信支付要求“中控伺服器需要定時查詢商戶的平臺證書列表,查詢間隔應小於 12 小時,並及時下載新的平臺證書。下載證書時,需與本地證書序列表對比,如果發現有新增證書序列號,那就是需要新換的證書。老證書需要在被棄用前及時清理掉”)
關於獲取公鑰的具體說明可以參閱騰訊官方提供的pdf文件 《證書及其序列號獲取方法說明 》 中的1.1.3.3 以外的章節。
1.1.2. 介面地址
請求 Url |
https://api.mch.weixin.qq.com/v3/certificates |
請求方式 |
GET |
1.1.3. 介面呼叫規則
- 非必填欄位的值如果為空,請求報文裡面不能傳遞該引數,否則會報錯
- 微信支付側有可能在不破壞協議相容性的前提下,增加請求引數或者應答物件中的欄位。商戶應當相容未來可能加入的新欄位。
- 認證方式:HTTPS 認證,SHA256 with RSA 簽名
- 字符集預設使用 UTF-8,請勿使用其它字符集
- 商戶與微信之間的互動(特別是支付通知回撥),都需要驗證簽名
- 處理返回時先判斷 HTTP 狀態碼,再判斷返回資料中的錯誤碼,才能確定交易狀態
- 返回和提交資料的簽名,商戶號,時間戳,隨機串等在 HTTP 頭中傳遞
- HTTP 請求頭設定規則如下:
請求頭 |
必填 |
說明 |
Accept |
是 |
應答的格式。目前僅支援:application/json |
Accept-Language |
否 |
應答的區域語言。目前支援:en,zh-CN,zh-HK,zh-TW,不傳則預設是:zh-CN 。詳細請參考設定錯誤描述語言章節 |
Authorization |
是 |
含有伺服器用於驗證商戶身份的憑證。詳細資訊請參考簽名生成方法章節 |
Content-Type |
是 |
請求資料(Body)的格式。當請求包含請求資料時必填。目前僅支援:application/json |
User-Agent |
是 |
發起請求的客戶端軟體的標識資訊 |
1.1.3.1. Authorization 的構造方式
微信支付要求請求通過 HTTP Authorization 頭來傳遞簽名。Authorization 由認證型別和簽名信息兩個部分組成。
Authorization: 認證型別 簽名信息
具體組成為:
認證型別:目前為 WECHATPAY2-SHA256-RSA2048
簽名信息:
商戶號 mchid
請求隨機串 nonce_str
簽名值 signature(詳見 1.1.3.2 計算簽名值方法)
時間戳 timestamp
商戶證書序列號 serial_no(詳見 1.1.3.4 獲取商戶證書序列號方法)
Authorization 頭的示例如下:(注意,示例因為排版可能存在換行,實際資料應在一行,mchid 前有一個空格)
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="10000100",nonce_str="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg",signature="hDV4aXhMvfZ31NABElvWHWuxYiR7lB1sjzcpldpWul/62o75d90l5oznquE+uVORPESfzBpCdtU6IiL+1Cdy3rG01sKXrWfFnjr4jm/imFxbq8BbVpE+HbrRXkR/jrc6gqSVuIjJfXSMK1yL5G35WgUWzWdAKiV3ELQk/sSYrhnOiulve/xM2bJvYFQDl/dvMazxW930JLm0lv1tEMuHuqcx5WN+1fq3VJ+J9UvwVTjQT8eXmHAzaYxXHEoDyN2T5/AVzZTuzcCt1cFk5Sj/tNUvDMklxy+eF7hOUCFzo98Z42OsdpC3GV02mYOApeNwVB7I5fCB//jerFqf9/VjA==",timestamp="1507709632",serial_no="345D5C1DB746787546E06E6DAD9E5BE987CEDFCF"
1.1.3.2. 計算簽名值方法
構造待簽名串
在運用具體的簽名演算法前,商戶需要先構造待簽名串。
第一步,獲取 HTTP 請求的方法(GET,POST,PUT 等)
GET
第二步,獲取請求的 URL,並去除域名的部分,如果連結帶引數,引數值必須進行 URLencode。示例請求的 URL 為
/v3/certificates
第三步,生成一個請求隨機串,演算法可開發者自定義(可呼叫系統隨機數生成函式轉化成字串),建議長度不少於 10 位。
kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg
第四步,獲取發起請求時的系統當前時間戳,即格林威治時間 1970 年 01 月 01 日 00 時 00 分 00 秒(北京時間 1970 年 01 月 01 日 08 時 00 分 00 秒)起至現在的總秒數,作為請求時間戳。時間戳必須是最新的,如果時間戳比微信支付伺服器時間晚 300 秒,微信支付伺服器會不認這個請求並報錯,請商戶保持自身系統的時間準確。
1507709906
第五步,獲取提交資料。注:當請求方法為 GET 時,請求報文為空。
第六步,按照如下方法,組成待簽名串。待簽名串共有五行,每行包括一個引數,行尾以\n 結束,包括最後一行。請注意,\n 為換行符(ASCII 編碼值為 0x0A)。
HTTP 請求方法\n
URL\n
請求時間戳\n
請求隨機串\n
請求報文\n
按照以上規則,請求報文的待簽名串為:
GET
/v3/certificates
1507709906
kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg
請注意,當請求方法為 GET 時請求報文為空,最後一行僅為一個換行符。
因此可以定義簽名串變數
String signContent=“GET\n/v3/certificates\n1507709906\nkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg\n\n”
計算簽名值
1) 如上方法,得到簽名串變數 signContent
超級管理員登入商戶平臺,在“賬戶中心”->“API 安全”->”API 證書(權威 CA 頒發)”中申請 API 商戶證書,申請過程中會獲取到私鑰證書檔案(申請流程詳見 1.1.3.3“申請 API 商戶證書“),開啟私鑰檔案獲取私鑰字元(定義變數 string sKey)
3) 設定 APIv3 金鑰
4) 很多程式語言支援簽名函式,建議商戶優先呼叫該類函式,使用商戶證書私鑰(sKey)對待簽名串(signContent)進行 SHA256 with RSA 簽名,並對簽名結果進行 Base64 編碼得到簽名值。(如 java 語言提供了 PKCS8EncodedKeySpec、KeyFactory、Base64、PrivateKey 和 Signature 等類)
。。。。。。
1.1.4. 請求引數
請求示例:
curl -v -X GET "https://api.mch.weixin.qq.com/v3/certificates" WECHATPAY2-SHA256-RSA2048 -H 'Authorization:mchid="10000100",nonce_str="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg",signature="hDV4aXhMvfZ31NABElvWHWuxYiR7lB1sjzcpldpWul/62o75d90l5oznquE+uVORPESfzBpCdtU6IiL+1Cdy3rG01sKXrWfFnjr4jm/imFxbq8BbVpE+HbrRXkR/jrc6gqSVuIjJfXSMK1yL5G35WgUWzWdAKiV3ELQk/sSYrhnOiulve/xM2bJvYFQDl/dvMazxW930JLm0lv1tEMuHuqcx5WN+1fq3VJ+J9UvwVTjQT8eXmHAzaYxXHEoDyN2T5/AVzZTuzcCt1cFk5Sj/tNUvDMklxy+eOF7hOUCFzo98Z42OsdpC3GV02mYOApeNwVB7I5fCB//jerFqf9/VjA==",timestamp="1507709632",serial_no="345D5C1DB746787546E06E6DAD9E5BE987CEDFCF"' -H 'Accept-Language:' -d -H 'Content-Type:application/json' -H 'Accept:application/json' -H 'User-Agent: curl/7.54.0'
1.1.5. 返回結果
異常返回:
名稱 |
變數名 |
必填 |
型別 |
示例值 |
描述 |
返回狀態碼 |
code |
是 |
string(32) |
INVALID_REQUEST |
錯誤碼,列舉值見錯誤碼列表 |
返回資訊 |
message |
否 |
string(256) |
引數格式校驗錯誤 |
返回資訊,如非空,為錯誤原因 |
正常返回:
名稱 |
變數名 |
必填 |
型別 |
示例值 |
描述 |
加密的平臺證書序列號 | serial_no | 是 | string(40) |
5157F09EFDC096DE1 5EBE81A47057A7232F1B8E1 |
證書的序列號 |
證書啟用時間 | effective_time | 是 | string(32) | 2018-06-08T10:34:56+08:00 | 啟用證書的時間,時間格式為?RFC3339。每個平臺證書的啟用時間是固定的。 |
證書棄用時間 | expire_time | 是 | string(32) | 2018-06-08T10:34:56+08:00 | 棄用證書的時間,時間格式為?RFC3339。更換平臺證書前,會提前24 小時修改老證書的棄用時間,介面返回新老兩個平臺證書。更換完成後,介面會返回最新的平臺證書。 |
加密證書的演算法 |
encrypt_certificat e.algorithm |
是 | string(32) | AEAD_AES_256_GCM | 加密證書的演算法,金鑰為APIv3 KEY, 需要登入商戶平臺設定 |
加密證書的隨機串 |
encrypt_certificat e.nonce |
是 | string(12) | 61f9c719728a | 加密證書的隨機串 |
關聯資料 |
encrypt_certificat e.associated_data |
是 | string(32) | certificate | 固定值: certificate |
加密後的證書內容 |
encrypt_certificat e.ciphertext |
是 | string(344) |
Y1IPF0kyPUySt2tRe+aJ7TK6c w08pqiXPr1g/agxl16AYarlrcsdq 1P8gcJc4iVkQfYouooRJdF4Eo….. |
使用 APIv3 KEY 和上述引數,可以解密出平臺證書的明文。證書明文為PEM 格式。(注意:更換證書時會出現 PEM格式中的證書失效時間與介面返回的證書棄用時間不一致的情況) |
舉例如下:
{
"data":[
{
"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
"effective_time ":"2018-06-08T10:34:56+08:00",
"expire_time ":"2018-12-08T10:34:56+08:00",
"encrypt_certificate":{
"algorithm":"AEAD_AES_256_GCM",
"nonce":"61f9c719728a",
"associated_data":"certificate",
"ciphertext":"sRvt… "
}
},
{
"serial_no":"50062CE505775F070CAB06E697F1BBD1AD4F4D87", //這個證書序列號在小微商戶申請入駐介面呼叫時需要用到
"effective_time ":"2018-12-07T10:34:56+08:00",
"expire_time ":"2020-12-07T10:34:56+08:00",
"encrypt_certificate":{
"algorithm":"AEAD_AES_256_GCM",
"nonce":"35f9c719727b",
"associated_data":"certificate",
"ciphertext":"aBvt… "
}
}
]
}
“加密後的證書內容”的解密演算法:
下面詳細描述對通知資料進行解密的流程
- 從微信支付商戶平臺上獲取商戶的 APIv3金鑰,記為“key”。
- 針對“algorithm”中描述的演算法(目前為“AEAD_AES_256_GCM”),取得對應的引數“nonce”和“associated_data”。
- 使用“key”、“nonce”和“associated_data”,對資料密文“ciphertext”進行解密,得到平臺證書的原文。
- 將原文寫入檔案,使用該檔案對敏感欄位進行加密。
注: “AEAD_AES_256_GCM”演算法的介面細節,請參考 rfc5116。微信支付使用的金鑰“key”長度為 32 個位元組,隨機串“nonce”長度 12 個位元組,“associated_data”長度小於 16 個位元組並可能為空。
很多程式語言支援 “AEAD_AES_256_GCM”演算法,如 java 語言中的 Cipher、SecretKey、GCMParameterSpec、Base64 等類。
官方說明pdf文件看完了,現在可以來捋一下步驟了:
- 通過證書私鑰字元對報文進行SHA256 with RSA簽名
- 將簽名與商戶號、請求隨機串、時間戳、商戶證書序列號一起,構建Authorization 頭
- 往介面地址 https://api.mch.weixin.qq.com/v3/certificates 傳送GET請求,請求時HTTP頭需要包括Accept、Content-Type、User-Agent、Authorization等
- 對返回值中的“encrypt_certificate.ciphertext”進行 “AEAD_AES_256_GCM”演算法解密
- 儲存解密所得的明文為敏感資訊加密公鑰證書
簡單點,直接上nodejs程式碼。
var https = require("https");
var crypto = require('crypto');
app.get('/wpayGenMgPkey',function(req,res){
//1、通過證書私鑰通過證書私鑰字元對報文進行SHA256 with RSA簽名
var pcert = '-----BEGIN PRIVATE KEY-----\n這裡對應新的API 證書(權威CA頒發)中的私鑰檔案字串\n-----END PRIVATE KEY-----';
var now = parseInt(Date.now() / 1000);
var rdm = parseInt(Math.random() * Math.pow(2, 64));
var plainText = 'GET\n/v3/certificates\n' + now + '\n' + rdm + '\n\n';
var data = new Buffer(plainText,'utf8');
var sign = crypto.createSign("RSA-SHA256");
sign.update(data);
var signStr = sign.sign(pcert, 'base64');
var mch_id = "這裡對應服務商商戶號";
//2、將簽名與商戶號、請求隨機串、時間戳、商戶證書序列號一起,構建Authorization 頭
var Auth = 'WECHATPAY2-SHA256-RSA2048 mchid="' + mch_id + '",nonce_str="' + rdm + '",signature="' + signStr + '",timestamp="' + now + '",serial_no="這裡對應新的API 證書(權威CA頒發)中的證書序列號"';
//3、往介面地址 https://api.mch.weixin.qq.com/v3/certificates 傳送GET請求,請求時HTTP頭需要包括Accept、Content-Type、User-Agent、Authorization等
var opts = {
method:'GET',
hostname:'api.mch.weixin.qq.com',
port:'443',
pfx:fs.readFileSync('./cert/這裡對應新的API 證書(權威CA頒發)中p12證書檔案.pfx'), //直接將.p12改字尾名為.pfx即可,此配置可以不填寫
passphrase:mch_id,
path:"/v3/certificates",
host:'api.mch.weixin.qq.com'
}
var body = '';
var rq = https.request(opts,function(rs){
rs.on('data',function(data){
body += data;
})
rs.on('end', function(){
var cJson = JSON.parse(body);
if(cJson.data){
//4、對返回值中的“encrypt_certificate.ciphertext”進行 “AEAD_AES_256_GCM”演算法解密
var nJson = cJson.data[cJson.data.length - 1];
var keys = '這裡對應APIv3金鑰串';
//編碼設定
var clearEncoding = 'binary';
//加密方式
var algorithm = 'aes-256-gcm';
//向量
var iv = nJson.encrypt_certificate.nonce;
//加密型別 base64/hex...
var cipherEncoding = 'hex';
//var cipherEncoding = 'base64';
var cipherChunks = [];
var cdata = nJson.encrypt_certificate.ciphertext;
cdata = new Buffer(cdata,'base64').toString('binary');
var decipher = crypto.createDecipheriv(algorithm, new Buffer(keys, clearEncoding), new Buffer(iv, clearEncoding));
decipher.setAutoPadding(true);
decipher.setAAD(new Buffer(nJson.encrypt_certificate.associated_data, clearEncoding))
var data = new Buffer(cdata,clearEncoding);
var rtn = decipher.update(data, clearEncoding, "utf-8").toString("utf8");
//這裡加上這一句反倒會報錯,不加的話解密出來的內容後面有一部分亂碼,需要剔除
//rtn += decipher.final("utf-8");
rtn = rtn.split('-----END CERTIFICATE-----')[0] + '-----END CERTIFICATE-----';
rtn = rtn.replace(/\n/g,'\\n')
//5、儲存解密所得的明文為敏感資訊加密公鑰證書(這裡請自己儲存)
res.send(rtn);
}else{
res.send(body);
}
})
});
//注意:header必須通過setHeader函式寫入,不能直接在opts中寫
rq.setHeader("Authorization",Auth);
rq.setHeader("Accept","application/json");
rq.setHeader("Accept-Language",'zh-CN');
rq.setHeader("Content-Type","application/json");
rq.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2763.0 Safari/537.36");
//請求內容為空
rq.write('');
rq.on('error',function(err){
if(fn){fn("<return_msg>" + err.message + "</return_msg>")}
});
rq.end();
});