Compare commits

...
7 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
12 changed files with 1246 additions and 216 deletions
+2 -1
View File
@@ -2,4 +2,5 @@
- [x] CI 集成,将 dist 传输到指定服务器指定目录。 - [x] CI 集成,将 dist 传输到指定服务器指定目录。
- [x] 添加 SM2 算法 - [x] 添加 SM2 算法
- [x] 证书工具支持输入逗号分隔证书 id 并下载 DER 格式 CRL 文件。
- [x] 根证书生成后支持下载 CRT 文件和根证书私钥。
+278 -130
View File
@@ -30,14 +30,14 @@
"@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"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -623,9 +623,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -636,13 +636,13 @@
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -653,13 +653,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -670,13 +670,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -687,13 +687,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -704,13 +704,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -721,13 +721,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -738,13 +738,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -755,13 +755,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -772,13 +772,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -789,13 +789,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -806,13 +806,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -823,13 +823,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -840,13 +840,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -857,13 +857,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -874,13 +874,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -891,13 +891,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -908,13 +908,30 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -925,13 +942,30 @@
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -942,13 +976,30 @@
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -959,13 +1010,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -976,13 +1027,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -993,13 +1044,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1010,7 +1061,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
@@ -4055,9 +4106,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.25.12",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -4065,32 +4116,35 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5", "@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.21.5", "@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.21.5", "@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.21.5", "@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.21.5", "@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.21.5", "@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.21.5", "@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-x64": "0.21.5", "@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.21.5", "@esbuild/netbsd-x64": "0.25.12",
"@esbuild/sunos-x64": "0.21.5", "@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/win32-arm64": "0.21.5", "@esbuild/openbsd-x64": "0.25.12",
"@esbuild/win32-ia32": "0.21.5", "@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/win32-x64": "0.21.5" "@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -7599,6 +7653,54 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
@@ -7912,21 +8014,24 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "6.4.3",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.25.0",
"postcss": "^8.4.43", "fdir": "^6.4.4",
"rollup": "^4.20.0" "picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@@ -7935,19 +8040,25 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*", "sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": { "@types/node": {
"optional": true "optional": true
}, },
"jiti": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@@ -7968,9 +8079,46 @@
}, },
"terser": { "terser": {
"optional": true "optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
} }
} }
}, },
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/walker": { "node_modules/walker": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz", "resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz",
+2 -2
View File
@@ -33,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"
} }
} }
+385 -8
View File
@@ -3,10 +3,11 @@ import { VERSION } from './version'
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3' import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4' import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify } from './utils/algorithm/sm2' import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify, sm2DerToC1C3C2, sm2C1C3C2ToDer } from './utils/algorithm/sm2'
import { parseSm2Cert, parseCertFromFile, generateRootCert, generateAppCert } from './utils/algorithm/cert' import { parseSm2Cert, parseCertFromFile, generateRootCert, generateAppCert, generateCrlFile, loadRootPrivateKeyFromCache } from './utils/algorithm/cert'
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert' import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
import { getInfoFromPKCS7, verifyPKCS7 } from './utils/crypto.js' import { getInfoFromPKCS7, verifyPKCS7 } from './utils/crypto.js'
import { extractCertificate } from './utils/extract-cert'
function App() { function App() {
// 从localStorage读取当前分类,默认sm3 // 从localStorage读取当前分类,默认sm3
@@ -173,6 +174,8 @@ function App() {
const [hexInput, setHexInput] = useState('') const [hexInput, setHexInput] = useState('')
const [utf8Input, setUtf8Input] = useState('') const [utf8Input, setUtf8Input] = useState('')
const [base64Input, setBase64Input] = useState('') const [base64Input, setBase64Input] = useState('')
const [derInput, setDerInput] = useState('')
const [c1c3c2Input, setC1c3c2Input] = useState('')
// 证书工具状态 // 证书工具状态
const [certInfo, setCertInfo] = useState<any>(null) const [certInfo, setCertInfo] = useState<any>(null)
@@ -180,12 +183,16 @@ function App() {
const [appCert, setAppCert] = useState<string>('') const [appCert, setAppCert] = useState<string>('')
const [commonName, setCommonName] = useState<string>('test.example.com') const [commonName, setCommonName] = useState<string>('test.example.com')
const [certError, setCertError] = useState<string>('') const [certError, setCertError] = useState<string>('')
const [crlCertificateIds, setCrlCertificateIds] = useState<string>('')
// PKCS7 签名工具状态 // PKCS7 签名工具状态
const [pkcs7Signature, setPkcs7Signature] = useState<string>('') const [pkcs7Signature, setPkcs7Signature] = useState<string>('')
const [pkcs7Info, setPkcs7Info] = useState<any>(null) const [pkcs7Info, setPkcs7Info] = useState<any>(null)
const [originalData, setOriginalData] = useState<string>('') const [originalData, setOriginalData] = useState<string>('')
const [verifyResult, setVerifyResult] = useState<string>('') const [verifyResult, setVerifyResult] = useState<string>('')
const [extractInput, setExtractInput] = useState<string>('')
const [extractedCert, setExtractedCert] = useState<any>(null)
const [extractError, setExtractError] = useState<string>('')
@@ -269,6 +276,26 @@ function App() {
} }
} }
// DER 转 C1C3C2
const handleDerToC1C3C2 = () => {
try {
const c1c3c2 = sm2DerToC1C3C2(derInput)
setC1c3c2Input(c1c3c2)
} catch (error) {
alert('转换失败: ' + (error instanceof Error ? error.message : error))
}
}
// C1C3C2 转 DER
const handleC1C3C2ToDer = () => {
try {
const der = sm2C1C3C2ToDer(c1c3c2Input)
setDerInput(der)
} catch (error) {
alert('转换失败: ' + (error instanceof Error ? error.message : error))
}
}
// 处理证书文件上传 // 处理证书文件上传
const handleCertFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCertFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
@@ -314,6 +341,46 @@ function App() {
} }
} }
// 下载文本文件
// 参数:content 为文件内容,fileName 为下载文件名,mimeType 为文件类型。
// 返回值:无。
const downloadTextFile = (content: string, fileName: string, mimeType: string) => {
const blob = new Blob([content], { type: mimeType })
const downloadUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(downloadUrl)
}
// 处理根证书文件下载
// 参数:无。
// 返回值:无。
const handleDownloadRootCert = () => {
if (!rootCert) {
setCertError('请先生成根证书')
return
}
downloadTextFile(rootCert, 'root-cert.crt', 'application/x-x509-ca-cert')
setCertError('')
}
// 处理根证书私钥下载
// 参数:无。
// 返回值:无。
const handleDownloadRootPrivateKey = () => {
const rootPrivateKey = loadRootPrivateKeyFromCache()
if (!rootPrivateKey) {
setCertError('根证书私钥不存在,请重新生成根证书')
return
}
downloadTextFile(rootPrivateKey, 'root-private-key.key', 'application/x-pem-file')
setCertError('')
}
// 处理生成应用证书 // 处理生成应用证书
const handleGenerateAppCert = () => { const handleGenerateAppCert = () => {
try { try {
@@ -325,6 +392,58 @@ function App() {
} }
} }
// 处理 CRL 文件下载
// 参数:无。
// 返回值:无。
// 注意事项:证书 id 按证书序列号处理,多个 id 使用英文逗号分隔。
const handleDownloadCrlFile = () => {
try {
const crlFile = generateCrlFile(crlCertificateIds)
const blob = new Blob([crlFile], { type: 'application/pkix-crl' })
const downloadUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = 'certificate-revocation-list.crl'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(downloadUrl)
setCertError('')
} catch (error) {
setCertError((error as Error).message)
}
}
const handleExtractCertificate = () => {
try {
const result = extractCertificate(extractInput)
setExtractedCert(result)
setExtractError('')
} catch (error) {
setExtractError((error as Error).message)
setExtractedCert(null)
}
}
const handleDownloadExtractedCert = (type: 'der' | 'pem') => {
if (!extractedCert) {
setExtractError('请先提取证书')
return
}
const content = type === 'der' ? extractedCert.der : extractedCert.pem
const blob = new Blob([content], { type: type === 'der' ? 'application/octet-stream' : 'application/x-pem-file' })
const downloadUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = type === 'der' ? 'extracted-cert.der' : 'extracted-cert.pem'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(downloadUrl)
setExtractError('')
}
// 处理 PKCS7 签名信息提取 // 处理 PKCS7 签名信息提取
const handleGetInfoFromPKCS7 = () => { const handleGetInfoFromPKCS7 = () => {
try { try {
@@ -384,7 +503,10 @@ function App() {
const handleDecryptSm2 = () => { const handleDecryptSm2 = () => {
try { try {
const decrypted = sm2Decrypt(sm2Encrypted, sm2PrivateKey) const decrypted = sm2Decrypt(sm2Encrypted, sm2PrivateKey)
setSm2Decrypted(decrypted) const hexResult = Array.from(decrypted)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('')
setSm2Decrypted(hexResult)
} catch (error) { } catch (error) {
alert('解密失败: ' + (error instanceof Error ? error.message : error)) alert('解密失败: ' + (error instanceof Error ? error.message : error))
} }
@@ -1452,7 +1574,7 @@ function App() {
transition: 'background 0.3s ease' transition: 'background 0.3s ease'
}} }}
> >
Hex ← Base64 Base64 → Hex
</button> </button>
</div> </div>
@@ -1501,6 +1623,100 @@ function App() {
</div> </div>
</div> </div>
</div> </div>
{/* DER / C1C3C2 互转卡片 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
<h3 style={{ color: '#e0e0e0', marginBottom: '15px' }}>SM2 DER / C1C3C2 密文格式互转</h3>
<div style={{ display: 'flex', gap: '1rem', flexDirection: isMobile ? 'column' : 'row' }}>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>DER 格式输入 (hex)</label>
<textarea
value={derInput}
onChange={(e) => setDerInput(e.target.value)}
onContextMenu={(e) => openMenu(e, derInput, setDerInput, false)}
onTouchStart={(e) => {
const touchTimer = setTimeout(() => {
openMenu(e, derInput, setDerInput, false)
}, 500)
return () => clearTimeout(touchTimer)
}}
placeholder="输入 DER 格式密文 (hex)"
style={{
height: '150px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#1e1e1e',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button
onClick={handleDerToC1C3C2}
style={{
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
DER → C1C3C2
</button>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>C1C3C2 格式输入 (hex)</label>
<textarea
value={c1c3c2Input}
onChange={(e) => setC1c3c2Input(e.target.value)}
onContextMenu={(e) => openMenu(e, c1c3c2Input, setC1c3c2Input, false)}
onTouchStart={(e) => {
const touchTimer = setTimeout(() => {
openMenu(e, c1c3c2Input, setC1c3c2Input, false)
}, 500)
return () => clearTimeout(touchTimer)
}}
placeholder="输入 C1C3C2 格式密文 (hex)"
style={{
height: '150px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#1e1e1e',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button
onClick={handleC1C3C2ToDer}
style={{
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
C1C3C2 → DER
</button>
</div>
</div>
</div>
</div> </div>
)} )}
@@ -1559,6 +1775,40 @@ function App() {
minHeight: '150px' minHeight: '150px'
}} }}
/> />
<div style={{ display: 'flex', gap: '10px', marginTop: '10px', flexWrap: 'wrap' }}>
<button
onClick={handleDownloadRootCert}
style={{
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
下载根证书 CRT
</button>
<button
onClick={handleDownloadRootPrivateKey}
style={{
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
下载根证书私钥
</button>
</div>
</div> </div>
)} )}
</div> </div>
@@ -1636,9 +1886,136 @@ function App() {
)} )}
</div> </div>
{/* 生成 CRL 文件 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '18px', borderRadius: '8px', marginBottom: '18px' }}>
<h3 style={{ color: '#f59e0b', marginBottom: '12px', fontSize: '18px' }}>3. 生成 CRL 文件</h3>
<div className="form-group" style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>证书 id:</label>
<textarea
value={crlCertificateIds}
onChange={(e) => setCrlCertificateIds(e.target.value)}
onContextMenu={(e) => openMenu(e, crlCertificateIds, setCrlCertificateIds, false)}
onTouchStart={(e) => {
const touchTimer = setTimeout(() => {
openMenu(e, crlCertificateIds, setCrlCertificateIds, false)
}, 500)
return () => clearTimeout(touchTimer)
}}
placeholder="可选,输入多个逗号分隔的证书 id,例如:01,02,0a"
style={{
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#3d3d3d',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical',
minHeight: '80px'
}}
/>
</div>
<button
onClick={handleDownloadCrlFile}
style={{
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
下载 CRL 文件
</button>
</div>
{/* 提取证书 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '18px', borderRadius: '8px', marginBottom: '18px' }}>
<h3 style={{ color: '#f59e0b', marginBottom: '12px', fontSize: '18px' }}>4. 提取证书</h3>
<div className="form-group" style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>输入内容(DER / HEX / PEM / CMS)</label>
<textarea
value={extractInput}
onChange={(e) => setExtractInput(e.target.value)}
onContextMenu={(e) => openMenu(e, extractInput, setExtractInput, false)}
onTouchStart={(e) => {
const touchTimer = setTimeout(() => {
openMenu(e, extractInput, setExtractInput, false)
}, 500)
return () => clearTimeout(touchTimer)
}}
placeholder="可直接粘贴证书 DER/HEX/PEM 或 PKCS#7/CMS 数据"
style={{
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#3d3d3d',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical',
minHeight: '140px'
}}
/>
</div>
<button
onClick={handleExtractCertificate}
style={{
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
提取证书
</button>
{extractError && (
<div style={{ marginTop: '10px', color: '#fca5a5' }}>{extractError}</div>
)}
{extractedCert && (
<div style={{ marginTop: '15px', backgroundColor: '#3d3d3d', padding: '15px', borderRadius: '8px' }}>
<h4 style={{ color: '#f59e0b', marginBottom: '10px' }}>提取结果</h4>
<div style={{ marginBottom: '10px', color: '#e0e0e0' }}>
<span style={{ fontWeight: 'bold' }}>来源:</span> {extractedCert.via}
</div>
<div style={{ marginBottom: '10px', color: '#e0e0e0' }}>
<span style={{ fontWeight: 'bold' }}>长度:</span> {extractedCert.length} bytes
</div>
<textarea
value={extractedCert.pem}
readOnly
style={{
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#2d2d2d',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical',
minHeight: '160px'
}}
/>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px', flexWrap: 'wrap' }}>
<button onClick={() => handleDownloadExtractedCert('der')} style={{ padding: '10px 16px', border: 'none', borderRadius: '4px', background: '#f59e0b', color: '#121212', fontWeight: 'bold', cursor: 'pointer' }}>下载 DER</button>
<button onClick={() => handleDownloadExtractedCert('pem')} style={{ padding: '10px 16px', border: 'none', borderRadius: '4px', background: '#f59e0b', color: '#121212', fontWeight: 'bold', cursor: 'pointer' }}>下载 PEM</button>
</div>
</div>
)}
</div>
{/* 证书解析 */} {/* 证书解析 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px' }}> <div style={{ backgroundColor: '#2d2d2d', padding: '18px', borderRadius: '8px', marginBottom: '18px' }}>
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>3. 证书解析</h3> <h3 style={{ color: '#f59e0b', marginBottom: '12px', fontSize: '18px' }}>5. 证书解析</h3>
<div className="form-group" style={{ marginBottom: '20px' }}> <div className="form-group" style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label> <label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}> <div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
@@ -1736,8 +2113,8 @@ function App() {
</div> </div>
{/* PKCS7 签名工具 */} {/* PKCS7 签名工具 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}> <div style={{ backgroundColor: '#2d2d2d', padding: '18px', borderRadius: '8px' }}>
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>4. PKCS7 签名工具</h3> <h3 style={{ color: '#f59e0b', marginBottom: '12px', fontSize: '18px' }}>6. PKCS7 签名工具</h3>
<div className="form-group" style={{ marginBottom: '15px' }}> <div className="form-group" style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>PKCS7 签名数据</label> <label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>PKCS7 签名数据</label>
+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')
})
})
+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)
})
})
+253 -65
View File
@@ -1,4 +1,15 @@
import forge from 'node-forge' 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字符串(浏览器兼容) // 将ArrayBuffer转换为Base64字符串(浏览器兼容)
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => { const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
@@ -28,11 +39,15 @@ const getCacheDirectory = (): string => {
} }
} }
// 保存根证书到本地缓存 // 保存根证书和私钥到本地缓存
const saveRootCertToCache = (certPem: string): void => { // 参数:certPem 为 PEM 格式根证书,privateKeyPem 为 PEM 格式根证书私钥。
// 返回值:无。
// 注意事项:浏览器环境保存到 localStorage,Node.js 环境保存到 cache 目录。
const saveRootCertToCache = (certPem: string, privateKeyPem: string): void => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// 浏览器环境:使用localStorage // 浏览器环境:使用localStorage
localStorage.setItem('rootCert', certPem) localStorage.setItem(ROOT_CERT_CACHE_KEY, certPem)
localStorage.setItem(ROOT_PRIVATE_KEY_CACHE_KEY, privateKeyPem)
} else { } else {
// Node.js环境:使用文件系统 // Node.js环境:使用文件系统
const fs = require('fs') const fs = require('fs')
@@ -46,15 +61,19 @@ const saveRootCertToCache = (certPem: string): void => {
// 保存证书到文件 // 保存证书到文件
const certPath = path.join(cacheDir, 'rootCert.pem') const certPath = path.join(cacheDir, 'rootCert.pem')
const privateKeyPath = path.join(cacheDir, 'rootPrivateKey.pem')
fs.writeFileSync(certPath, certPem) fs.writeFileSync(certPath, certPem)
fs.writeFileSync(privateKeyPath, privateKeyPem)
} }
} }
// 从本地缓存读取根证书 // 从本地缓存读取根证书
// 参数:无。
// 返回值:存在时返回 PEM 格式根证书,否则返回 null。
const loadRootCertFromCache = (): string | null => { const loadRootCertFromCache = (): string | null => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// 浏览器环境:从localStorage读取 // 浏览器环境:从localStorage读取
return localStorage.getItem('rootCert') return localStorage.getItem(ROOT_CERT_CACHE_KEY)
} else { } else {
// Node.js环境:从文件系统读取 // Node.js环境:从文件系统读取
const fs = require('fs') const fs = require('fs')
@@ -69,68 +88,100 @@ const loadRootCertFromCache = (): string | 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 => { export const generateRootCert = (): string => {
try { try {
// 生成密钥对 const rootCertificateBundle = createRootCertificateBundle()
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)
// 保存到本地缓存 // 保存到本地缓存
saveRootCertToCache(certPem) saveRootCertToCache(rootCertificateBundle.certPem, rootCertificateBundle.privateKeyPem)
return certPem return rootCertificateBundle.certPem
} catch (error) { } catch (error) {
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error)) throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error))
} }
@@ -144,6 +195,10 @@ export const generateAppCert = (commonName: string): string => {
if (!rootCertPem) { if (!rootCertPem) {
throw new Error('Root certificate not found in cache. Please generate root certificate first.') 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 rootCert = forge.pki.certificateFromPem(rootCertPem)
@@ -203,10 +258,8 @@ export const generateAppCert = (commonName: string): string => {
} }
]) ])
// 使用根证书的私钥签名(注意:这里需要根证书的私钥,实际应用中需要安全存储) // 使用根证书的私钥签名
// 由于我们没有保存根证书的私钥,这里使用模拟的私钥签名 const rootPrivateKey = forge.pki.privateKeyFromPem(rootPrivateKeyPem)
// 实际应用中,应该在生成根证书时同时保存私钥
const rootPrivateKey = forge.pki.privateKeyFromPem(rootCertPem)
appCert.sign(rootPrivateKey, forge.md.sha256.create()) appCert.sign(rootPrivateKey, forge.md.sha256.create())
// 转换为PEM格式 // 转换为PEM格式
@@ -218,6 +271,141 @@ export const generateAppCert = (commonName: string): string => {
} }
} }
// 将证书 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证书信息 // 解析SM2证书信息
export const parseSm2Cert = (certData: string): any => { export const parseSm2Cert = (certData: string): any => {
try { try {
+10 -8
View File
@@ -51,7 +51,7 @@ export const sm2Encrypt = (message: string, publicKey: string, cipherMode: numbe
} }
// SM2 解密 // SM2 解密
export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: number = 1): string => { export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: number = 1): Uint8Array => {
// 验证密文是否为有效的 hex 字符串 // 验证密文是否为有效的 hex 字符串
if (!isValidHex(ciphertext)) { if (!isValidHex(ciphertext)) {
throw new Error('Invalid ciphertext: must be hex string') throw new Error('Invalid ciphertext: must be hex string')
@@ -61,13 +61,15 @@ export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: n
throw new Error('Invalid private key: must be hex string') throw new Error('Invalid private key: must be hex string')
} }
// 解密 // 解密,指定 output 为 'bytes' 以获取 Uint8Array 格式
const decrypted = sm2Extended.doDecrypt(ciphertext, privateKey, cipherMode) const decrypted = sm2Extended.doDecrypt(ciphertext, privateKey, cipherMode, { output: 'bytes' })
// 将解密结果转换为 hex 字符串
const decryptedBytes = new TextEncoder().encode(decrypted) // 如果返回的是字符串,则转为 Uint8Array
return Array.from(decryptedBytes) if (typeof decrypted === 'string') {
.map(byte => byte.toString(16).padStart(2, '0')) return new TextEncoder().encode(decrypted)
.join('') }
return decrypted
} }
// SM2 签名 // SM2 签名
+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-20260426034359"; export const VERSION = "V1.0-20260824025705";