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

2) 获取商户证书私钥。请参照博文微信支付服务商API 证书(权威CA颁发)是做什么用的?

超级管理员登录商户平台,在“账户中心”->“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();		
});

猜你喜欢

转载自blog.csdn.net/yyoinge/article/details/81561204
今日推荐