This commit is contained in:
parent
951819fb96
commit
2a2e9e0d6b
@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
- [x] CI 集成,将 dist 传输到指定服务器指定目录。
|
- [x] CI 集成,将 dist 传输到指定服务器指定目录。
|
||||||
- [x] 添加 SM2 算法
|
- [x] 添加 SM2 算法
|
||||||
|
- [x] 证书工具支持输入逗号分隔证书 id 并下载 DER 格式 CRL 文件。
|
||||||
|
- [x] 根证书生成后支持下载 CRT 文件和根证书私钥。
|
||||||
|
|||||||
150
src/App.tsx
150
src/App.tsx
@ -4,7 +4,7 @@ import { VERSION } from './version'
|
|||||||
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
|
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
|
||||||
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
|
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
|
||||||
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify, sm2DerToC1C3C2, sm2C1C3C2ToDer } from './utils/algorithm/sm2'
|
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify, sm2DerToC1C3C2, sm2C1C3C2ToDer } from './utils/algorithm/sm2'
|
||||||
import { parseSm2Cert, parseCertFromFile, generateRootCert, generateAppCert } from './utils/algorithm/cert'
|
import { parseSm2Cert, parseCertFromFile, generateRootCert, generateAppCert, generateCrlFile, loadRootPrivateKeyFromCache } from './utils/algorithm/cert'
|
||||||
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
||||||
import { getInfoFromPKCS7, verifyPKCS7 } from './utils/crypto.js'
|
import { getInfoFromPKCS7, verifyPKCS7 } from './utils/crypto.js'
|
||||||
|
|
||||||
@ -182,6 +182,7 @@ function App() {
|
|||||||
const [appCert, setAppCert] = useState<string>('')
|
const [appCert, setAppCert] = useState<string>('')
|
||||||
const [commonName, setCommonName] = useState<string>('test.example.com')
|
const [commonName, setCommonName] = useState<string>('test.example.com')
|
||||||
const [certError, setCertError] = useState<string>('')
|
const [certError, setCertError] = useState<string>('')
|
||||||
|
const [crlCertificateIds, setCrlCertificateIds] = useState<string>('')
|
||||||
|
|
||||||
// PKCS7 签名工具状态
|
// PKCS7 签名工具状态
|
||||||
const [pkcs7Signature, setPkcs7Signature] = useState<string>('')
|
const [pkcs7Signature, setPkcs7Signature] = useState<string>('')
|
||||||
@ -336,6 +337,46 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载文本文件
|
||||||
|
// 参数:content 为文件内容,fileName 为下载文件名,mimeType 为文件类型。
|
||||||
|
// 返回值:无。
|
||||||
|
const downloadTextFile = (content: string, fileName: string, mimeType: string) => {
|
||||||
|
const blob = new Blob([content], { type: mimeType })
|
||||||
|
const downloadUrl = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(downloadUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理根证书文件下载
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:无。
|
||||||
|
const handleDownloadRootCert = () => {
|
||||||
|
if (!rootCert) {
|
||||||
|
setCertError('请先生成根证书')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadTextFile(rootCert, 'root-cert.crt', 'application/x-x509-ca-cert')
|
||||||
|
setCertError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理根证书私钥下载
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:无。
|
||||||
|
const handleDownloadRootPrivateKey = () => {
|
||||||
|
const rootPrivateKey = loadRootPrivateKeyFromCache()
|
||||||
|
if (!rootPrivateKey) {
|
||||||
|
setCertError('根证书私钥不存在,请重新生成根证书')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadTextFile(rootPrivateKey, 'root-private-key.key', 'application/x-pem-file')
|
||||||
|
setCertError('')
|
||||||
|
}
|
||||||
|
|
||||||
// 处理生成应用证书
|
// 处理生成应用证书
|
||||||
const handleGenerateAppCert = () => {
|
const handleGenerateAppCert = () => {
|
||||||
try {
|
try {
|
||||||
@ -347,6 +388,28 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 CRL 文件下载
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:无。
|
||||||
|
// 注意事项:证书 id 按证书序列号处理,多个 id 使用英文逗号分隔。
|
||||||
|
const handleDownloadCrlFile = () => {
|
||||||
|
try {
|
||||||
|
const crlFile = generateCrlFile(crlCertificateIds)
|
||||||
|
const blob = new Blob([crlFile], { type: 'application/pkix-crl' })
|
||||||
|
const downloadUrl = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = 'certificate-revocation-list.crl'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(downloadUrl)
|
||||||
|
setCertError('')
|
||||||
|
} catch (error) {
|
||||||
|
setCertError((error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理 PKCS7 签名信息提取
|
// 处理 PKCS7 签名信息提取
|
||||||
const handleGetInfoFromPKCS7 = () => {
|
const handleGetInfoFromPKCS7 = () => {
|
||||||
try {
|
try {
|
||||||
@ -1678,6 +1741,40 @@ function App() {
|
|||||||
minHeight: '150px'
|
minHeight: '150px'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', marginTop: '10px', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadRootCert}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: '#f59e0b',
|
||||||
|
color: '#121212',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下载根证书 CRT
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadRootPrivateKey}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: '#f59e0b',
|
||||||
|
color: '#121212',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下载根证书私钥
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1755,9 +1852,56 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 生成 CRL 文件 */}
|
||||||
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginBottom: '20px' }}>
|
||||||
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>3. 生成 CRL 文件</h3>
|
||||||
|
<div className="form-group" style={{ marginBottom: '15px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>证书 id:</label>
|
||||||
|
<textarea
|
||||||
|
value={crlCertificateIds}
|
||||||
|
onChange={(e) => setCrlCertificateIds(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, crlCertificateIds, setCrlCertificateIds, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, crlCertificateIds, setCrlCertificateIds, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
|
placeholder="可选,输入多个逗号分隔的证书 id,例如:01,02,0a"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: '#3d3d3d',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: '14px',
|
||||||
|
resize: 'vertical',
|
||||||
|
minHeight: '80px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadCrlFile}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: '#f59e0b',
|
||||||
|
color: '#121212',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下载 CRL 文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 证书解析 */}
|
{/* 证书解析 */}
|
||||||
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px' }}>
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px' }}>
|
||||||
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>3. 证书解析</h3>
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>4. 证书解析</h3>
|
||||||
<div className="form-group" style={{ marginBottom: '20px' }}>
|
<div className="form-group" style={{ marginBottom: '20px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
|
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
|
||||||
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
||||||
@ -1856,7 +2000,7 @@ function App() {
|
|||||||
|
|
||||||
{/* PKCS7 签名工具 */}
|
{/* PKCS7 签名工具 */}
|
||||||
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
|
||||||
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>4. PKCS7 签名工具</h3>
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>5. PKCS7 签名工具</h3>
|
||||||
|
|
||||||
<div className="form-group" style={{ marginBottom: '15px' }}>
|
<div className="form-group" style={{ marginBottom: '15px' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>PKCS7 签名数据</label>
|
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>PKCS7 签名数据</label>
|
||||||
|
|||||||
42
src/__tests__/algorithm/cert.test.ts
Normal file
42
src/__tests__/algorithm/cert.test.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as forge from 'node-forge'
|
||||||
|
import { generateCrlFile, generateRootCert, loadRootPrivateKeyFromCache, parseCrlCertificateIds } from '../../utils/algorithm/cert'
|
||||||
|
|
||||||
|
describe('证书 CRL 工具测试', () => {
|
||||||
|
test('1-1 空证书 id 应生成无吊销条目的 CRL 文件', () => {
|
||||||
|
const crlFile = generateCrlFile('')
|
||||||
|
const crlAsn1 = forge.asn1.fromDer(forge.util.createBuffer(crlFile))
|
||||||
|
const crlItems = crlAsn1.value as forge.asn1.Asn1[]
|
||||||
|
const tbsCertListItems = crlItems[0].value as forge.asn1.Asn1[]
|
||||||
|
|
||||||
|
expect(crlItems.length).toBe(3)
|
||||||
|
expect(tbsCertListItems.length).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('1-2 多个逗号分隔证书 id 应生成多个吊销条目', () => {
|
||||||
|
const crlFile = generateCrlFile('01, 02, 0a')
|
||||||
|
const crlAsn1 = forge.asn1.fromDer(forge.util.createBuffer(crlFile))
|
||||||
|
const crlItems = crlAsn1.value as forge.asn1.Asn1[]
|
||||||
|
const tbsCertListItems = crlItems[0].value as forge.asn1.Asn1[]
|
||||||
|
const revokedCertificates = tbsCertListItems[5].value as forge.asn1.Asn1[]
|
||||||
|
|
||||||
|
expect(revokedCertificates.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('1-3 重复证书 id 应去重', () => {
|
||||||
|
const certificateIds = parseCrlCertificateIds('01,1,02,02')
|
||||||
|
|
||||||
|
expect(certificateIds).toEqual(['01', '02'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('1-4 非十六进制证书 id 应抛出错误', () => {
|
||||||
|
expect(() => parseCrlCertificateIds('01, zz')).toThrow('证书 id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('1-5 生成根证书后应能读取根证书私钥', () => {
|
||||||
|
const rootCert = generateRootCert()
|
||||||
|
const rootPrivateKey = loadRootPrivateKeyFromCache()
|
||||||
|
|
||||||
|
expect(rootCert).toContain('BEGIN CERTIFICATE')
|
||||||
|
expect(rootPrivateKey).toContain('BEGIN RSA PRIVATE KEY')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,4 +1,15 @@
|
|||||||
import forge from 'node-forge'
|
import * as forge from 'node-forge'
|
||||||
|
|
||||||
|
const ROOT_CERT_CACHE_KEY = 'rootCert'
|
||||||
|
const ROOT_PRIVATE_KEY_CACHE_KEY = 'rootPrivateKey'
|
||||||
|
const SHA256_WITH_RSA_OID = forge.pki.oids.sha256WithRSAEncryption
|
||||||
|
|
||||||
|
type RootCertificateBundle = {
|
||||||
|
certPem: string
|
||||||
|
privateKeyPem: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let temporaryCrlSigner: RootCertificateBundle | null = null
|
||||||
|
|
||||||
// 将ArrayBuffer转换为Base64字符串(浏览器兼容)
|
// 将ArrayBuffer转换为Base64字符串(浏览器兼容)
|
||||||
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
||||||
@ -28,11 +39,15 @@ const getCacheDirectory = (): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存根证书到本地缓存
|
// 保存根证书和私钥到本地缓存
|
||||||
const saveRootCertToCache = (certPem: string): void => {
|
// 参数:certPem 为 PEM 格式根证书,privateKeyPem 为 PEM 格式根证书私钥。
|
||||||
|
// 返回值:无。
|
||||||
|
// 注意事项:浏览器环境保存到 localStorage,Node.js 环境保存到 cache 目录。
|
||||||
|
const saveRootCertToCache = (certPem: string, privateKeyPem: string): void => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// 浏览器环境:使用localStorage
|
// 浏览器环境:使用localStorage
|
||||||
localStorage.setItem('rootCert', certPem)
|
localStorage.setItem(ROOT_CERT_CACHE_KEY, certPem)
|
||||||
|
localStorage.setItem(ROOT_PRIVATE_KEY_CACHE_KEY, privateKeyPem)
|
||||||
} else {
|
} else {
|
||||||
// Node.js环境:使用文件系统
|
// Node.js环境:使用文件系统
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@ -46,15 +61,19 @@ const saveRootCertToCache = (certPem: string): void => {
|
|||||||
|
|
||||||
// 保存证书到文件
|
// 保存证书到文件
|
||||||
const certPath = path.join(cacheDir, 'rootCert.pem')
|
const certPath = path.join(cacheDir, 'rootCert.pem')
|
||||||
|
const privateKeyPath = path.join(cacheDir, 'rootPrivateKey.pem')
|
||||||
fs.writeFileSync(certPath, certPem)
|
fs.writeFileSync(certPath, certPem)
|
||||||
|
fs.writeFileSync(privateKeyPath, privateKeyPem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从本地缓存读取根证书
|
// 从本地缓存读取根证书
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:存在时返回 PEM 格式根证书,否则返回 null。
|
||||||
const loadRootCertFromCache = (): string | null => {
|
const loadRootCertFromCache = (): string | null => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// 浏览器环境:从localStorage读取
|
// 浏览器环境:从localStorage读取
|
||||||
return localStorage.getItem('rootCert')
|
return localStorage.getItem(ROOT_CERT_CACHE_KEY)
|
||||||
} else {
|
} else {
|
||||||
// Node.js环境:从文件系统读取
|
// Node.js环境:从文件系统读取
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@ -69,68 +88,100 @@ const loadRootCertFromCache = (): string | null => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从本地缓存读取根证书私钥
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:存在时返回 PEM 格式根证书私钥,否则返回 null。
|
||||||
|
export const loadRootPrivateKeyFromCache = (): string | null => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 浏览器环境:从localStorage读取
|
||||||
|
return localStorage.getItem(ROOT_PRIVATE_KEY_CACHE_KEY)
|
||||||
|
} else {
|
||||||
|
// Node.js环境:从文件系统读取
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const cacheDir = getCacheDirectory()
|
||||||
|
const privateKeyPath = path.join(cacheDir, 'rootPrivateKey.pem')
|
||||||
|
|
||||||
|
if (fs.existsSync(privateKeyPath)) {
|
||||||
|
return fs.readFileSync(privateKeyPath, 'utf8')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建根证书和根证书私钥
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:返回 PEM 格式根证书和 PEM 格式私钥。
|
||||||
|
const createRootCertificateBundle = (): RootCertificateBundle => {
|
||||||
|
// 生成密钥对
|
||||||
|
const keys = forge.pki.rsa.generateKeyPair(2048)
|
||||||
|
|
||||||
|
// 创建证书
|
||||||
|
const cert = forge.pki.createCertificate()
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
cert.version = 3
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
cert.serialNumber = generateSerialNumber()
|
||||||
|
|
||||||
|
// 设置主题和颁发者(根证书自己颁发自己)
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'commonName', value: 'Root CA' },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Root Certificate Authority' }
|
||||||
|
]
|
||||||
|
|
||||||
|
cert.setSubject(attrs)
|
||||||
|
cert.setIssuer(attrs)
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
cert.publicKey = keys.publicKey
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date()
|
||||||
|
const notAfter = new Date()
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 10) // 10年有效期
|
||||||
|
cert.validity.notBefore = notBefore
|
||||||
|
cert.validity.notAfter = notAfter
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
cert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyCertSign: true,
|
||||||
|
cRLSign: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 使用私钥自签名
|
||||||
|
cert.sign(keys.privateKey, forge.md.sha256.create())
|
||||||
|
|
||||||
|
return {
|
||||||
|
certPem: forge.pki.certificateToPem(cert),
|
||||||
|
privateKeyPem: forge.pki.privateKeyToPem(keys.privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成根证书
|
// 生成根证书
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:返回 PEM 格式根证书,并将根证书和私钥写入缓存。
|
||||||
export const generateRootCert = (): string => {
|
export const generateRootCert = (): string => {
|
||||||
try {
|
try {
|
||||||
// 生成密钥对
|
const rootCertificateBundle = createRootCertificateBundle()
|
||||||
const keys = forge.pki.rsa.generateKeyPair(2048)
|
|
||||||
|
|
||||||
// 创建证书
|
|
||||||
const cert = forge.pki.createCertificate()
|
|
||||||
|
|
||||||
// 设置版本
|
|
||||||
cert.version = 3
|
|
||||||
|
|
||||||
// 设置序列号
|
|
||||||
cert.serialNumber = generateSerialNumber()
|
|
||||||
|
|
||||||
// 设置主题和颁发者(根证书自己颁发自己)
|
|
||||||
const attrs = [
|
|
||||||
{ name: 'commonName', value: 'Root CA' },
|
|
||||||
{ name: 'countryName', value: 'CN' },
|
|
||||||
{ shortName: 'ST', value: 'Beijing' },
|
|
||||||
{ name: 'localityName', value: 'Beijing' },
|
|
||||||
{ name: 'organizationName', value: 'Security Kit' },
|
|
||||||
{ shortName: 'OU', value: 'Root Certificate Authority' }
|
|
||||||
]
|
|
||||||
|
|
||||||
cert.setSubject(attrs)
|
|
||||||
cert.setIssuer(attrs)
|
|
||||||
|
|
||||||
// 设置公钥
|
|
||||||
cert.publicKey = keys.publicKey
|
|
||||||
|
|
||||||
// 设置有效期
|
|
||||||
const notBefore = new Date()
|
|
||||||
const notAfter = new Date()
|
|
||||||
notAfter.setFullYear(notBefore.getFullYear() + 10) // 10年有效期
|
|
||||||
cert.validity.notBefore = notBefore
|
|
||||||
cert.validity.notAfter = notAfter
|
|
||||||
|
|
||||||
// 添加扩展
|
|
||||||
cert.setExtensions([
|
|
||||||
{
|
|
||||||
name: 'basicConstraints',
|
|
||||||
cA: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'keyUsage',
|
|
||||||
digitalSignature: true,
|
|
||||||
keyCertSign: true,
|
|
||||||
cRLSign: true
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 使用私钥自签名
|
|
||||||
cert.sign(keys.privateKey, forge.md.sha256.create())
|
|
||||||
|
|
||||||
// 转换为PEM格式
|
|
||||||
const certPem = forge.pki.certificateToPem(cert)
|
|
||||||
|
|
||||||
// 保存到本地缓存
|
// 保存到本地缓存
|
||||||
saveRootCertToCache(certPem)
|
saveRootCertToCache(rootCertificateBundle.certPem, rootCertificateBundle.privateKeyPem)
|
||||||
|
|
||||||
return certPem
|
return rootCertificateBundle.certPem
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error))
|
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error))
|
||||||
}
|
}
|
||||||
@ -144,6 +195,10 @@ export const generateAppCert = (commonName: string): string => {
|
|||||||
if (!rootCertPem) {
|
if (!rootCertPem) {
|
||||||
throw new Error('Root certificate not found in cache. Please generate root certificate first.')
|
throw new Error('Root certificate not found in cache. Please generate root certificate first.')
|
||||||
}
|
}
|
||||||
|
const rootPrivateKeyPem = loadRootPrivateKeyFromCache()
|
||||||
|
if (!rootPrivateKeyPem) {
|
||||||
|
throw new Error('Root private key not found in cache. Please regenerate root certificate first.')
|
||||||
|
}
|
||||||
|
|
||||||
// 解析根证书
|
// 解析根证书
|
||||||
const rootCert = forge.pki.certificateFromPem(rootCertPem)
|
const rootCert = forge.pki.certificateFromPem(rootCertPem)
|
||||||
@ -203,10 +258,8 @@ export const generateAppCert = (commonName: string): string => {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
// 使用根证书的私钥签名(注意:这里需要根证书的私钥,实际应用中需要安全存储)
|
// 使用根证书的私钥签名
|
||||||
// 由于我们没有保存根证书的私钥,这里使用模拟的私钥签名
|
const rootPrivateKey = forge.pki.privateKeyFromPem(rootPrivateKeyPem)
|
||||||
// 实际应用中,应该在生成根证书时同时保存私钥
|
|
||||||
const rootPrivateKey = forge.pki.privateKeyFromPem(rootCertPem)
|
|
||||||
appCert.sign(rootPrivateKey, forge.md.sha256.create())
|
appCert.sign(rootPrivateKey, forge.md.sha256.create())
|
||||||
|
|
||||||
// 转换为PEM格式
|
// 转换为PEM格式
|
||||||
@ -218,6 +271,141 @@ export const generateAppCert = (commonName: string): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将证书 id 规范化为 ASN.1 INTEGER 使用的十六进制序列号
|
||||||
|
// 参数:certificateId 为用户输入的单个证书 id。
|
||||||
|
// 返回值:返回偶数长度、正数编码的十六进制序列号。
|
||||||
|
// 注意事项:证书 id 按证书序列号处理,仅支持十六进制字符。
|
||||||
|
const normalizeCrlCertificateId = (certificateId: string): string => {
|
||||||
|
const cleanedCertificateId = certificateId.trim().replace(/^0x/i, '')
|
||||||
|
if (!/^[0-9a-fA-F]+$/.test(cleanedCertificateId)) {
|
||||||
|
throw new Error('证书 id 只能包含 0-9、a-f、A-F,多个 id 请使用逗号分隔')
|
||||||
|
}
|
||||||
|
let serialNumberHex = cleanedCertificateId.replace(/^0+/, '') || '0'
|
||||||
|
if (serialNumberHex.length % 2 === 1) {
|
||||||
|
serialNumberHex = '0' + serialNumberHex
|
||||||
|
}
|
||||||
|
if (parseInt(serialNumberHex.slice(0, 2), 16) >= 0x80) {
|
||||||
|
serialNumberHex = '00' + serialNumberHex
|
||||||
|
}
|
||||||
|
return serialNumberHex.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析逗号分隔的证书 id
|
||||||
|
// 参数:certificateIds 为用户输入的逗号分隔证书 id 字符串。
|
||||||
|
// 返回值:返回去重后的十六进制证书序列号数组。
|
||||||
|
export const parseCrlCertificateIds = (certificateIds: string): string[] => {
|
||||||
|
const serialNumbers = certificateIds
|
||||||
|
.split(',')
|
||||||
|
.map(certificateId => certificateId.trim())
|
||||||
|
.filter(certificateId => certificateId.length > 0)
|
||||||
|
.map(normalizeCrlCertificateId)
|
||||||
|
return Array.from(new Set(serialNumbers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将日期转换为 CRL 使用的 ASN.1 Time 对象
|
||||||
|
// 参数:date 为需要写入 CRL 的日期。
|
||||||
|
// 返回值:返回 UTCTime 或 GeneralizedTime ASN.1 对象。
|
||||||
|
const dateToCrlTimeAsn1 = (date: Date): forge.asn1.Asn1 => {
|
||||||
|
const utcTimeStart = new Date('1950-01-01T00:00:00Z')
|
||||||
|
const utcTimeEnd = new Date('2050-01-01T00:00:00Z')
|
||||||
|
if (date >= utcTimeStart && date < utcTimeEnd) {
|
||||||
|
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.UTCTIME, false, forge.asn1.dateToUtcTime(date))
|
||||||
|
}
|
||||||
|
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.GENERALIZEDTIME, false, forge.asn1.dateToGeneralizedTime(date))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 SHA256withRSA 算法标识
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:返回 CRL 签名算法使用的 ASN.1 AlgorithmIdentifier。
|
||||||
|
const createSha256WithRsaAlgorithmIdentifier = (): forge.asn1.Asn1 => {
|
||||||
|
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
|
||||||
|
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer(SHA256_WITH_RSA_OID).getBytes()),
|
||||||
|
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.NULL, false, '')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 CRL 的 TBSCertList 结构
|
||||||
|
// 参数:issuerCert 为签发者证书,serialNumbers 为吊销证书序列号数组,thisUpdate 为本次更新时间,nextUpdate 为下次更新时间。
|
||||||
|
// 返回值:返回用于签名的 TBSCertList ASN.1 对象。
|
||||||
|
const createTbsCertList = (issuerCert: forge.pki.Certificate, serialNumbers: string[], thisUpdate: Date, nextUpdate: Date): forge.asn1.Asn1 => {
|
||||||
|
const tbsCertListItems: forge.asn1.Asn1[] = [
|
||||||
|
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, forge.asn1.integerToDer(1).getBytes()),
|
||||||
|
createSha256WithRsaAlgorithmIdentifier(),
|
||||||
|
forge.pki.distinguishedNameToAsn1(issuerCert.subject),
|
||||||
|
dateToCrlTimeAsn1(thisUpdate),
|
||||||
|
dateToCrlTimeAsn1(nextUpdate)
|
||||||
|
]
|
||||||
|
|
||||||
|
if (serialNumbers.length > 0) {
|
||||||
|
tbsCertListItems.push(forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, serialNumbers.map(serialNumber => {
|
||||||
|
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
|
||||||
|
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, forge.util.hexToBytes(serialNumber)),
|
||||||
|
dateToCrlTimeAsn1(thisUpdate)
|
||||||
|
])
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
return forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, tbsCertListItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将二进制字符串转换为 ArrayBuffer
|
||||||
|
// 参数:binary 为 node-forge 输出的二进制字符串。
|
||||||
|
// 返回值:返回可用于 Blob 下载的字节数组。
|
||||||
|
const binaryStringToArrayBuffer = (binary: string): ArrayBuffer => {
|
||||||
|
const buffer = new ArrayBuffer(binary.length)
|
||||||
|
const bytes = new Uint8Array(buffer)
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i)
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 CRL 签发者证书和私钥
|
||||||
|
// 参数:无。
|
||||||
|
// 返回值:优先返回缓存根证书和私钥;不存在时返回临时签发者证书和私钥。
|
||||||
|
const getCrlSigner = (): RootCertificateBundle => {
|
||||||
|
const rootCertPem = loadRootCertFromCache()
|
||||||
|
const rootPrivateKeyPem = loadRootPrivateKeyFromCache()
|
||||||
|
if (rootCertPem && rootPrivateKeyPem) {
|
||||||
|
return {
|
||||||
|
certPem: rootCertPem,
|
||||||
|
privateKeyPem: rootPrivateKeyPem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!temporaryCrlSigner) {
|
||||||
|
temporaryCrlSigner = createRootCertificateBundle()
|
||||||
|
}
|
||||||
|
return temporaryCrlSigner
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 DER 格式 CRL 文件内容
|
||||||
|
// 参数:certificateIds 为可选的逗号分隔证书 id,按证书序列号写入吊销列表。
|
||||||
|
// 返回值:返回 DER 格式 CRL 文件的字节数组。
|
||||||
|
export const generateCrlFile = (certificateIds: string): ArrayBuffer => {
|
||||||
|
try {
|
||||||
|
const serialNumbers = parseCrlCertificateIds(certificateIds)
|
||||||
|
const crlSigner = getCrlSigner()
|
||||||
|
const issuerCert = forge.pki.certificateFromPem(crlSigner.certPem)
|
||||||
|
const issuerPrivateKey = forge.pki.privateKeyFromPem(crlSigner.privateKeyPem)
|
||||||
|
const thisUpdate = new Date()
|
||||||
|
const nextUpdate = new Date()
|
||||||
|
nextUpdate.setDate(thisUpdate.getDate() + 30)
|
||||||
|
const tbsCertList = createTbsCertList(issuerCert, serialNumbers, thisUpdate, nextUpdate)
|
||||||
|
const tbsCertListDer = forge.asn1.toDer(tbsCertList).getBytes()
|
||||||
|
const md = forge.md.sha256.create()
|
||||||
|
md.update(tbsCertListDer)
|
||||||
|
const signature = issuerPrivateKey.sign(md)
|
||||||
|
const crlAsn1 = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [
|
||||||
|
tbsCertList,
|
||||||
|
createSha256WithRsaAlgorithmIdentifier(),
|
||||||
|
forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.BITSTRING, false, String.fromCharCode(0) + signature)
|
||||||
|
])
|
||||||
|
return binaryStringToArrayBuffer(forge.asn1.toDer(crlAsn1).getBytes())
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to generate CRL file: ' + (error instanceof Error ? error.message : error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析SM2证书信息
|
// 解析SM2证书信息
|
||||||
export const parseSm2Cert = (certData: string): any => {
|
export const parseSm2Cert = (certData: string): any => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user