在服务商平台的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… "
}
}
]
}
“加密后的证书内容”的解密算法:
下面详细描述对通知数据进行解密的流程
- 从微信支付商户平台上获取商户的 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();
});