q-koa 13.8.37 → 13.8.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -91,7 +91,13 @@ const sendWxFail = (ctx, msg = 'FAIL') => {
|
|
|
91
91
|
* @param {string} options.md5Key MD5 签名密钥(signType=MD5 时使用)
|
|
92
92
|
* @returns {boolean}
|
|
93
93
|
*/
|
|
94
|
-
const validateUnionNotifySign = ({
|
|
94
|
+
const validateUnionNotifySign = ({
|
|
95
|
+
notifyData,
|
|
96
|
+
signType,
|
|
97
|
+
publicKey,
|
|
98
|
+
md5Key,
|
|
99
|
+
fallbackPlatformPublicKey,
|
|
100
|
+
}) => {
|
|
95
101
|
if (!notifyData || !notifyData.sign) return false
|
|
96
102
|
const params = { ...notifyData }
|
|
97
103
|
const sign = String(params.sign)
|
|
@@ -118,21 +124,50 @@ const validateUnionNotifySign = ({ notifyData, signType, publicKey, md5Key }) =>
|
|
|
118
124
|
return expected === sign.toUpperCase()
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
// RSA 双算法兜底:先试 RSA-SHA256(通联 2048-bit 新平台证书),失败再退 RSA-SHA1(1024-bit 老平台证书)
|
|
128
|
+
// 两份证书的签名/密钥长度天然不同,1024-bit 公钥永远验不出 256 字节签名,2048-bit 公钥也验不出 128 字节签名,
|
|
129
|
+
// 所以顺序上即使「错算法试一次」也不会误判,只是多一次 CPU crypto 调用(微秒级,可忽略)。
|
|
130
|
+
// 尝试用户配置的 union_publicKey(兼容无头尾/带PEM一行/带PEM多行;SHA256 先 SHA1 后)
|
|
131
|
+
if (publicKey) {
|
|
132
|
+
let pem = String(publicKey)
|
|
133
|
+
if (!pem.includes('-----BEGIN')) {
|
|
134
|
+
pem = `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----`
|
|
135
|
+
} else if (!pem.includes('\n')) {
|
|
136
|
+
pem = pem
|
|
137
|
+
.replace('-----BEGIN PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----\n')
|
|
138
|
+
.replace('-----END PUBLIC KEY-----', '\n-----END PUBLIC KEY-----')
|
|
139
|
+
}
|
|
140
|
+
for (const alg of ['RSA-SHA256', 'RSA-SHA1']) {
|
|
141
|
+
try {
|
|
142
|
+
const verify = crypto.createVerify(alg)
|
|
143
|
+
verify.update(signStr, 'utf8')
|
|
144
|
+
if (verify.verify(pem, sign, 'base64')) return true
|
|
145
|
+
} catch (e) {}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 兜底:通联官方生产平台公钥(1024-bit)+ RSA-SHA1。
|
|
149
|
+
// 这是因为 union_publicKey 配置通常是商户自己生成的应用公钥,用于商户端请求签名的「请求方向」,
|
|
150
|
+
// 而通联平台异步通知的「回调方向」签名用的是通联自己掌握的另一对密钥(老平台是 1024-bit SHA1)。
|
|
151
|
+
if (
|
|
152
|
+
fallbackPlatformPublicKey &&
|
|
153
|
+
(!publicKey || String(publicKey) !== String(fallbackPlatformPublicKey))
|
|
154
|
+
) {
|
|
155
|
+
try {
|
|
156
|
+
const pem = `-----BEGIN PUBLIC KEY-----\n${fallbackPlatformPublicKey}\n-----END PUBLIC KEY-----`
|
|
157
|
+
const verify = crypto.createVerify('RSA-SHA1')
|
|
158
|
+
verify.update(signStr, 'utf8')
|
|
159
|
+
if (verify.verify(pem, sign, 'base64')) return true
|
|
160
|
+
} catch (e) {}
|
|
133
161
|
}
|
|
162
|
+
return false
|
|
134
163
|
}
|
|
135
164
|
|
|
165
|
+
// 通联支付官方生产环境 1024-bit 平台公钥(用于回调验签兜底,与 allinpay.js 内置 publicKey.production 保持一致)
|
|
166
|
+
// 商户的 union_publicKey 配置通常是自己生成的 2048-bit 应用公钥(给商户侧请求签名用),和通联平台用于
|
|
167
|
+
// 异步通知签名的私钥不是同一对,所以当 union_publicKey 验签失败时,再用这张通联官方平台公钥补一轮。
|
|
168
|
+
const UNION_PROD_PLATFORM_PUBLIC_KEY =
|
|
169
|
+
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCm9OV6zH5DYH/ZnAVYHscEELdCNfNTHGuBv1nYYEY9FrOzE0/4kLl9f7Y9dkWHlc2ocDwbrFSm0Vqz0q2rJPxXUYBCQl5yW3jzuKSXif7q1yOwkFVtJXvuhf5WRy+1X5FOFoMvS7538No0RpnLzmNi3ktmiqmhpcY/1pmt20FHQQIDAQAB'
|
|
170
|
+
|
|
136
171
|
const check = ({ timestamp, nonce, signature, token }) => {
|
|
137
172
|
const tmp = [token, timestamp, nonce].sort().join('')
|
|
138
173
|
const currSign = crypto.createHash('sha1').update(tmp).digest('hex')
|
|
@@ -1569,6 +1604,7 @@ exports.union_notify = async (ctx) => {
|
|
|
1569
1604
|
signType: rawSignType,
|
|
1570
1605
|
publicKey: union_publicKey,
|
|
1571
1606
|
md5Key: union_md5Key,
|
|
1607
|
+
fallbackPlatformPublicKey: UNION_PROD_PLATFORM_PUBLIC_KEY,
|
|
1572
1608
|
})
|
|
1573
1609
|
if (!signOk) {
|
|
1574
1610
|
// ===== 诊断信息:帮助定位到底是 publicKey 错、签名串错、还是配置根本没拿到值 =====
|
|
@@ -1581,22 +1617,8 @@ exports.union_notify = async (ctx) => {
|
|
|
1581
1617
|
app,
|
|
1582
1618
|
message: `通联回调验签失败【详细诊断】type=${rawSignType} cusid=${cusid} appid=${notifyData.appid} trxid=${notifyData.trxid} | union_publicKey=${keyTypeInfo} | union_publicKey_isSet=!!${!!union_publicKey} | union_md5Key_isSet=!!${!!union_md5Key} | signStr_len=${diagsignStr.length} | signStr=${diagsignStr} | raw_data=${JSON.stringify(notifyData)}`,
|
|
1583
1619
|
})
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
// 避免无 sign 的伪造报文直接进业务。
|
|
1587
|
-
if (
|
|
1588
|
-
trxstatus === '0000' &&
|
|
1589
|
-
union_mchId &&
|
|
1590
|
-
cusid === union_mchId
|
|
1591
|
-
) {
|
|
1592
|
-
app.service.log.push({
|
|
1593
|
-
app,
|
|
1594
|
-
message: `通联回调验签临时BYPASS: trxid=${notifyData.trxid} out_trade_no=${notifyData.cusorderid} (24h内确认后删除此逻辑)`,
|
|
1595
|
-
})
|
|
1596
|
-
} else {
|
|
1597
|
-
ctx.body = 'FAIL'
|
|
1598
|
-
return
|
|
1599
|
-
}
|
|
1620
|
+
ctx.body = 'FAIL'
|
|
1621
|
+
return
|
|
1600
1622
|
}
|
|
1601
1623
|
}
|
|
1602
1624
|
|
|
@@ -67,10 +67,15 @@ class AllinPay {
|
|
|
67
67
|
const signStr = this.buildSignStr(params)
|
|
68
68
|
|
|
69
69
|
if (this.signType === 'RSA') {
|
|
70
|
-
//
|
|
70
|
+
// PEM 标准化:已有头尾直接用(缺换行补换行);否则默认包 PKCS#1 (BEGIN RSA PRIVATE KEY) 头尾
|
|
71
71
|
let pem
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const raw = String(this.privateKey)
|
|
73
|
+
if (raw.includes('-----BEGIN')) {
|
|
74
|
+
pem = raw.includes('\n')
|
|
75
|
+
? raw
|
|
76
|
+
: raw
|
|
77
|
+
.replace(/-----(BEGIN[^-]+KEY)-----/, '-----$1-----\n')
|
|
78
|
+
.replace(/-----(END[^-]+KEY)-----/, '\n-----$1-----')
|
|
74
79
|
} else {
|
|
75
80
|
pem = `-----BEGIN RSA PRIVATE KEY-----\n${this.privateKey}\n-----END RSA PRIVATE KEY-----`
|
|
76
81
|
}
|
|
@@ -99,15 +104,24 @@ class AllinPay {
|
|
|
99
104
|
|
|
100
105
|
const signStr = this.buildSignStr(paramsCopy)
|
|
101
106
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
// PEM 标准化:无头尾则包 SPKI 头尾 + 内容单换行;已有头尾若一行无换行补换行
|
|
108
|
+
let pem = String(this.publicKey)
|
|
109
|
+
if (!pem.includes('-----BEGIN')) {
|
|
110
|
+
pem = `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----`
|
|
111
|
+
} else if (!pem.includes('\n')) {
|
|
112
|
+
pem = pem
|
|
113
|
+
.replace('-----BEGIN PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----\n')
|
|
114
|
+
.replace('-----END PUBLIC KEY-----', '\n-----END PUBLIC KEY-----')
|
|
115
|
+
}
|
|
116
|
+
// RSA 双算法兜底:先 SHA256(新 2048-bit 证书),失败再 SHA1(老 1024-bit 证书)
|
|
117
|
+
for (const alg of ['RSA-SHA256', 'RSA-SHA1']) {
|
|
118
|
+
try {
|
|
119
|
+
const verify = crypto.createVerify(alg)
|
|
120
|
+
verify.update(signStr, 'utf8')
|
|
121
|
+
if (verify.verify(pem, sign, 'base64')) return true
|
|
122
|
+
} catch (e) {}
|
|
123
|
+
}
|
|
124
|
+
return false
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
async request(url, params) {
|