1. 程式人生 > >nodejs 如何通過API 證書(權威CA頒發)下載敏感資訊加密公鑰證書?

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… "
            }
        }
    ]
}

“加密後的證書內容”的解密演算法:

下面詳細描述對通知資料進行解密的流程

  1. 從微信支付商戶平臺上獲取商戶的 APIv3金鑰,記為“key”。
  2. 針對“algorithm”中描述的演算法(目前為“AEAD_AES_256_GCM”),取得對應的引數“nonce”和“associated_data”。
  3. 使用“key”、“nonce”和“associated_data”,對資料密文“ciphertext”進行解密,得到平臺證書的原文。
  4. 將原文寫入檔案,使用該檔案對敏感欄位進行加密。

注: “AEAD_AES_256_GCM”演算法的介面細節,請參考 rfc5116。微信支付使用的金鑰“key”長度為 32 個位元組,隨機串“nonce”長度 12 個位元組,“associated_data”長度小於 16 個位元組並可能為空。

很多程式語言支援 “AEAD_AES_256_GCM”演算法,如 java 語言中的 Cipher、SecretKey、GCMParameterSpec、Base64 等類。

官方說明pdf文件看完了,現在可以來捋一下步驟了:

  1. 通過證書私鑰字元對報文進行SHA256 with RSA簽名
  2. 將簽名與商戶號、請求隨機串、時間戳、商戶證書序列號一起,構建Authorization 頭
  3. 往介面地址 https://api.mch.weixin.qq.com/v3/certificates 傳送GET請求,請求時HTTP頭需要包括Accept、Content-Type、User-Agent、Authorization等
  4. 對返回值中的“encrypt_certificate.ciphertext”進行 “AEAD_AES_256_GCM”演算法解密
  5. 儲存解密所得的明文為敏感資訊加密公鑰證書

簡單點,直接上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();		
});