464 lines
17 KiB
JavaScript
464 lines
17 KiB
JavaScript
const sm4 = require('sm-crypto').sm4
|
||
const sm3 = require('sm-crypto').sm3
|
||
const sm2 = require('sm-crypto').sm2
|
||
const crypto = require('crypto')
|
||
const forge = require('node-forge')
|
||
const rs = require('jsrsasign')
|
||
|
||
|
||
module.exports.sm4en3 = function (key, data){
|
||
let sm3key = sm3(key)
|
||
sm3key = sm3key.substring(0, 32)
|
||
let encryptData = sm4.encrypt(data, sm3key)
|
||
return encryptData
|
||
}
|
||
|
||
|
||
module.exports.sm4de3 = function (key, data){
|
||
let sm3key = sm3(key)
|
||
sm3key = sm3key.substring(0, 32)
|
||
let decryptData = sm4.decrypt(data, sm3key)
|
||
return decryptData
|
||
}
|
||
|
||
|
||
/**
|
||
* 从 PKCS7 签名中获取信息(包含证书、公钥和签名值)
|
||
* 支持 RSA 和 SM2 算法
|
||
* @param {Buffer|string} p7sign - PKCS7 签名数据
|
||
* @returns {Object} 包含证书、公钥和签名值的对象
|
||
*/
|
||
module.exports.getInfoFromPKCS7 = function (p7sign) {
|
||
// 处理输入数据
|
||
let base64Data
|
||
if (Buffer.isBuffer(p7sign)) {
|
||
base64Data = p7sign.toString('base64')
|
||
} else if (typeof p7sign === 'string') {
|
||
// 判断是否是 PEM 格式
|
||
if (p7sign.includes('-----BEGIN PKCS7-----')) {
|
||
// 提取 base64 部分
|
||
base64Data = p7sign
|
||
.replace(/-----BEGIN PKCS7-----\n?/, '')
|
||
.replace(/-----END PKCS7-----\n?/, '')
|
||
.replace(/\n/g, '')
|
||
} else {
|
||
// 如果不是 PEM 格式,假设是 base64
|
||
base64Data = p7sign
|
||
}
|
||
} else {
|
||
throw new Error('Invalid input type for p7sign')
|
||
}
|
||
|
||
// 尝试使用 node-forge 解析(支持 RSA)
|
||
try {
|
||
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
|
||
const p7 = forge.pkcs7.messageFromPem(pemData)
|
||
return extractInfoFromForgeP7(p7)
|
||
} catch (forgeError) {
|
||
// 如果 node-forge 解析失败,尝试使用 jsrsasign(支持 SM2)
|
||
try {
|
||
return extractInfoFromJsrsasignP7(base64Data)
|
||
} catch (jsrsasignError) {
|
||
throw new Error(`Failed to parse PKCS7: ${forgeError.message}, ${jsrsasignError.message}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从 node-forge 解析的 PKCS7 中提取信息(RSA)
|
||
*/
|
||
function extractInfoFromForgeP7(p7) {
|
||
// 辅助函数:将属性数组转换为 DN 字符串
|
||
function attrsToDN(attrs) {
|
||
if (!attrs || attrs.length === 0) return ''
|
||
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
|
||
}
|
||
|
||
// 提取证书
|
||
let cert = null
|
||
if (p7.certificates && p7.certificates.length > 0) {
|
||
const certObj = p7.certificates[0]
|
||
cert = {
|
||
subject: certObj.subject.attributes.map(attr => ({
|
||
name: attr.name,
|
||
value: attr.value
|
||
})),
|
||
subjectDN: attrsToDN(certObj.subject.attributes),
|
||
issuer: certObj.issuer.attributes.map(attr => ({
|
||
name: attr.name,
|
||
value: attr.value
|
||
})),
|
||
issuerDN: attrsToDN(certObj.issuer.attributes),
|
||
serialNumber: certObj.serialNumber,
|
||
notBefore: certObj.validity.notBefore,
|
||
notAfter: certObj.validity.notAfter,
|
||
pem: forge.pki.certificateToPem(certObj)
|
||
}
|
||
}
|
||
|
||
// 提取公钥 - 从证书中提取公钥对象并转换为 PEM 和 Hex
|
||
let publicKey = null
|
||
let publicKeyHex = null
|
||
if (p7.certificates && p7.certificates.length > 0) {
|
||
const certObj = p7.certificates[0]
|
||
// 提取公钥对象并转换为 PEM 格式
|
||
if (certObj.publicKey) {
|
||
publicKey = forge.pki.publicKeyToPem(certObj.publicKey)
|
||
// 将公钥转换为 hex 格式
|
||
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(certObj.publicKey)
|
||
const publicKeyDer = forge.asn1.toDer(publicKeyAsn1)
|
||
publicKeyHex = forge.util.bytesToHex(publicKeyDer.getBytes())
|
||
}
|
||
}
|
||
|
||
// 提取签名值 - 从 rawCapture 中获取(已经是原始签名值)
|
||
let signature = null
|
||
let signatureHex = null
|
||
if (p7.rawCapture && p7.rawCapture.signature) {
|
||
// rawCapture.signature 已经是原始签名值字符串
|
||
signature = forge.util.encode64(p7.rawCapture.signature)
|
||
// 同时提供 hex 格式
|
||
signatureHex = forge.util.bytesToHex(p7.rawCapture.signature)
|
||
}
|
||
|
||
return {
|
||
cert,
|
||
publicKey,
|
||
publicKeyHex,
|
||
signature,
|
||
signatureHex
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从 jsrsasign 解析的 PKCS7 中提取信息(支持 SM2)
|
||
*/
|
||
function extractInfoFromJsrsasignP7(base64Data) {
|
||
const parser = new rs.KJUR.asn1.cms.CMSParser()
|
||
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
|
||
|
||
// 提取证书
|
||
let cert = null
|
||
let publicKey = null
|
||
let publicKeyHex = null
|
||
|
||
// p7.certs 是一个对象,包含 array 属性
|
||
const certs = p7.certs && p7.certs.array ? p7.certs.array : []
|
||
|
||
if (certs.length > 0) {
|
||
const certPem = certs[0]
|
||
|
||
// 解析证书获取详细信息
|
||
const x509 = new rs.X509()
|
||
x509.readCertPEM(certPem)
|
||
|
||
// 获取主题和颁发者
|
||
const subjectStr = x509.getSubjectString()
|
||
const issuerStr = x509.getIssuerString()
|
||
|
||
// 将字符串转换为数组格式
|
||
function parseDNString(dnStr) {
|
||
if (!dnStr || dnStr === '/') return []
|
||
const parts = dnStr.substring(1).split('/')
|
||
return parts.map(part => {
|
||
const [name, value] = part.split('=')
|
||
return { name, value }
|
||
})
|
||
}
|
||
|
||
const subjectAttrs = parseDNString(subjectStr)
|
||
const issuerAttrs = parseDNString(issuerStr)
|
||
|
||
// 转换为 DN 字符串
|
||
function attrsToDN(attrs) {
|
||
if (!attrs || attrs.length === 0) return ''
|
||
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
|
||
}
|
||
|
||
cert = {
|
||
subject: subjectAttrs,
|
||
subjectDN: attrsToDN(subjectAttrs),
|
||
issuer: issuerAttrs,
|
||
issuerDN: attrsToDN(issuerAttrs),
|
||
serialNumber: x509.getSerialNumberHex(),
|
||
notBefore: x509.getNotBefore(),
|
||
notAfter: x509.getNotAfter(),
|
||
pem: certPem
|
||
}
|
||
|
||
// 提取公钥 - 使用 ASN.1 解析提取原始公钥字节
|
||
try {
|
||
// 将 PEM 转换为 hex
|
||
const certHex = rs.pemtohex(certPem)
|
||
|
||
// 获取 tbsCertificate
|
||
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
|
||
|
||
// 获取 subjectPublicKeyInfo (在 tbsCertificate 中的索引是 6)
|
||
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
|
||
|
||
// 提取公钥值 (BIT STRING)
|
||
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
|
||
|
||
// BIT STRING 的内容:跳过 tag 03 (2 hex), 跳过 length (2 hex), 跳过 unused bits (2 hex)
|
||
// 所以从第 6 个字符开始
|
||
const publicKeyHexContent = publicKeyBitString.substring(6)
|
||
|
||
// 提取算法 OID 来检测是否是 SM2
|
||
const algorithmSeq = rs.ASN1HEX.getTLVbyList(spki, 0, [0])
|
||
const oidTlv = rs.ASN1HEX.getTLVbyList(algorithmSeq, 0, [0])
|
||
const oidHex = oidTlv.substring(4)
|
||
|
||
// SM2 OID: 1.2.156.10197.1.301 -> hex 2A811CCF550101301
|
||
const isSM2 = oidHex.includes('2A811CCF55') || oidHex.includes('2a811ccf55')
|
||
|
||
// 创建完整的 subjectPublicKeyInfo PEM
|
||
publicKey = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(spki, 'PUBLIC KEY')
|
||
// publicKeyHex 就是 subjectPublicKeyInfo 的完整 hex
|
||
publicKeyHex = spki
|
||
} catch (e) {
|
||
// SM2 公钥可能无法通过标准方式获取
|
||
console.error('提取 SM2 公钥错误:', e.message)
|
||
publicKey = null
|
||
publicKeyHex = null
|
||
}
|
||
}
|
||
|
||
// 提取签名值
|
||
let signature = null
|
||
let signatureHex = null
|
||
if (p7.sinfos && p7.sinfos.length > 0) {
|
||
const signer = p7.sinfos[0]
|
||
if (signer.sighex) {
|
||
signatureHex = signer.sighex
|
||
signature = rs.hextob64(signer.sighex)
|
||
}
|
||
}
|
||
|
||
return {
|
||
cert,
|
||
publicKey,
|
||
publicKeyHex,
|
||
signature,
|
||
signatureHex
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 验签 PKCS7 签名(自动判断 RSA 或 SM2 算法)
|
||
* @param {Buffer|string} p7sign - PKCS7 签名数据
|
||
* @param {Buffer|string} [originalData] - 原始数据,当签名是 detech 时必须,当签名是 attach 时不传
|
||
* @returns {boolean} 验签结果
|
||
*/
|
||
module.exports.verifyPKCS7 = function (p7sign, originalData) {
|
||
// 处理输入数据
|
||
let base64Data
|
||
if (Buffer.isBuffer(p7sign)) {
|
||
base64Data = p7sign.toString('base64')
|
||
} else if (typeof p7sign === 'string') {
|
||
// 判断是否是 PEM 格式
|
||
if (p7sign.includes('-----BEGIN PKCS7-----')) {
|
||
// 提取 base64 部分
|
||
base64Data = p7sign
|
||
.replace(/-----BEGIN PKCS7-----\n?/, '')
|
||
.replace(/-----END PKCS7-----\n?/, '')
|
||
.replace(/\n/g, '')
|
||
} else {
|
||
// 如果不是 PEM 格式,假设是 base64
|
||
base64Data = p7sign
|
||
}
|
||
} else {
|
||
throw new Error('Invalid input type for p7sign')
|
||
}
|
||
|
||
// 准备验证所需的数据
|
||
let verifyData
|
||
if (originalData) {
|
||
// 当提供了原始数据时,使用原始数据进行验证(detch 模式)
|
||
if (Buffer.isBuffer(originalData)) {
|
||
verifyData = originalData
|
||
} else if (typeof originalData === 'string') {
|
||
verifyData = Buffer.from(originalData)
|
||
} else {
|
||
throw new Error('Invalid input type for originalData')
|
||
}
|
||
}
|
||
|
||
// 尝试使用 node-forge 解析和验证(支持 RSA)
|
||
try {
|
||
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
|
||
const p7 = forge.pkcs7.messageFromPem(pemData)
|
||
|
||
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
|
||
if (!verifyData) {
|
||
if (p7.content) {
|
||
verifyData = Buffer.from(p7.content.getBytes(), 'binary')
|
||
} else {
|
||
throw new Error('No content found in PKCS7 signature for attach mode')
|
||
}
|
||
}
|
||
|
||
// 对于 RSA 签名,使用公钥手动验证
|
||
if (p7.certificates && p7.certificates.length > 0) {
|
||
const publicKey = p7.certificates[0].publicKey
|
||
if (publicKey && p7.rawCapture && p7.rawCapture.signature) {
|
||
// 计算数据的摘要
|
||
const md = forge.md.sha256.create()
|
||
md.update(verifyData.toString(), 'utf8')
|
||
const digest = md.digest().getBytes()
|
||
|
||
// 使用公钥验证签名
|
||
return publicKey.verify(digest, p7.rawCapture.signature)
|
||
}
|
||
}
|
||
|
||
// 如果 RSA 验证失败或不适用,尝试使用 node-forge 的 verify(如果支持)
|
||
return p7.verify()
|
||
} catch (forgeError) {
|
||
// 如果 node-forge 解析失败,尝试使用 jsrsasign 解析和验证(支持 SM2)
|
||
try {
|
||
return verifyPKCS7WithJsrsasign(base64Data, verifyData)
|
||
} catch (jsrsasignError) {
|
||
// 静默处理错误,直接返回 false
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用 jsrsasign 验证 PKCS7 签名(支持 SM2)
|
||
*/
|
||
function verifyPKCS7WithJsrsasign(base64Data, verifyData) {
|
||
const parser = new rs.KJUR.asn1.cms.CMSParser()
|
||
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
|
||
|
||
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
|
||
if (!verifyData) {
|
||
if (p7.econtent && p7.econtent.hex) {
|
||
// econtent.hex 是原始数据的 hex 编码
|
||
verifyData = Buffer.from(p7.econtent.hex, 'hex')
|
||
} else {
|
||
throw new Error('No content found in PKCS7 signature for attach mode')
|
||
}
|
||
}
|
||
|
||
// 获取签名信息
|
||
if (!p7.sinfos || p7.sinfos.length === 0) {
|
||
throw new Error('No signer info found in PKCS7')
|
||
}
|
||
|
||
const signer = p7.sinfos[0]
|
||
|
||
// 获取证书和公钥
|
||
if (!p7.certs || !p7.certs.array || p7.certs.array.length === 0) {
|
||
throw new Error('No certificate found in PKCS7')
|
||
}
|
||
|
||
const certPem = p7.certs.array[0]
|
||
const x509 = new rs.X509()
|
||
x509.readCertPEM(certPem)
|
||
|
||
// 判断签名算法
|
||
const sigAlg = signer.sigalg
|
||
|
||
// SM2 签名算法 OID: 1.2.156.10197.1.301.1 或 1.2.156.10197.1.501
|
||
if (sigAlg === '1.2.156.10197.1.301.1' || sigAlg === '1.2.156.10197.1.501' || sigAlg.includes('sm2')) {
|
||
// SM2 验证
|
||
// 提取公钥 hex
|
||
const certHex = rs.pemtohex(certPem)
|
||
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
|
||
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
|
||
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
|
||
let publicKeyHex = publicKeyBitString.substring(6) // 跳过 tag, length, unused bits
|
||
|
||
// 确保公钥有 04 前缀(sm-crypto 0.3.13 需要)
|
||
if (!publicKeyHex.startsWith('04')) {
|
||
publicKeyHex = '04' + publicKeyHex
|
||
}
|
||
|
||
// 计算 SM3 哈希
|
||
const hash = sm3(verifyData.toString())
|
||
|
||
// 签名格式:用户提供的是 ASN.1 DER 格式,需要使用 der: true 选项
|
||
let signatureHex = signer.sighex
|
||
|
||
// 使用 SM2 验证签名 (注意:使用 doVerifySignature,不是 doVerify)
|
||
// 尝试不同的选项组合,包括 userId
|
||
const userId = '1234567812345678' // 默认 userId
|
||
|
||
try {
|
||
// 尝试 1: 使用原始数据 + DER + SM3 + userId
|
||
const result1 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true, userId })
|
||
if (result1) return true
|
||
|
||
// 尝试 2: 使用原始数据 + DER + SM3
|
||
const result2 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true })
|
||
if (result2) return true
|
||
|
||
// 尝试 3: 使用原始数据 + DER
|
||
const result3 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true })
|
||
if (result3) return true
|
||
|
||
// 尝试 4: 使用哈希 + DER + SM3 + userId
|
||
const result4 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true, userId })
|
||
if (result4) return true
|
||
|
||
// 尝试 5: 使用哈希 + DER + SM3
|
||
const result5 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true })
|
||
if (result5) return true
|
||
|
||
// 尝试 6: 使用哈希 + DER
|
||
const result6 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true })
|
||
if (result6) return true
|
||
|
||
// 尝试 7: 不使用 der 选项(直接使用签名)
|
||
const result7 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex)
|
||
if (result7) return true
|
||
|
||
// 尝试 8: 不使用 der 选项 + SM3 + userId
|
||
const result8 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { hash: true, userId })
|
||
if (result8) return true
|
||
|
||
// 尝试 9: 转换签名格式为原始 r|s 格式
|
||
if (signatureHex.startsWith('30')) {
|
||
try {
|
||
// 解析 ASN.1 DER 格式
|
||
// 格式: 30 len 02 len r 02 len s
|
||
const rStart = 8 // 30440220
|
||
const r = signatureHex.substring(rStart, rStart + 64) // 32字节 = 64 hex 字符
|
||
const sStart = rStart + 64 + 4 // 0220
|
||
const s = signatureHex.substring(sStart, sStart + 64) // 32字节 = 64 hex 字符
|
||
const rawSignature = r + s
|
||
|
||
// 尝试使用原始 r|s 格式
|
||
const result9 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex)
|
||
if (result9) return true
|
||
|
||
// 尝试使用原始 r|s 格式 + SM3 + userId
|
||
const result10 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex, { hash: true, userId })
|
||
if (result10) return true
|
||
} catch (e) {
|
||
// 忽略错误,继续尝试
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 忽略错误,继续尝试其他选项
|
||
}
|
||
|
||
return false
|
||
} else {
|
||
// RSA 验证(通过 jsrsasign)
|
||
try {
|
||
const pubKey = x509.getPublicKey()
|
||
const sig = new rs.KJUR.crypto.Signature({ alg: 'SHA256withRSA' })
|
||
sig.init(pubKey)
|
||
sig.updateString(verifyData.toString())
|
||
return sig.verify(signer.sighex)
|
||
} catch (e) {
|
||
throw new Error('RSA verification failed: ' + e.message)
|
||
}
|
||
}
|
||
}
|
||
|