go
复制代码
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"github.com/gin-gonic/gin"
"io"
"math/big"
"mime/multipart"
"net/http"
"time"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
api := r.Group("/api")
{
api.POST("/generate-ca", generateCA)
api.POST("/sign-cert", signCert)
api.POST("/encrypt", encrypt)
api.POST("/decrypt", decrypt)
api.POST("/sign", sign)
api.POST("/verify", verify)
api.POST("/generate-keypair", generateKeypair)
}
r.Run(":8080")
}
// 生成 CA 证书
func generateCA(c *gin.Context) {
var req struct {
CommonName string `form:"common_name" binding:"required"`
Days int `form:"days" binding:"required,min=1"`
Country string `form:"country"`
Province string `form:"province"`
Locality string `form:"locality"`
Organization string `form:"organization"`
OrganizationalUnit string `form:"organizational_unit"`
KeyBits int `form:"key_bits" binding:"required,oneof=2048 4096"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成 RSA 密钥对
privateKey, err := rsa.GenerateKey(rand.Reader, req.KeyBits)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成私钥失败"})
return
}
// 构造证书模板
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: req.CommonName,
Country: []string{req.Country},
Province: []string{req.Province},
Locality: []string{req.Locality},
Organization: []string{req.Organization},
OrganizationalUnit: []string{req.OrganizationalUnit},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, req.Days),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
// 自签名
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建证书失败"})
return
}
// 编码为 PEM
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
// 提取公钥
pubASN1, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "编码公钥失败"})
return
}
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
c.JSON(http.StatusOK, gin.H{
"ca_cert": string(certPEM),
"ca_private_key": string(privPEM),
"ca_public_key": string(pubPEM), // 新增公钥
})
}
// 签发终端证书
func signCert(c *gin.Context) {
// 获取上传的 CA 证书和私钥
caCertFile, err := c.FormFile("ca_cert")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 CA 证书文件"})
return
}
caKeyFile, err := c.FormFile("ca_private_key")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 CA 私钥文件"})
return
}
// 读取文件内容
caCertBytes, err := readFileBytes(caCertFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取 CA 证书失败"})
return
}
caKeyBytes, err := readFileBytes(caKeyFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取 CA 私钥失败"})
return
}
// 解析 CA 证书和私钥
caCertBlock, _ := pem.Decode(caCertBytes)
if caCertBlock == nil || caCertBlock.Type != "CERTIFICATE" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 CA 证书 PEM"})
return
}
caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析 CA 证书失败"})
return
}
caKeyBlock, _ := pem.Decode(caKeyBytes)
if caKeyBlock == nil || (caKeyBlock.Type != "RSA PRIVATE KEY" && caKeyBlock.Type != "PRIVATE KEY") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 CA 私钥 PEM"})
return
}
var caPrivateKey interface{}
if caKeyBlock.Type == "RSA PRIVATE KEY" {
caPrivateKey, err = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
} else {
caPrivateKey, err = x509.ParsePKCS8PrivateKey(caKeyBlock.Bytes)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析 CA 私钥失败"})
return
}
caRsaPrivateKey, ok := caPrivateKey.(*rsa.PrivateKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "CA 私钥不是 RSA 密钥"})
return
}
// 获取证书参数
commonName := c.PostForm("common_name")
days := c.PostForm("days")
country := c.PostForm("country")
province := c.PostForm("province")
locality := c.PostForm("locality")
organization := c.PostForm("organization")
organizationalUnit := c.PostForm("organizational_unit")
keyBits := c.PostForm("key_bits")
if commonName == "" || days == "" || keyBits == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必要参数 (common_name, days, key_bits)"})
return
}
daysInt := atoi(days, 365)
keyBitsInt := atoi(keyBits, 2048)
// 生成新证书的密钥对
certPrivateKey, err := rsa.GenerateKey(rand.Reader, keyBitsInt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成证书私钥失败"})
return
}
// 证书模板
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: commonName,
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
Organization: []string{organization},
OrganizationalUnit: []string{organizationalUnit},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, daysInt),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: false,
}
// 使用 CA 签发新证书
certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, &certPrivateKey.PublicKey, caRsaPrivateKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "签发证书失败"})
return
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(certPrivateKey)})
// 提取公钥
pubASN1, err := x509.MarshalPKIXPublicKey(&certPrivateKey.PublicKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "编码公钥失败"})
return
}
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
c.JSON(http.StatusOK, gin.H{
"sub_cert": string(certPEM),
"sub_private_key": string(privPEM),
"sub_public_key": string(pubPEM), // 新增公钥
})
}
// 加密
func encrypt(c *gin.Context) {
pubKeyFile, err := c.FormFile("public_key")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少公钥文件"})
return
}
message := c.PostForm("message")
if message == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少消息"})
return
}
pubKeyBytes, err := readFileBytes(pubKeyFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取公钥失败"})
return
}
pubKeyBlock, _ := pem.Decode(pubKeyBytes)
if pubKeyBlock == nil || (pubKeyBlock.Type != "PUBLIC KEY" && pubKeyBlock.Type != "RSA PUBLIC KEY") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的公钥 PEM"})
return
}
var pubKey interface{}
if pubKeyBlock.Type == "RSA PUBLIC KEY" {
pubKey, err = x509.ParsePKCS1PublicKey(pubKeyBlock.Bytes)
} else {
pubKey, err = x509.ParsePKIXPublicKey(pubKeyBlock.Bytes)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析公钥失败"})
return
}
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "公钥不是 RSA 公钥"})
return
}
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPubKey, []byte(message), nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加密失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"encrypted": base64.StdEncoding.EncodeToString(ciphertext),
})
}
// 解密
func decrypt(c *gin.Context) {
privKeyFile, err := c.FormFile("private_key")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少私钥文件"})
return
}
encryptedB64 := c.PostForm("encrypted")
if encryptedB64 == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少加密数据"})
return
}
ciphertext, err := base64.StdEncoding.DecodeString(encryptedB64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 base64 编码"})
return
}
privKeyBytes, err := readFileBytes(privKeyFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取私钥失败"})
return
}
privKeyBlock, _ := pem.Decode(privKeyBytes)
if privKeyBlock == nil || (privKeyBlock.Type != "RSA PRIVATE KEY" && privKeyBlock.Type != "PRIVATE KEY") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的私钥 PEM"})
return
}
var privKey interface{}
if privKeyBlock.Type == "RSA PRIVATE KEY" {
privKey, err = x509.ParsePKCS1PrivateKey(privKeyBlock.Bytes)
} else {
privKey, err = x509.ParsePKCS8PrivateKey(privKeyBlock.Bytes)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析私钥失败"})
return
}
rsaPrivKey, ok := privKey.(*rsa.PrivateKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "私钥不是 RSA 私钥"})
return
}
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, rsaPrivKey, ciphertext, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "解密失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": string(plaintext),
})
}
// 签名
func sign(c *gin.Context) {
privKeyFile, err := c.FormFile("private_key")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少私钥文件"})
return
}
message := c.PostForm("message")
if message == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少消息"})
return
}
privKeyBytes, err := readFileBytes(privKeyFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取私钥失败"})
return
}
privKeyBlock, _ := pem.Decode(privKeyBytes)
if privKeyBlock == nil || (privKeyBlock.Type != "RSA PRIVATE KEY" && privKeyBlock.Type != "PRIVATE KEY") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的私钥 PEM"})
return
}
var privKey interface{}
if privKeyBlock.Type == "RSA PRIVATE KEY" {
privKey, err = x509.ParsePKCS1PrivateKey(privKeyBlock.Bytes)
} else {
privKey, err = x509.ParsePKCS8PrivateKey(privKeyBlock.Bytes)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析私钥失败"})
return
}
rsaPrivKey, ok := privKey.(*rsa.PrivateKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "私钥不是 RSA 私钥"})
return
}
hashed := sha256.Sum256([]byte(message))
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivKey, crypto.SHA256, hashed[:])
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "签名失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"signature": base64.StdEncoding.EncodeToString(signature),
})
}
// 验签
func verify(c *gin.Context) {
pubKeyFile, err := c.FormFile("public_key")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少公钥文件"})
return
}
message := c.PostForm("message")
signatureB64 := c.PostForm("signature")
if message == "" || signatureB64 == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少消息或签名"})
return
}
signature, err := base64.StdEncoding.DecodeString(signatureB64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 base64 签名"})
return
}
pubKeyBytes, err := readFileBytes(pubKeyFile)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取公钥失败"})
return
}
pubKeyBlock, _ := pem.Decode(pubKeyBytes)
if pubKeyBlock == nil || (pubKeyBlock.Type != "PUBLIC KEY" && pubKeyBlock.Type != "RSA PUBLIC KEY") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的公钥 PEM"})
return
}
var pubKey interface{}
if pubKeyBlock.Type == "RSA PUBLIC KEY" {
pubKey, err = x509.ParsePKCS1PublicKey(pubKeyBlock.Bytes)
} else {
pubKey, err = x509.ParsePKIXPublicKey(pubKeyBlock.Bytes)
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "解析公钥失败"})
return
}
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "公钥不是 RSA 公钥"})
return
}
hashed := sha256.Sum256([]byte(message))
err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, hashed[:], signature)
valid := err == nil
c.JSON(http.StatusOK, gin.H{
"valid": valid,
})
}
// 生成 RSA 密钥对
func generateKeypair(c *gin.Context) {
var req struct {
KeyBits int `form:"key_bits" binding:"required,oneof=2048 4096"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
privateKey, err := rsa.GenerateKey(rand.Reader, req.KeyBits)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成私钥失败"})
return
}
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
pubASN1, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "编码公钥失败"})
return
}
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
c.JSON(http.StatusOK, gin.H{
"private_key": string(privPEM),
"public_key": string(pubPEM),
})
}
// 辅助函数:从上传的文件头读取所有字节
func readFileBytes(fileHeader *multipart.FileHeader) ([]byte, error) {
file, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer file.Close()
return io.ReadAll(file)
}
// 简单字符串转整数
func atoi(s string, defaultValue int) int {
var v int
fmt.Sscanf(s, "%d", &v)
if v == 0 {
return defaultValue
}
return v
}
html
复制代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CA 工具包</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.tabs { overflow: hidden; border-bottom: 1px solid #ccc; }
.tab-button { background-color: #f1f1f1; float: left; border: none; outline: none; cursor: pointer; padding: 10px 20px; transition: 0.3s; }
.tab-button:hover { background-color: #ddd; }
.tab-button.active { background-color: #ccc; }
.tab-content { display: none; padding: 20px; border: 1px solid #ccc; border-top: none; }
.tab-content.active { display: block; }
label { display: inline-block; width: 150px; margin: 5px 0; }
input[type=text], input[type=number], textarea { width: 300px; padding: 5px; }
textarea { height: 100px; font-family: monospace; }
button { margin: 10px 0; padding: 8px 15px; }
.result { margin-top: 15px; padding: 10px; background: #f5f5f5; border-radius: 5px; }
.error { color: red; }
.section { border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; }
.download-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
margin: 2px 5px 2px 0;
cursor: pointer;
border-radius: 3px;
}
.download-btn:hover { background-color: #45a049; }
pre { background: #fff; padding: 10px; border: 1px solid #ddd; overflow: auto; }
</style>
</head>
<body>
<h1>CA 工具包</h1>
<div class="tabs">
<button class="tab-button active" onclick="openTab(event, 'ca')">生成 CA</button>
<button class="tab-button" onclick="openTab(event, 'cert')">签发证书</button>
<button class="tab-button" onclick="openTab(event, 'crypto')">加密/解密</button>
<button class="tab-button" onclick="openTab(event, 'sign')">签名/验签</button>
<button class="tab-button" onclick="openTab(event, 'keygen')">生成密钥对</button>
</div>
<!-- 生成 CA -->
<div id="ca" class="tab-content active">
<h2>生成自签名 CA 证书</h2>
<form id="caForm">
<label>通用名称 (CN):</label> <input type="text" name="common_name" required><br>
<label>有效期 (天):</label> <input type="number" name="days" value="365" min="1" required><br>
<label>国家 (C):</label> <input type="text" name="country" value="CN"><br>
<label>省份 (ST):</label> <input type="text" name="province" value="Beijing"><br>
<label>城市 (L):</label> <input type="text" name="locality" value="Beijing"><br>
<label>组织 (O):</label> <input type="text" name="organization" value="MyOrg"><br>
<label>组织单位 (OU):</label> <input type="text" name="organizational_unit" value="IT"><br>
<label>密钥位数:</label>
<select name="key_bits">
<option value="2048">2048</option>
<option value="4096">4096</option>
</select><br>
<button type="button" onclick="submitCA()">生成 CA</button>
</form>
<div id="caResult" class="result"></div>
</div>
<!-- 签发证书 -->
<div id="cert" class="tab-content">
<h2>使用 CA 签发终端证书</h2>
<form id="certForm" enctype="multipart/form-data">
<label>CA 证书 (PEM):</label> <input type="file" name="ca_cert" accept=".crt,.pem" required><br>
<label>CA 私钥 (PEM):</label> <input type="file" name="ca_private_key" accept=".key,.pem" required><br>
<label>通用名称 (CN):</label> <input type="text" name="common_name" required><br>
<label>有效期 (天):</label> <input type="number" name="days" value="365" min="1" required><br>
<label>国家 (C):</label> <input type="text" name="country"><br>
<label>省份 (ST):</label> <input type="text" name="province"><br>
<label>城市 (L):</label> <input type="text" name="locality"><br>
<label>组织 (O):</label> <input type="text" name="organization"><br>
<label>组织单位 (OU):</label> <input type="text" name="organizational_unit"><br>
<label>密钥位数:</label>
<select name="key_bits">
<option value="2048">2048</option>
<option value="4096">4096</option>
</select><br>
<button type="button" onclick="submitCert()">签发证书</button>
</form>
<div id="certResult" class="result"></div>
</div>
<!-- 加密/解密 -->
<div id="crypto" class="tab-content">
<div class="section">
<h3>加密</h3>
<form id="encryptForm" enctype="multipart/form-data">
<label>公钥 (PEM):</label> <input type="file" name="public_key" accept=".pem,.pub" required><br>
<label>消息:</label> <input type="text" name="message" size="50" required><br>
<button type="button" onclick="submitEncrypt()">加密</button>
</form>
<div id="encryptResult" class="result"></div>
</div>
<div class="section">
<h3>解密</h3>
<form id="decryptForm" enctype="multipart/form-data">
<label>私钥 (PEM):</label> <input type="file" name="private_key" accept=".key,.pem" required><br>
<label>加密数据 (base64):</label> <textarea name="encrypted" required></textarea><br>
<button type="button" onclick="submitDecrypt()">解密</button>
</form>
<div id="decryptResult" class="result"></div>
</div>
</div>
<!-- 签名/验签 -->
<div id="sign" class="tab-content">
<div class="section">
<h3>签名</h3>
<form id="signForm" enctype="multipart/form-data">
<label>私钥 (PEM):</label> <input type="file" name="private_key" accept=".key,.pem" required><br>
<label>消息:</label> <input type="text" name="message" size="50" required><br>
<button type="button" onclick="submitSign()">签名</button>
</form>
<div id="signResult" class="result"></div>
</div>
<div class="section">
<h3>验签</h3>
<form id="verifyForm" enctype="multipart/form-data">
<label>公钥 (PEM):</label> <input type="file" name="public_key" accept=".pem,.pub" required><br>
<label>消息:</label> <input type="text" name="message" size="50" required><br>
<label>签名 (base64):</label> <textarea name="signature" required></textarea><br>
<button type="button" onclick="submitVerify()">验签</button>
</form>
<div id="verifyResult" class="result"></div>
</div>
</div>
<!-- 生成密钥对 -->
<div id="keygen" class="tab-content">
<h2>生成 RSA 密钥对</h2>
<form id="keygenForm">
<label>密钥位数:</label>
<select name="key_bits">
<option value="2048">2048</option>
<option value="4096">4096</option>
</select><br>
<button type="button" onclick="submitKeygen()">生成</button>
</form>
<div id="keygenResult" class="result"></div>
</div>
<script>
function openTab(evt, tabName) {
var i, tabcontent, tabbuttons;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove("active");
}
tabbuttons = document.getElementsByClassName("tab-button");
for (i = 0; i < tabbuttons.length; i++) {
tabbuttons[i].classList.remove("active");
}
document.getElementById(tabName).classList.add("active");
evt.currentTarget.classList.add("active");
}
async function postForm(formId, url, resultId) {
const form = document.getElementById(formId);
const formData = new FormData(form);
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
const resultDiv = document.getElementById(resultId);
// 清空之前的内容
resultDiv.innerHTML = '';
if (response.ok) {
// 显示 JSON 预览
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(data, null, 2);
resultDiv.appendChild(pre);
// 检测 PEM 字段并提供下载按钮
const downloadDiv = document.createElement('div');
downloadDiv.innerHTML = '<h4>下载 PEM 文件:</h4>';
let hasPem = false;
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string' && value.trim().startsWith('-----BEGIN')) {
hasPem = true;
const btn = document.createElement('button');
btn.className = 'download-btn';
btn.textContent = `下载 ${key}.pem`;
btn.onclick = function() {
const blob = new Blob([value], { type: 'application/x-pem-file' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = key + '.pem';
link.click();
URL.revokeObjectURL(link.href);
};
downloadDiv.appendChild(btn);
downloadDiv.appendChild(document.createElement('br'));
}
}
if (hasPem) {
resultDiv.appendChild(downloadDiv);
}
resultDiv.classList.remove('error');
} else {
resultDiv.innerHTML = '<span class="error">错误: ' + (data.error || '未知错误') + '</span>';
resultDiv.classList.add('error');
}
}
function submitCA() { postForm('caForm', '/api/generate-ca', 'caResult'); }
function submitCert() { postForm('certForm', '/api/sign-cert', 'certResult'); }
function submitEncrypt() { postForm('encryptForm', '/api/encrypt', 'encryptResult'); }
function submitDecrypt() { postForm('decryptForm', '/api/decrypt', 'decryptResult'); }
function submitSign() { postForm('signForm', '/api/sign', 'signResult'); }
function submitVerify() { postForm('verifyForm', '/api/verify', 'verifyResult'); }
function submitKeygen() { postForm('keygenForm', '/api/generate-keypair', 'keygenResult'); }
</script>
</body>
</html>