Compare commits

..
20 Commits
Author SHA1 Message Date
cheney ce31ab2200 升级 Vite 到 6.x
Build and Deploy / build (push) Successful in 1m18s
2026-08-24 10:58:55 +08:00
cheney c6126b123a fix bugs
Build and Deploy / build (push) Successful in 1m1s
2026-08-07 13:49:02 +08:00
cheney 2adcf8f077 Changes to be committed:
Build and Deploy / build (push) Failing after 59s
modified:   src/App.tsx
	new file:   src/__tests__/utils/extract-cert.test.ts
	new file:   src/utils/extract-cert.js
	modified:   src/version.js
2026-08-07 11:33:57 +08:00
cheney 2a2e9e0d6b 新增功能
Build and Deploy / build (push) Successful in 5m41s
2026-07-01 15:27:28 +08:00
cheney 951819fb96 改个箭头 2026-06-30 15:08:49 +08:00
cheney ab9441982f Merge branch 'main' of http://git.honor3.com/Fullstack/seckit
Build and Deploy / build (push) Successful in 2m40s
2026-04-28 11:20:44 +08:00
cheney 4b3d5b762f hotfox 2026-04-28 11:18:22 +08:00
cheney 045e939dcd 把 kit 中的证书模块拿过来了。
Build and Deploy / build (push) Successful in 2m14s
2026-04-26 11:55:15 +08:00
cheney 32626c84a6 一些证书测试代码
Build and Deploy / build (push) Successful in 56s
2026-04-24 15:52:59 +08:00
cheney 58533aa5d0 保存 2026-04-24 09:53:03 +08:00
cheney 3459a389ac der c1c3c2 转换工具 2026-04-23 14:18:39 +08:00
cheney f4228370d9 整理
Build and Deploy / build (push) Successful in 1m4s
2026-04-18 15:53:54 +08:00
cheney 88acc24e86 添加证书工具
Build and Deploy / build (push) Successful in 1m3s
2026-04-18 15:14:29 +08:00
cheney b757597db9 base64 格式转换 2026-04-18 15:04:19 +08:00
cheney 20c27b2e5b sm2 改进 2026-04-18 14:58:39 +08:00
cheney c9d66a77b1 fix build bug
Build and Deploy / build (push) Successful in 7m29s
2026-04-18 13:34:02 +08:00
cheney cd5fe4cee5 UI 调整
Build and Deploy / build (push) Failing after 16m27s
2026-04-18 11:11:25 +08:00
cheney cccff194b6 sm2 签名验签通过 2026-04-18 11:09:41 +08:00
cheney 0cb442eaa0 sm2 加密解密通过 2026-04-18 10:54:59 +08:00
cheney 9f6df6bbd8 更新 UI 2026-04-17 13:34:35 +08:00
27 changed files with 5714 additions and 262 deletions
+3
View File
@@ -41,3 +41,6 @@ out
# Temporary files # Temporary files
*.tmp *.tmp
*.temp *.temp
certs
+4 -2
View File
@@ -1,4 +1,6 @@
# TODO # TODO
- [ ] CI 集成,将 dist 传输到指定服务器指定目录。 - [x] CI 集成,将 dist 传输到指定服务器指定目录。
- [x] 添加 SM2 算法
- [x] 证书工具支持输入逗号分隔证书 id 并下载 DER 格式 CRL 文件。
- [x] 根证书生成后支持下载 CRT 文件和根证书私钥。
+4
View File
@@ -0,0 +1,4 @@
设计规范
- 所有输入框,双击后弹出提示框,确认后可以清空内容。移动端为长按弹出提示框,确认后可以清空内容。
+1354 -141
View File
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -11,12 +11,21 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@peculiar/webcrypto": "^1.5.0",
"@types/node-forge": "^1.3.14",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
"gm-crypto": "*", "gm-crypto": "*",
"gmsm-sm2js": "^0.7.1",
"gmsm-sm3js": "^0.2.0", "gmsm-sm3js": "^0.2.0",
"gmsm-sm4js": "^0.7.0", "gmsm-sm4js": "^0.7.0",
"jsrsasign": "^11.1.3",
"node-forge": "^1.4.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"sjcl-with-all": "^1.0.8" "sjcl-with-all": "^1.0.8",
"sm-crypto": "^0.4.0",
"stream-browserify": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
@@ -24,13 +33,13 @@
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.7.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"jest": "^30.3.0", "jest": "^30.3.0",
"ts-jest": "^29.4.9", "ts-jest": "^29.4.9",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.0.8" "vite": "^6.4.3"
} }
} }
+2080 -51
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
import * as forge from 'node-forge'
import { generateCrlFile, generateRootCert, loadRootPrivateKeyFromCache, parseCrlCertificateIds } from '../../utils/algorithm/cert'
describe('证书 CRL 工具测试', () => {
test('1-1 空证书 id 应生成无吊销条目的 CRL 文件', () => {
const crlFile = generateCrlFile('')
const crlAsn1 = forge.asn1.fromDer(forge.util.createBuffer(crlFile))
const crlItems = crlAsn1.value as forge.asn1.Asn1[]
const tbsCertListItems = crlItems[0].value as forge.asn1.Asn1[]
expect(crlItems.length).toBe(3)
expect(tbsCertListItems.length).toBe(5)
})
test('1-2 多个逗号分隔证书 id 应生成多个吊销条目', () => {
const crlFile = generateCrlFile('01, 02, 0a')
const crlAsn1 = forge.asn1.fromDer(forge.util.createBuffer(crlFile))
const crlItems = crlAsn1.value as forge.asn1.Asn1[]
const tbsCertListItems = crlItems[0].value as forge.asn1.Asn1[]
const revokedCertificates = tbsCertListItems[5].value as forge.asn1.Asn1[]
expect(revokedCertificates.length).toBe(3)
})
test('1-3 重复证书 id 应去重', () => {
const certificateIds = parseCrlCertificateIds('01,1,02,02')
expect(certificateIds).toEqual(['01', '02'])
})
test('1-4 非十六进制证书 id 应抛出错误', () => {
expect(() => parseCrlCertificateIds('01, zz')).toThrow('证书 id')
})
test('1-5 生成根证书后应能读取根证书私钥', () => {
const rootCert = generateRootCert()
const rootPrivateKey = loadRootPrivateKeyFromCache()
expect(rootCert).toContain('BEGIN CERTIFICATE')
expect(rootPrivateKey).toContain('BEGIN RSA PRIVATE KEY')
})
})
+47 -3
View File
@@ -1,4 +1,4 @@
import { generateSm2KeyPair, sm2Encrypt, sm2Decrypt } from '../../utils/algorithm/sm2' import { sm2GenerateKeyPair, sm2Encrypt, sm2Decrypt, sm2C1C3C2ToDer, sm2DerToC1C3C2 } from '../../utils/algorithm/sm2'
// UTF8 转 Hex // UTF8 转 Hex
const utf8ToHex = (utf8: string): string => { const utf8ToHex = (utf8: string): string => {
@@ -8,7 +8,7 @@ const utf8ToHex = (utf8: string): string => {
describe('SM2 算法测试', () => { describe('SM2 算法测试', () => {
test('生成密钥对', () => { test('生成密钥对', () => {
const { privateKey, publicKey } = generateSm2KeyPair() const { privateKey, publicKey } = sm2GenerateKeyPair()
expect(privateKey).toBeTruthy() expect(privateKey).toBeTruthy()
expect(publicKey).toBeTruthy() expect(publicKey).toBeTruthy()
expect(privateKey.length).toBeGreaterThan(0) expect(privateKey.length).toBeGreaterThan(0)
@@ -16,7 +16,7 @@ describe('SM2 算法测试', () => {
}) })
test('加密和解密', () => { test('加密和解密', () => {
const { privateKey, publicKey } = generateSm2KeyPair() const { privateKey, publicKey } = sm2GenerateKeyPair()
const originalMessage = '测试消息' const originalMessage = '测试消息'
const messageHex = utf8ToHex(originalMessage) const messageHex = utf8ToHex(originalMessage)
@@ -37,4 +37,48 @@ describe('SM2 算法测试', () => {
// console.log('Decrypted Message:', decryptedMessage) // console.log('Decrypted Message:', decryptedMessage)
// expect(decryptedMessage).toBe(originalMessage) // expect(decryptedMessage).toBe(originalMessage)
}) })
test('C1C3C2 转 DER 格式', () => {
const { publicKey } = sm2GenerateKeyPair()
const originalMessage = '测试消息'
const messageHex = utf8ToHex(originalMessage)
const encrypted = sm2Encrypt(messageHex, publicKey)
console.log('C1C3C2 Format:', encrypted)
const derFormat = sm2C1C3C2ToDer(encrypted)
console.log('DER Format:', derFormat)
expect(derFormat).toBeTruthy()
expect(derFormat.length).toBeGreaterThan(0)
})
test('DER 转 C1C3C2 格式', () => {
const { publicKey } = sm2GenerateKeyPair()
const originalMessage = '测试消息'
const messageHex = utf8ToHex(originalMessage)
const encrypted = sm2Encrypt(messageHex, publicKey)
const derFormat = sm2C1C3C2ToDer(encrypted)
const c1c3c2Format = sm2DerToC1C3C2(derFormat)
console.log('Converted C1C3C2 Format:', c1c3c2Format)
expect(c1c3c2Format).toBeTruthy()
expect(c1c3c2Format.length).toBeGreaterThan(0)
})
test('C1C3C2 与 DER 格式互转一致性', () => {
const { publicKey } = sm2GenerateKeyPair()
const originalMessage = '测试消息'
const messageHex = utf8ToHex(originalMessage)
const originalC1C3C2 = sm2Encrypt(messageHex, publicKey)
const derFormat = sm2C1C3C2ToDer(originalC1C3C2)
const convertedC1C3C2 = sm2DerToC1C3C2(derFormat)
console.log('Original C1C3C2:', originalC1C3C2)
console.log('Converted C1C3C2:', convertedC1C3C2)
// 验证互转后的数据是否一致
expect(convertedC1C3C2).toBe(originalC1C3C2)
})
}) })
+288
View File
@@ -0,0 +1,288 @@
const test = require('tape')
const rs = require('jsrsasign')
const sm2 = require('../src/sm2')
const util = require('../src/util')
const publicKeyPemFromAliKmsForSign = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAERrsLH25zLm2LIo6tivZM9afLprSX
6TCKAmQJArAO7VOtZyW4PQwfaTsUIF7IXEFG4iI8bNuTQwMykUzLu2ypEA==
-----END PUBLIC KEY-----
`
const sm2PKIXPublicKeyHex = '3059301306072a8648ce3d020106082a811ccf5501822d03420004ef7db908af06082ef4a30e0ec28623371c106a53296a7b0e1a9b5717bd9cb81beb20d094aba685fd0f6a7ecc007ccf797ba634476326723b303d9dec873f440b'
const signatureHex = '30450220757984e0a063394ee0792b52172dd4273c05e2a66d734ff804a37b9ac639c098022100d9739a8d7a37fc88a1b4210998da489ad5b0dee1c8cb9097e532318aded5d204'
const csrFromAli = `-----BEGIN CERTIFICATE REQUEST-----
MIIBYjCCAQkCAQAwRzELMAkGA1UEBhMCQ04xEzARBgNVBAMMCkNhcmdvU21hcnQx
DzANBgNVBAcMBlpodWhhaTESMBAGA1UECAwJR3Vhbmdkb25nMFkwEwYHKoZIzj0C
AQYIKoEcz1UBgi0DQgAERrsLH25zLm2LIo6tivZM9afLprSX6TCKAmQJArAO7VOt
ZyW4PQwfaTsUIF7IXEFG4iI8bNuTQwMykUzLu2ypEKBgMC4GCSqGSIb3DQEJDjEh
MB8wHQYDVR0OBBYEFA3FO8vT+8qZBfGZa2TRhLRbme+9MC4GCSqGSIb3DQEJDjEh
MB8wHQYDVR0RBBYwFIESZW1tYW4uc3VuQGlxYXguY29tMAoGCCqBHM9VAYN1A0cA
MEQCIBQx6yv3rzfWCkKqDZQOfNKESQc6NtpQbeVvcxfBrciwAiAj78kkrF5R3g4l
bxIHjKZHc2sztHCXe7cseWGiLq0syg==
-----END CERTIFICATE REQUEST-----
`
// CA was from https://www.gmcert.org/
const CA_CERT = `-----BEGIN CERTIFICATE-----
MIICIzCCAcigAwIBAgIJAKun/ZLoSXfeMAoGCCqBHM9VAYN1MGcxCzAJBgNVBAYT
AkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdIYWlEaWFuMRMwEQYDVQQK
DApHTUNlcnQub3JnMR8wHQYDVQQDDBZHTUNlcnQgR00gUm9vdCBDQSAtIDAxMB4X
DTE5MTAyNDEyMzEzM1oXDTM5MDcxMTEyMzEzM1owZzELMAkGA1UEBhMCQ04xEDAO
BgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0hhaURpYW4xEzARBgNVBAoMCkdNQ2Vy
dC5vcmcxHzAdBgNVBAMMFkdNQ2VydCBHTSBSb290IENBIC0gMDEwWTATBgcqhkjO
PQIBBggqgRzPVQGCLQNCAASXWWtv+ifV7dJHqPNXwcmioh/48Wg3IuI+o11nLEOD
zljxL2yMxoQM6xfNJHuqadXXNZv3D2rml5Pk0W/tmfHEo10wWzAdBgNVHQ4EFgQU
f1peOwCEWSoPmL6hDm85lUMQTQcwHwYDVR0jBBgwFoAUf1peOwCEWSoPmL6hDm85
lUMQTQcwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwCgYIKoEcz1UBg3UDSQAw
RgIhAJ7AZAC0i+4OyfxDuvPIg0I7ZtqL2kII2f1syaIW4C6iAiEAlHuUu0TMrOAr
sU47scL1B9BhyEh5tbEjsKLHia3K0YU=
-----END CERTIFICATE-----
`
// cert was generated from https://www.gmcert.org/
const cert = `-----BEGIN CERTIFICATE-----
MIICDTCCAbOgAwIBAgIJAOWoGwJCnVw5MAoGCCqBHM9VAYN1MGcxCzAJBgNVBAYT
AkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdIYWlEaWFuMRMwEQYDVQQK
DApHTUNlcnQub3JnMR8wHQYDVQQDDBZHTUNlcnQgR00gUm9vdCBDQSAtIDAxMB4X
DTIxMDIyNDA3NTgxMloXDTIyMDIyNDA3NTgxMlowIjELMAkGA1UEBhMCQ04xEzAR
BgNVBAMMCkNhcmdvU21hcnQwWTATBgcqhkjOPQIBBggqgRzPVQGCLQNCAATi93H1
6+sN4/e6ksqPb/yAaR5/ewgO0PVAtAqMXV3IIZsug/VgFrduCzE71PKHHKKrY3MA
d1pP8ozvDIGpoYJ8o4GMMIGJMAwGA1UdEwEB/wQCMAAwCwYDVR0PBAQDAgeAMCwG
CWCGSAGG+EIBDQQfFh1HTUNlcnQub3JnIFNpZ25lZCBDZXJ0aWZpY2F0ZTAdBgNV
HQ4EFgQUPY0wMfEXn8wNhQTy7bL/dNJcA1UwHwYDVR0jBBgwFoAUf1peOwCEWSoP
mL6hDm85lUMQTQcwCgYIKoEcz1UBg3UDSAAwRQIgQsJ/kjgsc5cDavOvLvAOn2c9
u1EHM5QIWn58/xlMu1gCIQDk7Kp4A/c+W2lr93yFHiTPxwtKIz/nwtH4GRAcxeiM
iA==
-----END CERTIFICATE-----
`
const sm2PrivateKeyEncryptedPKCS8 = `
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIH2MGEGCSqGSIb3DQEFDTBUMDQGCSqGSIb3DQEFDDAnBBDa6ckWJNP3QBD7MIF8
4nVqAgEQAgEQMA0GCSqBHM9VAYMRAgUAMBwGCCqBHM9VAWgCBBDMUgr+5Y/XN2g9
mPGiISzGBIGQytwK98/ET4WrS0H7AsUri6FTqztrzAvgzFl3+s9AsaYtUlzE3EzE
x6RWxo8kpKO2yj0a/Jh9WZCD4XAcoZ9aMopiWlOdpXJr/iQlMGdirCYIoF37lHMc
jZHNffmk4ii7NxCfjrzpiFq4clYsNMXeSEnq1tuOEur4kYcjHYSIFc9bPG656a60
+SIJsJuPFi0f
-----END ENCRYPTED PRIVATE KEY-----`
const sm2PrivateKeyPlainPKCS8 = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQgbFoKCy7tPL7D5PEl
K/4OKMUEoca/GZnuuwr57w+ObIWhRANCAASDVuZCpA69GNKbo1MvvZ87vujwJ8P2
85pbovhwNp+ZiJgfXv5V0cXN9sDvKwcIR6FPf99CcqjfCcRC8wWK+Uuh
-----END PRIVATE KEY-----`
const pkcs8SM2P256PrivateKeyHex = '308187020100301306072a8648ce3d020106082a811ccf5501822d046d306b0201010420b26da57ba53004ddcd387ad46a361b51b308481f2327d47fb10c5fb3a8c86b92a144034200040d5365bfdbdc564c5b0eda0a85ddbd753821a709de90efe0666ba2544766acf1100ac0484d166842011da5cd6139e53dedb99ce37cea9edf4941628066e861bf'
const sec1SM2PrivateKeyHex = '30770201010420857dd87970aab4328dad891c781e3b270742aa9cf5d3d3764efe77f6c3d6e33aa00a06082a811ccf5501822da14403420004ced963a5705a0490ff13dde893cbda6de61f41fcaf917a5b4007d30cdec46426bc39b9c18d15b2a68a64dc333f262e600b675856285b42296f24741ee6f562a0'
test('SM2 P-256 encrypt/decrypt local', function (t) {
const ec = new rs.ECDSA({ curve: sm2.getCurveName() })
const plainText = 'send reinforcements, we\'re going to advance'
const expected = '73656e64207265696e666f7263656d656e74732c20776527726520676f696e6720746f20616476616e6365'
const keypair = ec.generateKeyPairHex()
const ciphertext = sm2.encrypt(keypair.ecpubhex, plainText)
const asn1text = sm2.plainCiphertext2ASN1(ciphertext)
console.log('ciphertext=' + ciphertext)
console.log('asn.1 ciphertext=' + asn1text)
const ciphertext1 = sm2.asn1Ciphertext2Plain(asn1text)
t.equal(ciphertext1, ciphertext)
const result = sm2.decryptHex(keypair.ecprvhex, ciphertext)
t.equal(result, expected)
const result2 = sm2.decryptHex(keypair.ecprvhex, asn1text)
t.equal(result2, expected)
t.end()
})
test('SM2 P-256 encrypt, output hex asn.1 ciphertext', function (t) {
const ec = new rs.ECDSA({ curve: sm2.getCurveName() })
const plainText = 'send reinforcements, we\'re going to advance'
const expected = '73656e64207265696e666f7263656d656e74732c20776527726520676f696e6720746f20616476616e6365'
const keypair = ec.generateKeyPairHex()
const ciphertext = sm2.encrypt(keypair.ecpubhex, plainText, sm2.asn1EncrypterOptions())
const tag = ciphertext.substring(0, 2)
t.equal(tag, '30')
const result = sm2.decryptHex(keypair.ecprvhex, ciphertext)
t.equal(result, expected)
t.end()
})
test('SM2 P-256 sign/verify local', function (t) {
const ec = new rs.ECDSA({ curve: sm2.getCurveName() })
const keypair = ec.generateKeyPairHex()
const sig1 = sm2.createSM2Signature()
sig1.init({ curve: sm2.getCurveName(), d: keypair.ecprvhex })
sig1.updateString('emmansun')
const hSig = sig1.sign()
console.log('hSig=' + hSig)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: keypair.ecpubhex })
sig2.updateString('emmansun')
t.true(sig2.verify(hSig))
t.end()
})
test('SM2 P-256 sm2 specific sign/verify', function (t) {
const ec = new rs.ECDSA({ curve: sm2.getCurveName() })
const keypair = ec.generateKeyPairHex()
const sig1 = sm2.createSM2Signature()
sig1.init({ curve: sm2.getCurveName(), d: keypair.ecprvhex })
const hSig = sig1.sm2Sign('emmansun')
const hSig1 = sig1.sm2Sign('emmansun 1')
console.log('hSig=' + hSig)
console.log('hSig1=' + hSig1)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: keypair.ecpubhex })
t.true(sig2.sm2Verify(hSig, 'emmansun'))
t.true(sig2.sm2Verify(hSig1, 'emmansun 1'))
t.end()
})
test('NIST P-256 sign/verify local', function (t) {
const ec = new rs.ECDSA({ curve: sm2.getCurveName() })
const keypair = ec.generateKeyPairHex()
const sig1 = new sm2.Signature({ alg: 'SHA256withECDSA' })
sig1.init({ curve: sm2.getCurveName(), d: keypair.ecprvhex })
sig1.updateString('emmansun')
const hSig = sig1.sign()
console.log('hSig=' + hSig)
const sig2 = new sm2.Signature({ alg: 'SHA256withECDSA' })
sig2.init({ curve: sm2.getCurveName(), xy: keypair.ecpubhex })
sig2.updateString('emmansun')
t.true(sig2.verify(hSig))
t.end()
})
test('SM2 parse public key pem, verify signature, both from ali KMS', function (t) {
const sig = sm2.createSM2Signature()
sig.init(publicKeyPemFromAliKmsForSign)
t.equal(sig.pubKey.curveName, sm2.getCurveName())
t.true(sig.verifyWithMessageHash('66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0', signatureHex))
t.end()
})
test('SM2 calculate ZA', function (t) {
const sig = sm2.createSM2Signature()
sig.init(publicKeyPemFromAliKmsForSign)
const za = util.toHex(sig.pubKey.calculateZA())
t.equal(za, '17e7fc071f1418200aeead3c5118a2f18381431d92b808a3bd1ba2d8270c2914')
t.end()
})
test('SM2 parse CSR from ALI KMS', function (t) {
const result = rs.asn1.csr.CSRUtil.getParam(csrFromAli)
t.equal(result.sigalg, sm2.getSignAlg())
t.end()
})
test('SM2 gen CSR', function (t) {
const kp = rs.KEYUTIL.generateKeypair('EC', sm2.getCurveName())
const prvKey = kp.prvKeyObj
const pubKey = kp.pubKeyObj
const csr = rs.asn1.csr.CSRUtil.newCSRPEM({
subject: { str: '/C=US/O=TEST' },
sbjpubkey: pubKey,
sigalg: sm2.getSignAlg(),
sbjprvkey: prvKey
})
console.log(csr)
const result = rs.asn1.csr.CSRUtil.getParam(csr)
console.log(JSON.stringify(result))
t.end()
})
test('SM2 read cert', function (t) {
const x = sm2.createX509()
x.readCertPEM(cert)
t.equal(x.getSignatureAlgorithmField(), sm2.getSignAlg())
t.true(x.verifySignature(rs.KEYUTIL.getKey(CA_CERT)))
t.end()
})
test('Parse PKCS8 encrypted SM2 private key', function (t) {
const key = rs.KEYUTIL.getKeyFromEncryptedPKCS8PEM(sm2PrivateKeyEncryptedPKCS8, 'Password1')
t.equal(key.curveName, 'sm2p256v1')
t.equal(key.prvKeyHex, '6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85')
t.equal(key.pubKeyHex, '048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1')
t.end()
})
test('Parse PKCS8 unencrypted SM2 private key', function (t) {
const key = rs.KEYUTIL.getKeyFromPlainPrivatePKCS8PEM(sm2PrivateKeyPlainPKCS8)
t.equal(key.curveName, 'sm2p256v1')
t.equal(key.prvKeyHex, '6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85')
t.equal(key.pubKeyHex, '048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1')
t.end()
})
test('Parse PKCS8 unencrypted SM2 private key with Signature', function (t) {
const sig1 = sm2.createSM2Signature()
sig1.init(sm2PrivateKeyPlainPKCS8)
const hSig = sig1.sm2Sign('emmansun')
console.log('hSig=' + hSig)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: sig1.prvKey.pubKeyHex })
t.true(sig2.sm2Verify(hSig, 'emmansun'))
t.end()
})
test('Parse PKCS8 encrypted SM2 private key with Signature', function (t) {
const sig1 = sm2.createSM2Signature()
sig1.init(sm2PrivateKeyEncryptedPKCS8, 'Password1')
const hSig = sig1.sm2Sign('emmansun')
console.log('hSig=' + hSig)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: sig1.prvKey.pubKeyHex })
t.true(sig2.sm2Verify(hSig, 'emmansun'))
t.end()
})
test('Parse PKCS8 unencrypted SM2 private key (hex) with Signature', function (t) {
const sig1 = sm2.createSM2Signature()
sig1.init(pkcs8SM2P256PrivateKeyHex, undefined, 'pkcs8prv')
const hSig = sig1.sm2Sign('emmansun')
console.log('hSig=' + hSig)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: sig1.prvKey.pubKeyHex })
t.true(sig2.sm2Verify(hSig, 'emmansun'))
t.end()
})
test('Parse SEC1 SM2 private key (hex) with Signature', function (t) {
const sig1 = sm2.createSM2Signature()
sig1.init(sec1SM2PrivateKeyHex, undefined, 'pkcs5prv')
const hSig = sig1.sm2Sign('emmansun')
console.log('hSig=' + hSig)
const sig2 = sm2.createSM2Signature()
sig2.init({ curve: sm2.getCurveName(), xy: sig1.prvKey.pubKeyHex })
t.true(sig2.sm2Verify(hSig, 'emmansun'))
t.end()
})
test('Parse SM2 PKIX public key (hex) with Signature', function (t) {
const sig1 = sm2.createSM2Signature()
sig1.init(sm2PKIXPublicKeyHex, undefined, 'pkcs8pub')
t.equals(sig1.pubKey.pubKeyHex, '04ef7db908af06082ef4a30e0ec28623371c106a53296a7b0e1a9b5717bd9cb81beb20d094aba685fd0f6a7ecc007ccf797ba634476326723b303d9dec873f440b')
t.end()
})
Binary file not shown.
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
import { extractCertificate } from '../../utils/extract-cert'
describe('extractCertificate', () => {
it('extracts a DER-like certificate sequence from raw bytes', () => {
const certBody = new Uint8Array(120)
certBody[0] = 0x02
certBody[1] = 0x01
certBody[2] = 0x01
const der = new Uint8Array(130)
der[0] = 0x30
der[1] = 0x78
der[2] = 0x02
der[3] = 0x01
der[4] = 0x01
der.set(certBody, 5)
const result = extractCertificate(der)
expect(result.via).toBe('direct certificate')
expect(result.pem).toContain('BEGIN CERTIFICATE')
expect(result.der.length).toBeGreaterThan(0)
})
})
+7
View File
@@ -0,0 +1,7 @@
declare module 'gmsm-sm2js' {
export function generateKeyPairHex(privateKey?: string): { privateKey: string; publicKey: string };
export function encrypt(publicKey: string, message: string): string;
export function decrypt(privateKey: string, ciphertext: string): string;
export function sign(privateKey: string, message: string): string;
export function verify(publicKey: string, message: string, signature: string): boolean;
}
+24
View File
@@ -0,0 +1,24 @@
declare module 'sm-crypto' {
export interface SM2KeyPair {
privateKey: string;
publicKey: string;
}
export const sm2: {
generateKeyPairHex: (privateKey?: string) => SM2KeyPair;
doEncrypt: (message: string, publicKey: string, cipherMode?: number) => string;
doDecrypt: (ciphertext: string, privateKey: string, cipherMode?: number, options?: { output?: string }) => string | Uint8Array;
doSignature: (message: string | Uint8Array, privateKey: string, options?: { hash?: boolean, der?: boolean, userId?: string, publicKey?: string, pointPool?: any[] }) => string;
doVerifySignature: (message: string | Uint8Array, signature: string, publicKey: string, options?: { hash?: boolean, der?: boolean, userId?: string }) => boolean;
getPublicKeyFromPrivateKey: (privateKey: string) => string;
};
export const sm3: {
digest: (message: string) => string;
};
export const sm4: {
encrypt: (message: string, key: string, options?: any) => string;
decrypt: (ciphertext: string, key: string, options?: any) => string;
};
}
+487
View File
@@ -0,0 +1,487 @@
import * as forge from 'node-forge'
const ROOT_CERT_CACHE_KEY = 'rootCert'
const ROOT_PRIVATE_KEY_CACHE_KEY = 'rootPrivateKey'
const SHA256_WITH_RSA_OID = forge.pki.oids.sha256WithRSAEncryption
type RootCertificateBundle = {
certPem: string
privateKeyPem: string
}
let temporaryCrlSigner: RootCertificateBundle | null = null
// 将ArrayBuffer转换为Base64字符串(浏览器兼容)
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
// 生成随机的序列号
const generateSerialNumber = (): string => {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString()
}
// 获取本地缓存目录路径
const getCacheDirectory = (): string => {
if (typeof window !== 'undefined') {
// 浏览器环境
return 'localStorage'
} else {
// Node.js环境
return './cache'
}
}
// 保存根证书和私钥到本地缓存
// 参数:certPem 为 PEM 格式根证书,privateKeyPem 为 PEM 格式根证书私钥。
// 返回值:无。
// 注意事项:浏览器环境保存到 localStorage,Node.js 环境保存到 cache 目录。
const saveRootCertToCache = (certPem: string, privateKeyPem: string): void => {
if (typeof window !== 'undefined') {
// 浏览器环境:使用localStorage
localStorage.setItem(ROOT_CERT_CACHE_KEY, certPem)
localStorage.setItem(ROOT_PRIVATE_KEY_CACHE_KEY, privateKeyPem)
} else {
// Node.js环境:使用文件系统
const fs = require('fs')
const path = require('path')
const cacheDir = getCacheDirectory()
// 确保缓存目录存在
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true })
}
// 保存证书到文件
const certPath = path.join(cacheDir, 'rootCert.pem')
const privateKeyPath = path.join(cacheDir, 'rootPrivateKey.pem')
fs.writeFileSync(certPath, certPem)
fs.writeFileSync(privateKeyPath, privateKeyPem)
}
}
// 从本地缓存读取根证书
// 参数:无。
// 返回值:存在时返回 PEM 格式根证书,否则返回 null。
const loadRootCertFromCache = (): string | null => {
if (typeof window !== 'undefined') {
// 浏览器环境:从localStorage读取
return localStorage.getItem(ROOT_CERT_CACHE_KEY)
} else {
// Node.js环境:从文件系统读取
const fs = require('fs')
const path = require('path')
const cacheDir = getCacheDirectory()
const certPath = path.join(cacheDir, 'rootCert.pem')
if (fs.existsSync(certPath)) {
return fs.readFileSync(certPath, 'utf8')
}
return null
}
}
// 从本地缓存读取根证书私钥
// 参数:无。
// 返回值:存在时返回 PEM 格式根证书私钥,否则返回 null。
export const loadRootPrivateKeyFromCache = (): string | null => {
if (typeof window !== 'undefined') {
// 浏览器环境:从localStorage读取
return localStorage.getItem(ROOT_PRIVATE_KEY_CACHE_KEY)
} else {
// Node.js环境:从文件系统读取
const fs = require('fs')
const path = require('path')
const cacheDir = getCacheDirectory()
const privateKeyPath = path.join(cacheDir, 'rootPrivateKey.pem')
if (fs.existsSync(privateKeyPath)) {
return fs.readFileSync(privateKeyPath, 'utf8')
}
return null
}
}
// 创建根证书和根证书私钥
// 参数:无。
// 返回值:返回 PEM 格式根证书和 PEM 格式私钥。
const createRootCertificateBundle = (): RootCertificateBundle => {
// 生成密钥对
const keys = forge.pki.rsa.generateKeyPair(2048)
// 创建证书
const cert = forge.pki.createCertificate()
// 设置版本
cert.version = 3
// 设置序列号
cert.serialNumber = generateSerialNumber()
// 设置主题和颁发者(根证书自己颁发自己)
const attrs = [
{ name: 'commonName', value: 'Root CA' },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Root Certificate Authority' }
]
cert.setSubject(attrs)
cert.setIssuer(attrs)
// 设置公钥
cert.publicKey = keys.publicKey
// 设置有效期
const notBefore = new Date()
const notAfter = new Date()
notAfter.setFullYear(notBefore.getFullYear() + 10) // 10年有效期
cert.validity.notBefore = notBefore
cert.validity.notAfter = notAfter
// 添加扩展
cert.setExtensions([
{
name: 'basicConstraints',
cA: true
},
{
name: 'keyUsage',
digitalSignature: true,
keyCertSign: true,
cRLSign: true
}
])
// 使用私钥自签名
cert.sign(keys.privateKey, forge.md.sha256.create())
return {
certPem: forge.pki.certificateToPem(cert),
privateKeyPem: forge.pki.privateKeyToPem(keys.privateKey)
}
}
// 生成根证书
// 参数:无。
// 返回值:返回 PEM 格式根证书,并将根证书和私钥写入缓存。
export const generateRootCert = (): string => {
try {
const rootCertificateBundle = createRootCertificateBundle()
// 保存到本地缓存
saveRootCertToCache(rootCertificateBundle.certPem, rootCertificateBundle.privateKeyPem)
return rootCertificateBundle.certPem
} catch (error) {
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error))
}
}
// 基于根证书生成应用证书
export const generateAppCert = (commonName: string): string => {
try {
// 从缓存加载根证书
const rootCertPem = loadRootCertFromCache()
if (!rootCertPem) {
throw new Error('Root certificate not found in cache. Please generate root certificate first.')
}
const rootPrivateKeyPem = loadRootPrivateKeyFromCache()
if (!rootPrivateKeyPem) {
throw new Error('Root private key not found in cache. Please regenerate root certificate first.')
}
// 解析根证书
const rootCert = forge.pki.certificateFromPem(rootCertPem)
// 生成应用证书的密钥对
const appKeys = forge.pki.rsa.generateKeyPair(2048)
// 创建应用证书
const appCert = forge.pki.createCertificate()
// 设置版本
appCert.version = 3
// 设置序列号
appCert.serialNumber = generateSerialNumber()
// 设置主题
const appAttrs = [
{ name: 'commonName', value: commonName },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Application' }
]
appCert.setSubject(appAttrs)
// 设置颁发者(根证书)
appCert.setIssuer(rootCert.subject.attributes)
// 设置公钥
appCert.publicKey = appKeys.publicKey
// 设置有效期
const notBefore = new Date()
const notAfter = new Date()
notAfter.setFullYear(notBefore.getFullYear() + 5) // 5年有效期
appCert.validity.notBefore = notBefore
appCert.validity.notAfter = notAfter
// 添加扩展
appCert.setExtensions([
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true
},
{
name: 'extendedKeyUsage',
serverAuth: true,
clientAuth: true
}
])
// 使用根证书的私钥签名
const rootPrivateKey = forge.pki.privateKeyFromPem(rootPrivateKeyPem)
appCert.sign(rootPrivateKey, forge.md.sha256.create())
// 转换为PEM格式
const appCertPem = forge.pki.certificateToPem(appCert)
return appCertPem
} catch (error) {
throw new Error('Failed to generate application certificate: ' + (error instanceof Error ? error.message : error))
}
}
// 将证书 id 规范化为 ASN.1 INTEGER 使用的十六进制序列号
// 参数:certificateId 为用户输入的单个证书 id。
// 返回值:返回偶数长度、正数编码的十六进制序列号。
// 注意事项:证书 id 按证书序列号处理,仅支持十六进制字符。
const normalizeCrlCertificateId = (certificateId: string): string => {
const cleanedCertificateId = certificateId.trim().replace(/^0x/i, '')
if (!/^[0-9a-fA-F]+$/.test(cleanedCertificateId)) {
throw new Error('证书 id 只能包含 0-9、a-f、A-F,多个 id 请使用逗号分隔')
}
let serialNumberHex = cleanedCertificateId.replace(/^0+/, '') || '0'
if (serialNumberHex.length % 2 === 1) {
serialNumberHex = '0' + serialNumberHex
}
if (parseInt(serialNumberHex.slice(0, 2), 16) >= 0x80) {
serialNumberHex = '00' + serialNumberHex
}
return serialNumberHex.toLowerCase()
}
// 解析逗号分隔的证书 id
// 参数:certificateIds 为用户输入的逗号分隔证书 id 字符串。
// 返回值:返回去重后的十六进制证书序列号数组。
export const parseCrlCertificateIds = (certificateIds: string): string[] => {
const serialNumbers = certificateIds
.split(',')
.map(certificateId => certificateId.trim())
.filter(certificateId => certificateId.length > 0)
.map(normalizeCrlCertificateId)
return Array.from(new Set(serialNumbers))
}
// 将日期转换为 CRL 使用的 ASN.1 Time 对象
// 参数:date 为需要写入 CRL 的日期。
// 返回值:返回 UTCTime 或 GeneralizedTime ASN.1 对象。
const dateToCrlTimeAsn1 = (date: Date): forge.asn1.Asn1 => {
const utcTimeStart = new Date('1950-01-01T00:00:00Z')
const utcTimeEnd = new Date('2050-01-01T00:00:00Z')
if (date >= utcTimeStart && date < utcTimeEnd) {
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.UTCTIME, false, forge.asn1.dateToUtcTime(date))
}
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.GENERALIZEDTIME, false, forge.asn1.dateToGeneralizedTime(date))
}
// 创建 SHA256withRSA 算法标识
// 参数:无。
// 返回值:返回 CRL 签名算法使用的 ASN.1 AlgorithmIdentifier。
const createSha256WithRsaAlgorithmIdentifier = (): forge.asn1.Asn1 => {
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer(SHA256_WITH_RSA_OID).getBytes()),
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.NULL, false, '')
])
}
// 创建 CRL 的 TBSCertList 结构
// 参数:issuerCert 为签发者证书,serialNumbers 为吊销证书序列号数组,thisUpdate 为本次更新时间,nextUpdate 为下次更新时间。
// 返回值:返回用于签名的 TBSCertList ASN.1 对象。
const createTbsCertList = (issuerCert: forge.pki.Certificate, serialNumbers: string[], thisUpdate: Date, nextUpdate: Date): forge.asn1.Asn1 => {
const tbsCertListItems: forge.asn1.Asn1[] = [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, forge.asn1.integerToDer(1).getBytes()),
createSha256WithRsaAlgorithmIdentifier(),
forge.pki.distinguishedNameToAsn1(issuerCert.subject),
dateToCrlTimeAsn1(thisUpdate),
dateToCrlTimeAsn1(nextUpdate)
]
if (serialNumbers.length > 0) {
tbsCertListItems.push(forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, serialNumbers.map(serialNumber => {
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, forge.util.hexToBytes(serialNumber)),
dateToCrlTimeAsn1(thisUpdate)
])
})))
}
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, tbsCertListItems)
}
// 将二进制字符串转换为 ArrayBuffer
// 参数:binary 为 node-forge 输出的二进制字符串。
// 返回值:返回可用于 Blob 下载的字节数组。
const binaryStringToArrayBuffer = (binary: string): ArrayBuffer => {
const buffer = new ArrayBuffer(binary.length)
const bytes = new Uint8Array(buffer)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return buffer
}
// 获取 CRL 签发者证书和私钥
// 参数:无。
// 返回值:优先返回缓存根证书和私钥;不存在时返回临时签发者证书和私钥。
const getCrlSigner = (): RootCertificateBundle => {
const rootCertPem = loadRootCertFromCache()
const rootPrivateKeyPem = loadRootPrivateKeyFromCache()
if (rootCertPem && rootPrivateKeyPem) {
return {
certPem: rootCertPem,
privateKeyPem: rootPrivateKeyPem
}
}
if (!temporaryCrlSigner) {
temporaryCrlSigner = createRootCertificateBundle()
}
return temporaryCrlSigner
}
// 生成 DER 格式 CRL 文件内容
// 参数:certificateIds 为可选的逗号分隔证书 id,按证书序列号写入吊销列表。
// 返回值:返回 DER 格式 CRL 文件的字节数组。
export const generateCrlFile = (certificateIds: string): ArrayBuffer => {
try {
const serialNumbers = parseCrlCertificateIds(certificateIds)
const crlSigner = getCrlSigner()
const issuerCert = forge.pki.certificateFromPem(crlSigner.certPem)
const issuerPrivateKey = forge.pki.privateKeyFromPem(crlSigner.privateKeyPem)
const thisUpdate = new Date()
const nextUpdate = new Date()
nextUpdate.setDate(thisUpdate.getDate() + 30)
const tbsCertList = createTbsCertList(issuerCert, serialNumbers, thisUpdate, nextUpdate)
const tbsCertListDer = forge.asn1.toDer(tbsCertList).getBytes()
const md = forge.md.sha256.create()
md.update(tbsCertListDer)
const signature = issuerPrivateKey.sign(md)
const crlAsn1 = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
tbsCertList,
createSha256WithRsaAlgorithmIdentifier(),
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.BITSTRING, false, String.fromCharCode(0) + signature)
])
return binaryStringToArrayBuffer(forge.asn1.toDer(crlAsn1).getBytes())
} catch (error) {
throw new Error('Failed to generate CRL file: ' + (error instanceof Error ? error.message : error))
}
}
// 解析SM2证书信息
export const parseSm2Cert = (certData: string): any => {
try {
// 移除证书中的换行符和空格
const cleanCert = certData.replace(/\s/g, '')
// 解析DER编码的证书(浏览器兼容)
const binary = atob(cleanCert)
const derBuffer = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
derBuffer[i] = binary.charCodeAt(i)
}
const asn1 = forge.asn1.fromDer(String.fromCharCode(...derBuffer))
const cert = forge.pki.certificateFromAsn1(asn1)
// 提取证书信息
const subject: any = {}
cert.subject.attributes.forEach((attr: any) => {
if (attr.name) {
subject[attr.name] = attr.value
}
})
const issuer: any = {}
cert.issuer.attributes.forEach((attr: any) => {
if (attr.name) {
issuer[attr.name] = attr.value
}
})
// 提取公钥信息
let publicKeyHex = ''
// 尝试获取公钥的十六进制表示
try {
if (cert.publicKey) {
// 尝试将公钥转换为PEM格式,然后提取十六进制
try {
const pem = forge.pki.publicKeyToPem(cert.publicKey)
// 从PEM中提取十六进制
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '')
} catch (e) {
// 如果无法转换为PEM,使用空字符串
publicKeyHex = ''
}
}
} catch (e) {
// 如果转换失败,使用空字符串
publicKeyHex = ''
}
// 提取证书用途
let keyUsage: string[] = []
if (cert.extensions) {
const keyUsageExt = cert.extensions.find((ext: any) => ext.id === 'keyUsage')
if (keyUsageExt && keyUsageExt.value) {
keyUsage = keyUsageExt.value
}
}
return {
version: cert.version,
serialNumber: cert.serialNumber.toString(),
subject: subject,
issuer: issuer,
notBefore: cert.validity.notBefore,
notAfter: cert.validity.notAfter,
publicKey: publicKeyHex,
keyUsage: keyUsage
}
} catch (error) {
throw new Error('Failed to parse certificate: ' + (error instanceof Error ? error.message : error))
}
}
// 从文件内容解析证书
export const parseCertFromFile = (fileContent: ArrayBuffer): string => {
return arrayBufferToBase64(fileContent)
}
+286 -20
View File
@@ -1,17 +1,36 @@
import { SM2 } from 'gm-crypto' import { isValidHex, hexToArrayBuffer } from '../convert/hex'
// 验证是否为有效的 hex 字符串 // 导入 sm-crypto 库,它在浏览器环境中可用
const isValidHex = (hex: string): boolean => { import { sm2 } from 'sm-crypto'
return /^[0-9a-fA-F]*$/.test(hex) && hex.length % 2 === 0
// 类型断言,确保 sm2 对象具有我们需要的方法
const sm2Extended = sm2 as any
// SM2 密钥对生成
export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => {
const keypair = sm2.generateKeyPairHex()
return {
privateKey: keypair.privateKey,
publicKey: keypair.publicKey
}
} }
// 生成 SM2 密钥对 // SM2 从私钥计算公钥
export const generateSm2KeyPair = () => { export const sm2GetPublicKeyFromPrivateKey = (privateKey: string): string => {
return SM2.generateKeyPair() // 验证私钥是否为有效的 hex 字符串
if (!privateKey) {
throw new Error('Private key is required')
}
if (!isValidHex(privateKey)) {
throw new Error('Invalid private key: must be hex string')
}
const publicKey = sm2.getPublicKeyFromPrivateKey(privateKey)
return publicKey
} }
// SM2 公钥加密 // SM2 加密
export const sm2Encrypt = (message: string, publicKey: string): string => { export const sm2Encrypt = (message: string, publicKey: string, cipherMode: number = 1): string => {
// 验证消息是否为有效的 hex 字符串 // 验证消息是否为有效的 hex 字符串
if (!isValidHex(message)) { if (!isValidHex(message)) {
throw new Error('Invalid message: must be hex string') throw new Error('Invalid message: must be hex string')
@@ -21,23 +40,270 @@ export const sm2Encrypt = (message: string, publicKey: string): string => {
throw new Error('Invalid public key: must be hex string') throw new Error('Invalid public key: must be hex string')
} }
// 使用 hex 输入模式加密 // 将 hex 字符串转换为字节数组
const encrypted = SM2.encrypt(message, publicKey, { inputEncoding: 'hex' }) const messageBytes = new Uint8Array(hexToArrayBuffer(message))
return typeof encrypted === 'string' ? encrypted : Buffer.from(encrypted).toString('hex') // 将字节数组转换为字符串
const messageString = new TextDecoder().decode(messageBytes)
// 加密
const encrypted = sm2Extended.doEncrypt(messageString, publicKey, cipherMode)
return encrypted
} }
// SM2 私钥解密 // SM2 解密
export const sm2Decrypt = (encrypted: string, privateKey: string): string => { export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: number = 1): Uint8Array => {
// 验证加密数据是否为有效的 hex 字符串 // 验证密文是否为有效的 hex 字符串
if (!isValidHex(encrypted)) { if (!isValidHex(ciphertext)) {
throw new Error('Invalid encrypted data: must be hex string') throw new Error('Invalid ciphertext: must be hex string')
} }
// 验证私钥是否为有效的 hex 字符串 // 验证私钥是否为有效的 hex 字符串
if (!isValidHex(privateKey)) { if (!isValidHex(privateKey)) {
throw new Error('Invalid private key: must be hex string') throw new Error('Invalid private key: must be hex string')
} }
// 使用 hex 输入模式解密 // 解密,指定 output 为 'bytes' 以获取 Uint8Array 格式
const decrypted = SM2.decrypt(encrypted, privateKey, { inputEncoding: 'hex' }) const decrypted = sm2Extended.doDecrypt(ciphertext, privateKey, cipherMode, { output: 'bytes' })
return typeof decrypted === 'string' ? decrypted : new TextDecoder('utf-8').decode(decrypted)
// 如果返回的是字符串,则转为 Uint8Array
if (typeof decrypted === 'string') {
return new TextEncoder().encode(decrypted)
}
return decrypted
}
// SM2 签名
export const sm2Sign = (message: string, privateKey: string): string => {
// 验证消息是否为有效的 hex 字符串
if (!isValidHex(message)) {
throw new Error('Invalid message: must be hex string')
}
// 验证私钥是否为有效的 hex 字符串
if (!isValidHex(privateKey)) {
throw new Error('Invalid private key: must be hex string')
}
// 将 hex 字符串转换为字节数组
const messageBytes = new Uint8Array(hexToArrayBuffer(message))
// 签名
const signature = sm2.doSignature(messageBytes, privateKey, { hash: false })
return signature
}
// SM2 验证签名
export const sm2Verify = (message: string, signature: string, publicKey: string): boolean => {
// 验证消息是否为有效的 hex 字符串
if (!isValidHex(message)) {
throw new Error('Invalid message: must be hex string')
}
// 验证签名是否为有效的 hex 字符串
if (!isValidHex(signature)) {
throw new Error('Invalid signature: must be hex string')
}
// 验证公钥是否为有效的 hex 字符串
if (!isValidHex(publicKey)) {
throw new Error('Invalid public key: must be hex string')
}
// 将 hex 字符串转换为字节数组
const messageBytes = new Uint8Array(hexToArrayBuffer(message))
// 验证签名
const result = sm2.doVerifySignature(messageBytes, signature, publicKey, { hash: false })
return result
}
// 辅助函数:将十六进制字符串转换为字节数组
const hexToBytes = (hex: string): Uint8Array => {
const bytes = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
}
return bytes
}
// 辅助函数:将字节数组转换为十六进制字符串
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0')).join('')
}
// 辅助函数:ASN.1 DER编码长度
const encodeLength = (length: number): Uint8Array => {
if (length < 128) {
return new Uint8Array([length])
} else if (length < 256) {
return new Uint8Array([0x81, length])
} else if (length < 65536) {
return new Uint8Array([0x82, length >> 8, length & 0xff])
} else {
throw new Error('Length too large')
}
}
// SM2 密文 C1C3C2 格式转 DER 格式
export const sm2C1C3C2ToDer = (c1c3c2: string): string => {
// 验证输入是否为有效的 hex 字符串
if (!isValidHex(c1c3c2)) {
throw new Error('Invalid C1C3C2: must be hex string')
}
// C1C3C2 格式结构:C1(65字节) + C3(32字节) + C2(可变长度)
// C1: 04 + x(32字节) + y(32字节)
const c1HexLength = 130; // 65字节 * 2 (hex)
const c3HexLength = 64; // 32字节 * 2 (hex)
if (c1c3c2.length < c1HexLength + c3HexLength) {
throw new Error('Invalid C1C3C2: length too short')
}
const c1 = c1c3c2.substring(0, c1HexLength)
const c3 = c1c3c2.substring(c1HexLength, c1HexLength + c3HexLength)
const c2 = c1c3c2.substring(c1HexLength + c3HexLength)
// 转换为字节数组
const c1Bytes = hexToBytes(c1)
const c2Bytes = hexToBytes(c2)
const c3Bytes = hexToBytes(c3)
// ASN.1 DER 编码
// 1. 开始 SEQUENCE
const sequenceTag = new Uint8Array([0x30])
// 2. 编码 C1 (BIT STRING)
const c1Tag = new Uint8Array([0x03])
const c1BitString = new Uint8Array([0x00, ...c1Bytes]) // BIT STRING 前加 0x00
const c1DerLength = encodeLength(c1BitString.length)
// 3. 编码 C2 (OCTET STRING)
const c2Tag = new Uint8Array([0x04])
const c2DerLength = encodeLength(c2Bytes.length)
// 4. 编码 C3 (OCTET STRING)
const c3Tag = new Uint8Array([0x04])
const c3DerLength = encodeLength(c3Bytes.length)
// 计算总长度
const totalLength = c1Tag.length + c1DerLength.length + c1BitString.length +
c2Tag.length + c2DerLength.length + c2Bytes.length +
c3Tag.length + c3DerLength.length + c3Bytes.length
const sequenceLength = encodeLength(totalLength)
// 组合所有部分
const derBytes = new Uint8Array(
sequenceTag.length + sequenceLength.length + totalLength
)
let offset = 0
// 写入 SEQUENCE
derBytes.set(sequenceTag, offset)
offset += sequenceTag.length
derBytes.set(sequenceLength, offset)
offset += sequenceLength.length
// 写入 C1
derBytes.set(c1Tag, offset)
offset += c1Tag.length
derBytes.set(c1DerLength, offset)
offset += c1DerLength.length
derBytes.set(c1BitString, offset)
offset += c1BitString.length
// 写入 C2
derBytes.set(c2Tag, offset)
offset += c2Tag.length
derBytes.set(c2DerLength, offset)
offset += c2DerLength.length
derBytes.set(c2Bytes, offset)
offset += c2Bytes.length
// 写入 C3
derBytes.set(c3Tag, offset)
offset += c3Tag.length
derBytes.set(c3DerLength, offset)
offset += c3DerLength.length
derBytes.set(c3Bytes, offset)
return bytesToHex(derBytes)
}
// 辅助函数:ASN.1 DER解码长度
const decodeLength = (bytes: Uint8Array, offset: number): { length: number, newOffset: number } => {
const firstByte = bytes[offset]
if (firstByte < 128) {
return { length: firstByte, newOffset: offset + 1 }
} else if (firstByte === 0x81) {
return { length: bytes[offset + 1], newOffset: offset + 2 }
} else if (firstByte === 0x82) {
return { length: (bytes[offset + 1] << 8) | bytes[offset + 2], newOffset: offset + 3 }
} else {
throw new Error('Invalid length encoding')
}
}
// SM2 密文 DER 格式转 C1C3C2 格式
export const sm2DerToC1C3C2 = (der: string): string => {
// 验证输入是否为有效的 hex 字符串
if (!isValidHex(der)) {
throw new Error('Invalid DER: must be hex string')
}
const derBytes = hexToBytes(der)
let offset = 0
// 检查是否为 SEQUENCE
if (derBytes[offset] !== 0x30) {
throw new Error('Invalid DER: not a SEQUENCE')
}
offset += 1
// 解码 SEQUENCE 长度
const { newOffset: seqOffset } = decodeLength(derBytes, offset)
offset = seqOffset
// 解码 C1 (BIT STRING)
if (derBytes[offset] !== 0x03) {
throw new Error('Invalid DER: C1 is not a BIT STRING')
}
offset += 1
const { length: c1Length, newOffset: c1Offset } = decodeLength(derBytes, offset)
offset = c1Offset
// 跳过 BIT STRING 的 0x00 前缀
if (derBytes[offset] !== 0x00) {
throw new Error('Invalid DER: BIT STRING missing leading 0x00')
}
offset += 1
const c1Bytes = derBytes.slice(offset, offset + c1Length - 1) // -1 因为跳过了 0x00
offset += c1Length - 1
// 解码 C2 (OCTET STRING)
if (derBytes[offset] !== 0x04) {
throw new Error('Invalid DER: C2 is not an OCTET STRING')
}
offset += 1
const { length: c2Length, newOffset: c2Offset } = decodeLength(derBytes, offset)
offset = c2Offset
const c2Bytes = derBytes.slice(offset, offset + c2Length)
offset += c2Length
// 解码 C3 (OCTET STRING)
if (derBytes[offset] !== 0x04) {
throw new Error('Invalid DER: C3 is not an OCTET STRING')
}
offset += 1
const { length: c3Length, newOffset: c3Offset } = decodeLength(derBytes, offset)
offset = c3Offset
const c3Bytes = derBytes.slice(offset, offset + c3Length)
// 组合成 C1C3C2 格式
const c1c3c2 = bytesToHex(c1Bytes) + bytesToHex(c3Bytes) + bytesToHex(c2Bytes)
return c1c3c2
} }
+12
View File
@@ -9,3 +9,15 @@ export const utf8ToHex = (utf8: string): string => {
const bytes = new TextEncoder().encode(utf8) const bytes = new TextEncoder().encode(utf8)
return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0')).join('') return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0')).join('')
} }
// Hex 转 Base64
export const hexToBase64 = (hex: string): string => {
const bytes = new Uint8Array(hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || [])
return btoa(String.fromCharCode(...bytes))
}
// Base64 转 Hex
export const base64ToHex = (base64: string): string => {
const bytes = new Uint8Array(Array.from(atob(base64), char => char.charCodeAt(0)))
return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0')).join('')
}
+19
View File
@@ -0,0 +1,19 @@
export function sm4en3(key: string, data: string): string;
export function sm4de3(key: string, data: string): string;
export function getInfoFromPKCS7(p7sign: Buffer | string): {
cert: {
subject: Array<{ name: string; value: string }>;
subjectDN: string;
issuer: Array<{ name: string; value: string }>;
issuerDN: string;
serialNumber: string;
notBefore: string;
notAfter: string;
pem: string;
} | null;
publicKey: string | null;
publicKeyHex: string | null;
signature: string | null;
signatureHex: string | null;
};
export function verifyPKCS7(p7sign: Buffer | string, originalData?: Buffer | string): boolean;
+354
View File
@@ -0,0 +1,354 @@
import sm4 from 'sm-crypto'
import sm3 from 'sm-crypto'
import sm2 from 'sm-crypto'
import forge from 'node-forge'
import { KJUR, ASN1HEX, X509,pemtohex } from 'jsrsasign'
export function sm4en3(key, data) {
let sm3key = sm3(key)
sm3key = sm3key.substring(0, 32)
let encryptData = sm4.encrypt(data, sm3key)
return encryptData
}
export function sm4de3(key, data) {
let sm3key = sm3(key)
sm3key = sm3key.substring(0, 32)
let decryptData = sm4.decrypt(data, sm3key)
return decryptData
}
export function getInfoFromPKCS7(p7sign) {
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
if (p7sign.includes('-----BEGIN PKCS7-----')) {
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
return extractInfoFromForgeP7(p7)
} catch (forgeError) {
try {
return extractInfoFromJsrsasignP7(base64Data)
} catch (jsrsasignError) {
throw new Error(`Failed to parse PKCS7: ${forgeError.message}, ${jsrsasignError.message}`)
}
}
}
function extractInfoFromForgeP7(p7) {
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)
}
}
let publicKey = null
let publicKeyHex = null
if (p7.certificates && p7.certificates.length > 0) {
const certObj = p7.certificates[0]
if (certObj.publicKey) {
publicKey = forge.pki.publicKeyToPem(certObj.publicKey)
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(certObj.publicKey)
const publicKeyDer = forge.asn1.toDer(publicKeyAsn1)
publicKeyHex = forge.util.bytesToHex(publicKeyDer.getBytes())
}
}
let signature = null
let signatureHex = null
if (p7.rawCapture && p7.rawCapture.signature) {
signature = forge.util.encode64(p7.rawCapture.signature)
signatureHex = forge.util.bytesToHex(p7.rawCapture.signature)
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
function extractInfoFromJsrsasignP7(base64Data) {
const parser = new KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(ASN1HEX.b64tohex(base64Data))
let cert = null
let publicKey = null
let publicKeyHex = null
const certs = p7.certs && p7.certs.array ? p7.certs.array : []
if (certs.length > 0) {
const certPem = certs[0]
const x509 = new 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)
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
}
try {
const certHex = pemtohex(certPem)
const tbsCert = ASN1HEX.getTLVbyList(certHex, 0, [0])
const spki = ASN1HEX.getTLVbyList(tbsCert, 0, [6])
const publicKeyBitString = ASN1HEX.getTLVbyList(spki, 0, [1])
const publicKeyHexContent = publicKeyBitString.substring(6)
const algorithmSeq = ASN1HEX.getTLVbyList(spki, 0, [0])
const oidTlv = ASN1HEX.getTLVbyList(algorithmSeq, 0, [0])
const oidHex = oidTlv.substring(4)
const isSM2 = oidHex.includes('2A811CCF55') || oidHex.includes('2a811ccf55')
publicKey = KJUR.asn1.ASN1Util.getPEMStringFromHex(spki, 'PUBLIC KEY')
publicKeyHex = spki
} catch (e) {
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 = ASN1HEX.hextob64(signer.sighex)
}
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
export function verifyPKCS7(p7sign, originalData) {
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
if (p7sign.includes('-----BEGIN PKCS7-----')) {
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
let verifyData
if (originalData) {
if (Buffer.isBuffer(originalData)) {
verifyData = originalData
} else if (typeof originalData === 'string') {
verifyData = Buffer.from(originalData)
} else {
throw new Error('Invalid input type for originalData')
}
}
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
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')
}
}
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)
}
}
return p7.verify()
} catch (forgeError) {
try {
return verifyPKCS7WithJsrsasign(base64Data, verifyData)
} catch (jsrsasignError) {
return false
}
}
}
function verifyPKCS7WithJsrsasign(base64Data, verifyData) {
const parser = new KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(ASN1HEX.b64tohex(base64Data))
if (!verifyData) {
if (p7.econtent && p7.econtent.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 X509()
x509.readCertPEM(certPem)
const sigAlg = signer.sigalg
if (sigAlg === '1.2.156.10197.1.301.1' || sigAlg === '1.2.156.10197.1.501' || sigAlg.includes('sm2')) {
const certHex = pemtohex(certPem)
const tbsCert = ASN1HEX.getTLVbyList(certHex, 0, [0])
const spki = ASN1HEX.getTLVbyList(tbsCert, 0, [6])
const publicKeyBitString = ASN1HEX.getTLVbyList(spki, 0, [1])
let publicKeyHex = publicKeyBitString.substring(6)
if (!publicKeyHex.startsWith('04')) {
publicKeyHex = '04' + publicKeyHex
}
const hash = sm3(verifyData.toString())
let signatureHex = signer.sighex
const userId = '1234567812345678'
try {
const result1 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result1) return true
const result2 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true })
if (result2) return true
const result3 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true })
if (result3) return true
const result4 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result4) return true
const result5 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true })
if (result5) return true
const result6 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true })
if (result6) return true
const result7 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex)
if (result7) return true
const result8 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { hash: true, userId })
if (result8) return true
if (signatureHex.startsWith('30')) {
try {
const rStart = 8
const r = signatureHex.substring(rStart, rStart + 64)
const sStart = rStart + 64 + 4
const s = signatureHex.substring(sStart, sStart + 64)
const rawSignature = r + s
const result9 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex)
if (result9) return true
const result10 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex, { hash: true, userId })
if (result10) return true
} catch (e) {
}
}
} catch (e) {
}
return false
} else {
try {
const pubKey = x509.getPublicKey()
const sig = new 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)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export interface ExtractedCertificate {
der: Uint8Array
pem: string
hex: string
via: string
start: number
end: number
length: number
}
export function extractCertificate(input: string | ArrayBuffer | ArrayBufferView | Uint8Array): ExtractedCertificate
+1
View File
@@ -0,0 +1 @@
export { extractCertificate, extractCertificateFromText } from './extract-cert.ts'
+237
View File
@@ -0,0 +1,237 @@
/**
* 从 PKCS#7/CMS ContentInfo (DER) 中抽取内含的 X.509 证书。
*
* 可在浏览器和 Node 环境中使用。
* 支持输入:
* (a) CMS ContentInfo { OID, [0] EXPLICIT SEQUENCE(SignedData) }
* (b) 直接是一张 X.509 证书 DER
* (c) 其他 ASN.1 结构中嵌套证书 (fallback 全文扫描)
*/
interface TLV {
tag: number
headerLen: number
contentLen: number
totalLen: number
contentStart: number
contentEnd: number
}
interface LocatedCertificate {
range: { start: number; end: number }
via: string
}
export interface ExtractedCertificate {
der: Uint8Array
pem: string
hex: string
via: string
start: number
end: number
length: number
}
function toByteArray(input: string | ArrayBuffer | ArrayBufferView | Uint8Array | null | undefined): Uint8Array | null {
if (input == null) return null
if (input instanceof Uint8Array) return input
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
if (input instanceof ArrayBuffer) return new Uint8Array(input)
if (typeof input === 'string') {
const trimmed = input.trim()
if (!trimmed) return new Uint8Array(0)
if (/^-----BEGIN/.test(trimmed)) {
const base64Body = trimmed
.replace(/-----BEGIN CERTIFICATE-----/i, '')
.replace(/-----END CERTIFICATE-----/i, '')
.replace(/\s+/g, '')
return base64ToBytes(base64Body)
}
if (/^[0-9a-fA-F\s]+$/.test(trimmed)) {
return hexToBytes(trimmed)
}
return new TextEncoder().encode(trimmed)
}
return null
}
function base64ToBytes(value: string): Uint8Array {
if (typeof window !== 'undefined' && typeof window.atob === 'function') {
const binary = window.atob(value)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
return bytes
}
if (typeof Buffer !== 'undefined') {
return new Uint8Array(Buffer.from(value, 'base64'))
}
return new Uint8Array([])
}
function hexToBytes(value: string): Uint8Array {
const cleaned = value.replace(/\s+/g, '')
const bytes = new Uint8Array(cleaned.length / 2)
for (let i = 0; i < cleaned.length; i += 2) {
const chunk = cleaned.slice(i, i + 2)
if (!chunk) continue
bytes[i / 2] = Number.parseInt(chunk, 16)
}
return bytes
}
function bytesToHex(bytes: Uint8Array): string {
let out = ''
for (const byte of bytes) out += byte.toString(16).padStart(2, '0')
return out
}
function readTLV(buf: Uint8Array, offset: number): TLV | null {
if (offset >= buf.length) return null
const tag = buf[offset++]
if ((tag & 0x1f) === 0x1f) return null
let len = buf[offset++]
let headerLen = 2
if (len & 0x80) {
const n = len & 0x7f
if (n === 0 || offset + n > buf.length) return null
len = 0
for (let i = 0; i < n; i++) len = (len << 8) | buf[offset++]
headerLen = 2 + n
}
if (offset + len > buf.length) return null
return { tag, headerLen, contentLen: len, totalLen: headerLen + len, contentStart: offset, contentEnd: offset + len }
}
function readChildren(buf: Uint8Array, start: number, end: number): TLV[] {
const out: TLV[] = []
let pos = start
while (pos < end) {
const tlv = readTLV(buf, pos)
if (!tlv || tlv.totalLen <= 0) break
out.push(tlv)
pos += tlv.totalLen
}
return out
}
function looksLikeCertificate(buf: Uint8Array, tlv: TLV): boolean {
if (tlv.tag !== 0x30) return false
if (tlv.contentLen < 100 || tlv.contentLen > 8192) return false
const first = buf[tlv.contentStart]
if (first === 0x02) return true
if (first === 0xa0) {
const wrap = readTLV(buf, tlv.contentStart)
if (!wrap || wrap.tag !== 0xa0) return false
const inner = readTLV(buf, wrap.contentStart)
return !!(inner && inner.tag === 0x02)
}
return false
}
function findCertificateSEQUENCE(buf: Uint8Array): LocatedCertificate | null {
for (let i = 0; i < buf.length - 4; i++) {
if (buf[i] !== 0x30) continue
const tlv = readTLV(buf, i)
if (tlv && looksLikeCertificate(buf, tlv)) {
return { range: { start: i, end: i + tlv.totalLen }, via: 'fallback scan' }
}
}
return null
}
function pickCertInCertificatesField(buf: Uint8Array, start: number, end: number): LocatedCertificate | null {
if (end - start > 4 && buf[start] === 0x30) {
const wrap = readTLV(buf, start)
if (wrap && wrap.tag === 0x30 && wrap.totalLen === (end - start)) {
const items = readChildren(buf, wrap.contentStart, wrap.contentEnd)
if (items.length > 0 && looksLikeCertificate(buf, items[0])) {
return { range: { start: items[0].contentStart - items[0].headerLen, end: items[0].contentEnd }, via: 'CMS ContentInfo' }
}
}
}
for (const c of readChildren(buf, start, end)) {
if (looksLikeCertificate(buf, c)) {
return { range: { start: c.contentStart - c.headerLen, end: c.contentEnd }, via: 'CMS ContentInfo' }
}
}
return null
}
function findCertViaCMS(buf: Uint8Array): LocatedCertificate | null {
const top = readTLV(buf, 0)
if (!top || top.tag !== 0x30 || top.contentLen < 8) return null
const topChildren = readChildren(buf, top.contentStart, top.contentEnd)
if (topChildren.length < 2) return null
const wrap0 = topChildren[1]
if (wrap0.tag !== 0xa0) return null
const sdTLV = readTLV(buf, wrap0.contentStart)
if (!sdTLV || sdTLV.tag !== 0x30) return null
const sdChildren = readChildren(buf, sdTLV.contentStart, sdTLV.contentEnd)
if (sdChildren.length < 4) return null
const certsField = sdChildren[3]
if (certsField.tag !== 0xa0) return null
return pickCertInCertificatesField(buf, certsField.contentStart, certsField.contentEnd)
}
function locateCertificate(buf: Uint8Array): LocatedCertificate | null {
const tlv = readTLV(buf, 0)
if (tlv && tlv.tag === 0x30 && looksLikeCertificate(buf, tlv)) {
return { range: { start: 0, end: tlv.totalLen }, via: 'direct certificate' }
}
const cms = findCertViaCMS(buf)
if (cms) return cms
return findCertificateSEQUENCE(buf)
}
function bytesToBase64(bytes: Uint8Array): string {
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(binary)
}
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64')
}
return ''
}
function toPEM(der: Uint8Array): string {
const b64 = bytesToBase64(der)
const lines = b64.match(/.{1,64}/g)?.join('\n') || b64
return '-----BEGIN CERTIFICATE-----\n' + lines + '\n-----END CERTIFICATE-----\n'
}
export function extractCertificate(input: string | ArrayBuffer | ArrayBufferView | Uint8Array | null | undefined): ExtractedCertificate {
const buf = toByteArray(input)
if (!buf) {
throw new Error('unsupported input type')
}
const located = locateCertificate(buf)
if (!located) {
throw new Error('no X.509 certificate SEQUENCE found')
}
const { range, via } = located
const der = buf.subarray(range.start, range.end)
return {
der,
pem: toPEM(der),
hex: bytesToHex(der),
via,
start: range.start,
end: range.end,
length: der.length,
}
}
export function extractCertificateFromText(input: string | ArrayBuffer | ArrayBufferView | Uint8Array | null | undefined): ExtractedCertificate {
return extractCertificate(input)
}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "V1.0-20260417022550"; export const VERSION = "V1.0-20260824025705";
+190
View File
@@ -0,0 +1,190 @@
// 批量生成证书测试脚本
const fs = require('fs');
const path = require('path');
const forge = require('node-forge');
// 生成随机的序列号
const generateSerialNumber = () => {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
};
// 生成根证书
const generateRootCert = () => {
try {
// 生成密钥对
const keys = forge.pki.rsa.generateKeyPair(2048);
// 创建证书
const cert = forge.pki.createCertificate();
// 设置版本
cert.version = 3;
// 设置序列号
cert.serialNumber = generateSerialNumber();
// 设置主题和颁发者(根证书自己颁发自己)
const attrs = [
{ name: 'commonName', value: 'Root CA' },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Root Certificate Authority' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// 设置公钥
cert.publicKey = keys.publicKey;
// 设置有效期
const notBefore = new Date();
const notAfter = new Date();
notAfter.setFullYear(notBefore.getFullYear() + 10); // 10年有效期
cert.validity.notBefore = notBefore;
cert.validity.notAfter = notAfter;
// 添加扩展
cert.setExtensions([
{
name: 'basicConstraints',
cA: true
},
{
name: 'keyUsage',
digitalSignature: true,
keyCertSign: true,
cRLSign: true
}
]);
// 使用私钥自签名
cert.sign(keys.privateKey, forge.md.sha256.create());
// 转换为PEM格式
const certPem = forge.pki.certificateToPem(cert);
return { certPem, privateKey: keys.privateKey };
} catch (error) {
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error));
}
};
// 基于根证书生成应用证书
const generateAppCert = (commonName, rootCert, rootPrivateKey) => {
try {
// 解析根证书
const parsedRootCert = forge.pki.certificateFromPem(rootCert);
// 生成应用证书的密钥对
const appKeys = forge.pki.rsa.generateKeyPair(2048);
// 创建应用证书
const appCert = forge.pki.createCertificate();
// 设置版本
appCert.version = 3;
// 设置序列号
appCert.serialNumber = generateSerialNumber();
// 设置主题
const appAttrs = [
{ name: 'commonName', value: commonName },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Application' }
];
appCert.setSubject(appAttrs);
// 设置颁发者(根证书)
appCert.setIssuer(parsedRootCert.subject.attributes);
// 设置公钥
appCert.publicKey = appKeys.publicKey;
// 设置有效期
const notBefore = new Date();
const notAfter = new Date();
notAfter.setFullYear(notBefore.getFullYear() + 5); // 5年有效期
appCert.validity.notBefore = notBefore;
appCert.validity.notAfter = notAfter;
// 添加扩展
appCert.setExtensions([
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true
}
]);
// 使用根证书的私钥签名
appCert.sign(rootPrivateKey, forge.md.sha256.create());
// 转换为PEM格式
const appCertPem = forge.pki.certificateToPem(appCert);
return appCertPem;
} catch (error) {
throw new Error('Failed to generate application certificate: ' + (error instanceof Error ? error.message : error));
}
};
// 主函数
async function main() {
console.log('开始生成证书...');
// 创建输出目录
const outputDir = path.join(__dirname, 'certs');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
try {
// 生成根证书
console.log('1. 生成根证书...');
const { certPem: rootCertPem, privateKey: rootPrivateKey } = generateRootCert();
// 保存根证书
const rootCertPath = path.join(outputDir, 'root-cert.pem');
fs.writeFileSync(rootCertPath, rootCertPem);
console.log(`根证书已保存到: ${rootCertPath}`);
// 生成1000个用户证书
console.log('2. 生成1000个用户证书...');
for (let i = 1; i <= 1000; i++) {
const commonName = `user-${i}.example.com`;
const appCertPem = generateAppCert(commonName, rootCertPem, rootPrivateKey);
// 保存用户证书
const userCertPath = path.join(outputDir, `user-${i}-cert.pem`);
fs.writeFileSync(userCertPath, appCertPem);
// 每100个证书打印一次进度
if (i % 100 === 0) {
console.log(`已生成 ${i} 个用户证书`);
}
}
console.log('\n证书生成完成!');
console.log(`根证书路径: ${path.join(outputDir, 'root-cert.pem')}`);
console.log(`用户证书路径: ${outputDir}/user-*-cert.pem`);
console.log(`共生成 1 个根证书和 1000 个用户证书`);
} catch (error) {
console.error('生成证书失败:', error.message);
}
}
// 运行主函数
main();
+159
View File
@@ -0,0 +1,159 @@
// 测试证书模块 - 使用 CommonJS 模块系统
const forge = require('node-forge');
// 直接测试证书生成功能
console.log('=== 测试证书模块功能 ===');
// 生成随机的序列号
const generateSerialNumber = () => {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
};
// 测试生成根证书
console.log('\n1. 测试生成根证书');
try {
// 生成密钥对
const keys = forge.pki.rsa.generateKeyPair(2048);
// 创建证书
const cert = forge.pki.createCertificate();
// 设置版本
cert.version = 3;
// 设置序列号
cert.serialNumber = generateSerialNumber();
// 设置主题和颁发者(根证书自己颁发自己)
const attrs = [
{ name: 'commonName', value: 'Root CA' },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Root Certificate Authority' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// 设置公钥
cert.publicKey = keys.publicKey;
// 设置有效期
const notBefore = new Date();
const notAfter = new Date();
notAfter.setFullYear(notBefore.getFullYear() + 10); // 10年有效期
cert.validity.notBefore = notBefore;
cert.validity.notAfter = notAfter;
// 添加扩展
cert.setExtensions([
{
name: 'basicConstraints',
cA: true
},
{
name: 'keyUsage',
digitalSignature: true,
keyCertSign: true,
cRLSign: true
}
]);
// 使用私钥自签名
cert.sign(keys.privateKey, forge.md.sha256.create());
// 转换为PEM格式
const certPem = forge.pki.certificateToPem(cert);
console.log('✓ 根证书生成成功!');
console.log('根证书内容(前200字符):', certPem.substring(0, 200) + '...');
// 测试生成应用证书
console.log('\n2. 测试生成应用证书');
// 生成应用证书的密钥对
const appKeys = forge.pki.rsa.generateKeyPair(2048);
// 创建应用证书
const appCert = forge.pki.createCertificate();
// 设置版本
appCert.version = 3;
// 设置序列号
appCert.serialNumber = generateSerialNumber();
// 设置主题
const appAttrs = [
{ name: 'commonName', value: 'test.example.com' },
{ name: 'countryName', value: 'CN' },
{ shortName: 'ST', value: 'Beijing' },
{ name: 'localityName', value: 'Beijing' },
{ name: 'organizationName', value: 'Security Kit' },
{ shortName: 'OU', value: 'Application' }
];
appCert.setSubject(appAttrs);
// 设置颁发者(根证书)
appCert.setIssuer(cert.subject.attributes);
// 设置公钥
appCert.publicKey = appKeys.publicKey;
// 设置有效期
const appNotBefore = new Date();
const appNotAfter = new Date();
appNotAfter.setFullYear(appNotBefore.getFullYear() + 5); // 5年有效期
appCert.validity.notBefore = appNotBefore;
appCert.validity.notAfter = appNotAfter;
// 添加扩展
appCert.setExtensions([
{
name: 'basicConstraints',
cA: false
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true
},
{
name: 'extendedKeyUsage',
serverAuth: true,
clientAuth: true
}
]);
// 使用根证书的私钥签名
appCert.sign(keys.privateKey, forge.md.sha256.create());
// 转换为PEM格式
const appCertPem = forge.pki.certificateToPem(appCert);
console.log('✓ 应用证书生成成功!');
console.log('应用证书内容(前200字符):', appCertPem.substring(0, 200) + '...');
// 测试证书解析
console.log('\n3. 测试证书解析');
// 解析根证书
const parsedRootCert = forge.pki.certificateFromPem(certPem);
console.log('✓ 根证书解析成功!');
console.log('根证书主题:', parsedRootCert.subject.getField('CN').value);
// 解析应用证书
const parsedAppCert = forge.pki.certificateFromPem(appCertPem);
console.log('✓ 应用证书解析成功!');
console.log('应用证书主题:', parsedAppCert.subject.getField('CN').value);
console.log('应用证书颁发者:', parsedAppCert.issuer.getField('CN').value);
console.log('\n=== 所有测试通过!===');
} catch (error) {
console.error('测试失败:', error.message);
console.error(error.stack);
}
+22
View File
@@ -0,0 +1,22 @@
// 测试证书模块
import { generateRootCert, generateAppCert, parseSm2Cert } from './src/utils/algorithm/cert.js'
// 测试生成根证书
console.log('=== 测试生成根证书 ===')
try {
const rootCert = generateRootCert()
console.log('根证书生成成功!')
console.log('根证书内容(前200字符):', rootCert.substring(0, 200) + '...')
} catch (error) {
console.error('生成根证书失败:', error.message)
}
// 测试生成应用证书
console.log('\n=== 测试生成应用证书 ===')
try {
const appCert = generateAppCert('test.example.com')
console.log('应用证书生成成功!')
console.log('应用证书内容(前200字符):', appCert.substring(0, 200) + '...')
} catch (error) {
console.error('生成应用证书失败:', error.message)
}
+7 -1
View File
@@ -4,5 +4,11 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: './' resolve: {
alias: {
crypto: 'crypto-browserify',
stream: 'stream-browserify',
buffer: 'buffer'
}
}
}) })