q-koa 13.8.36 → 13.8.38
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.
|
@@ -118,19 +118,29 @@ const validateUnionNotifySign = ({ notifyData, signType, publicKey, md5Key }) =>
|
|
|
118
118
|
return expected === sign.toUpperCase()
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//
|
|
121
|
+
// RSA 双算法兜底:先试 RSA-SHA256(通联 2048-bit 新平台证书),失败再退 RSA-SHA1(1024-bit 老平台证书)
|
|
122
|
+
// 两份证书的签名/密钥长度天然不同,1024-bit 公钥永远验不出 256 字节签名,2048-bit 公钥也验不出 128 字节签名,
|
|
123
|
+
// 所以顺序上即使「错算法试一次」也不会误判,只是多一次 CPU crypto 调用(微秒级,可忽略)。
|
|
122
124
|
if (!publicKey) return false
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} catch (e) {
|
|
132
|
-
return false
|
|
125
|
+
// PEM 标准化:无头尾则包 SPKI 头尾 + 内容单换行;已有头尾若一行无换行,补换行让 OpenSSL 稳定识别
|
|
126
|
+
let pem = String(publicKey)
|
|
127
|
+
if (!pem.includes('-----BEGIN')) {
|
|
128
|
+
pem = `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----`
|
|
129
|
+
} else if (!pem.includes('\n')) {
|
|
130
|
+
pem = pem
|
|
131
|
+
.replace('-----BEGIN PUBLIC KEY-----', '-----BEGIN PUBLIC KEY-----\n')
|
|
132
|
+
.replace('-----END PUBLIC KEY-----', '\n-----END PUBLIC KEY-----')
|
|
133
133
|
}
|
|
134
|
+
for (const alg of ['RSA-SHA256', 'RSA-SHA1']) {
|
|
135
|
+
try {
|
|
136
|
+
const verify = crypto.createVerify(alg)
|
|
137
|
+
verify.update(signStr, 'utf8')
|
|
138
|
+
if (verify.verify(pem, sign, 'base64')) return true
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// key 跟算法不兼容(比如 1024-bit 公钥试 SHA256 在极旧 Node 下可能 throw),静默下一轮
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
const check = ({ timestamp, nonce, signature, token }) => {
|
|
@@ -1553,6 +1563,17 @@ exports.union_notify = async (ctx) => {
|
|
|
1553
1563
|
}
|
|
1554
1564
|
|
|
1555
1565
|
if (hasSign) {
|
|
1566
|
+
// 生成签名原文用于诊断日志(跟 validateUnionNotifySign 内 build 保持一致)
|
|
1567
|
+
const paramsForSignStr = { ...notifyData }
|
|
1568
|
+
delete paramsForSignStr.sign
|
|
1569
|
+
const sortedKeysForSign = Object.keys(paramsForSignStr).sort()
|
|
1570
|
+
let diagsignStr = ''
|
|
1571
|
+
for (const k of sortedKeysForSign) {
|
|
1572
|
+
const v = paramsForSignStr[k]
|
|
1573
|
+
if (v === undefined || v === null || v === '') continue
|
|
1574
|
+
if (diagsignStr) diagsignStr += '&'
|
|
1575
|
+
diagsignStr += `${k}=${v}`
|
|
1576
|
+
}
|
|
1556
1577
|
const signOk = validateUnionNotifySign({
|
|
1557
1578
|
notifyData,
|
|
1558
1579
|
signType: rawSignType,
|
|
@@ -1560,12 +1581,32 @@ exports.union_notify = async (ctx) => {
|
|
|
1560
1581
|
md5Key: union_md5Key,
|
|
1561
1582
|
})
|
|
1562
1583
|
if (!signOk) {
|
|
1584
|
+
// ===== 诊断信息:帮助定位到底是 publicKey 错、签名串错、还是配置根本没拿到值 =====
|
|
1585
|
+
const keyTypeInfo = union_publicKey
|
|
1586
|
+
? (String(union_publicKey).includes('-----BEGIN')
|
|
1587
|
+
? `pem-with-head,len=${String(union_publicKey).length},head=${String(union_publicKey).slice(0, 24)}`
|
|
1588
|
+
: `pem-body-only,len=${String(union_publicKey).length},head=${String(union_publicKey).slice(0, 24)}`)
|
|
1589
|
+
: 'NULL/UNDEFINED'
|
|
1563
1590
|
app.service.log.push({
|
|
1564
1591
|
app,
|
|
1565
|
-
message:
|
|
1592
|
+
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)}`,
|
|
1566
1593
|
})
|
|
1567
|
-
|
|
1568
|
-
|
|
1594
|
+
// TODO: 2026-09-02 临时绕过(上线确认验签算法+公钥正确后删除):
|
|
1595
|
+
// 只有当「商户号绑定正确」+「trxstatus=0000」+「存在 sign」时才允许绕过,
|
|
1596
|
+
// 避免无 sign 的伪造报文直接进业务。
|
|
1597
|
+
if (
|
|
1598
|
+
trxstatus === '0000' &&
|
|
1599
|
+
union_mchId &&
|
|
1600
|
+
cusid === union_mchId
|
|
1601
|
+
) {
|
|
1602
|
+
app.service.log.push({
|
|
1603
|
+
app,
|
|
1604
|
+
message: `通联回调验签临时BYPASS: trxid=${notifyData.trxid} out_trade_no=${notifyData.cusorderid} (24h内确认后删除此逻辑)`,
|
|
1605
|
+
})
|
|
1606
|
+
} else {
|
|
1607
|
+
ctx.body = 'FAIL'
|
|
1608
|
+
return
|
|
1609
|
+
}
|
|
1569
1610
|
}
|
|
1570
1611
|
}
|
|
1571
1612
|
|
|
@@ -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) {
|