Compare commits
15
Commits
c9d66a77b1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce31ab2200 | ||
|
|
c6126b123a | ||
|
|
2adcf8f077 | ||
|
|
2a2e9e0d6b | ||
|
|
951819fb96 | ||
|
|
ab9441982f | ||
|
|
4b3d5b762f | ||
|
|
045e939dcd | ||
|
|
32626c84a6 | ||
|
|
58533aa5d0 | ||
|
|
3459a389ac | ||
|
|
f4228370d9 | ||
|
|
88acc24e86 | ||
|
|
b757597db9 | ||
|
|
20c27b2e5b |
@@ -41,3 +41,6 @@ out
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
|
||||||
|
certs
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
- [x] CI 集成,将 dist 传输到指定服务器指定目录。
|
- [x] CI 集成,将 dist 传输到指定服务器指定目录。
|
||||||
- [x] 添加 SM2 算法
|
- [x] 添加 SM2 算法
|
||||||
|
- [x] 证书工具支持输入逗号分隔证书 id 并下载 DER 格式 CRL 文件。
|
||||||
|
- [x] 根证书生成后支持下载 CRT 文件和根证书私钥。
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
设计规范
|
||||||
|
|
||||||
|
- 所有输入框,双击后弹出提示框,确认后可以清空内容。移动端为长按弹出提示框,确认后可以清空内容。
|
||||||
|
|
||||||
Generated
+299
-132
@@ -9,12 +9,15 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/webcrypto": "^1.5.0",
|
"@peculiar/webcrypto": "^1.5.0",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.1",
|
"crypto-browserify": "^3.12.1",
|
||||||
"gm-crypto": "*",
|
"gm-crypto": "*",
|
||||||
"gmsm-sm2js": "^0.7.1",
|
"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",
|
||||||
@@ -27,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": {
|
||||||
@@ -620,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"
|
||||||
],
|
],
|
||||||
@@ -633,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"
|
||||||
],
|
],
|
||||||
@@ -650,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"
|
||||||
],
|
],
|
||||||
@@ -667,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"
|
||||||
],
|
],
|
||||||
@@ -684,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"
|
||||||
],
|
],
|
||||||
@@ -701,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"
|
||||||
],
|
],
|
||||||
@@ -718,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"
|
||||||
],
|
],
|
||||||
@@ -735,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"
|
||||||
],
|
],
|
||||||
@@ -752,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"
|
||||||
],
|
],
|
||||||
@@ -769,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"
|
||||||
],
|
],
|
||||||
@@ -786,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"
|
||||||
],
|
],
|
||||||
@@ -803,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"
|
||||||
],
|
],
|
||||||
@@ -820,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"
|
||||||
],
|
],
|
||||||
@@ -837,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"
|
||||||
],
|
],
|
||||||
@@ -854,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"
|
||||||
],
|
],
|
||||||
@@ -871,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"
|
||||||
],
|
],
|
||||||
@@ -888,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"
|
||||||
],
|
],
|
||||||
@@ -905,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"
|
||||||
],
|
],
|
||||||
@@ -922,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"
|
||||||
],
|
],
|
||||||
@@ -939,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"
|
||||||
],
|
],
|
||||||
@@ -956,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"
|
||||||
],
|
],
|
||||||
@@ -973,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"
|
||||||
],
|
],
|
||||||
@@ -990,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"
|
||||||
],
|
],
|
||||||
@@ -1007,7 +1061,7 @@
|
|||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
@@ -2417,12 +2471,20 @@
|
|||||||
"version": "25.6.0",
|
"version": "25.6.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz",
|
||||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.19.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-forge": {
|
||||||
|
"version": "1.3.14",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/node-forge/-/node-forge-1.3.14.tgz",
|
||||||
|
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
@@ -4044,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",
|
||||||
@@ -4054,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": {
|
||||||
@@ -6386,6 +6451,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-forge": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||||
|
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -7579,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",
|
||||||
@@ -7792,7 +7914,6 @@
|
|||||||
"version": "7.19.2",
|
"version": "7.19.2",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
@@ -7893,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"
|
||||||
@@ -7916,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
|
||||||
},
|
},
|
||||||
@@ -7949,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",
|
||||||
|
|||||||
+5
-2
@@ -12,12 +12,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/webcrypto": "^1.5.0",
|
"@peculiar/webcrypto": "^1.5.0",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.1",
|
"crypto-browserify": "^3.12.1",
|
||||||
"gm-crypto": "*",
|
"gm-crypto": "*",
|
||||||
"gmsm-sm2js": "^0.7.1",
|
"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",
|
||||||
@@ -30,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1584
-244
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sm2GenerateKeyPair, 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 => {
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Vendored
+3
-2
@@ -6,10 +6,11 @@ declare module 'sm-crypto' {
|
|||||||
|
|
||||||
export const sm2: {
|
export const sm2: {
|
||||||
generateKeyPairHex: (privateKey?: string) => SM2KeyPair;
|
generateKeyPairHex: (privateKey?: string) => SM2KeyPair;
|
||||||
doEncrypt: (message: string, publicKey: string) => string;
|
doEncrypt: (message: string, publicKey: string, cipherMode?: number) => string;
|
||||||
doDecrypt: (ciphertext: string, privateKey: string) => 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;
|
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;
|
doVerifySignature: (message: string | Uint8Array, signature: string, publicKey: string, options?: { hash?: boolean, der?: boolean, userId?: string }) => boolean;
|
||||||
|
getPublicKeyFromPrivateKey: (privateKey: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sm3: {
|
export const sm3: {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+216
-12
@@ -3,6 +3,9 @@ import { isValidHex, hexToArrayBuffer } from '../convert/hex'
|
|||||||
// 导入 sm-crypto 库,它在浏览器环境中可用
|
// 导入 sm-crypto 库,它在浏览器环境中可用
|
||||||
import { sm2 } from 'sm-crypto'
|
import { sm2 } from 'sm-crypto'
|
||||||
|
|
||||||
|
// 类型断言,确保 sm2 对象具有我们需要的方法
|
||||||
|
const sm2Extended = sm2 as any
|
||||||
|
|
||||||
// SM2 密钥对生成
|
// SM2 密钥对生成
|
||||||
export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => {
|
export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => {
|
||||||
const keypair = sm2.generateKeyPairHex()
|
const keypair = sm2.generateKeyPairHex()
|
||||||
@@ -15,16 +18,19 @@ export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string }
|
|||||||
// SM2 从私钥计算公钥
|
// SM2 从私钥计算公钥
|
||||||
export const sm2GetPublicKeyFromPrivateKey = (privateKey: string): string => {
|
export const sm2GetPublicKeyFromPrivateKey = (privateKey: string): string => {
|
||||||
// 验证私钥是否为有效的 hex 字符串
|
// 验证私钥是否为有效的 hex 字符串
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error('Private key is required')
|
||||||
|
}
|
||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
const keypair = sm2.generateKeyPairHex(privateKey)
|
const publicKey = sm2.getPublicKeyFromPrivateKey(privateKey)
|
||||||
return keypair.publicKey
|
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')
|
||||||
@@ -40,12 +46,12 @@ export const sm2Encrypt = (message: string, publicKey: string): string => {
|
|||||||
const messageString = new TextDecoder().decode(messageBytes)
|
const messageString = new TextDecoder().decode(messageBytes)
|
||||||
|
|
||||||
// 加密
|
// 加密
|
||||||
const encrypted = sm2.doEncrypt(messageString, publicKey)
|
const encrypted = sm2Extended.doEncrypt(messageString, publicKey, cipherMode)
|
||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
// SM2 解密
|
// SM2 解密
|
||||||
export const sm2Decrypt = (ciphertext: string, privateKey: string): 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')
|
||||||
@@ -55,13 +61,15 @@ export const sm2Decrypt = (ciphertext: string, privateKey: string): string => {
|
|||||||
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 = sm2.doDecrypt(ciphertext, privateKey)
|
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 签名
|
||||||
@@ -103,3 +111,199 @@ export const sm2Verify = (message: string, signature: string, publicKey: string)
|
|||||||
const result = sm2.doVerifySignature(messageBytes, signature, publicKey, { hash: false })
|
const result = sm2.doVerifySignature(messageBytes, signature, publicKey, { hash: false })
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+19
@@ -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;
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+11
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { extractCertificate, extractCertificateFromText } from './extract-cert.ts'
|
||||||
@@ -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
@@ -1 +1 @@
|
|||||||
export const VERSION = "V1.0-20260418053331";
|
export const VERSION = "V1.0-20260824025705";
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user