Files
anylink/server/dbdata/cert_client.go
wsczx 861b07a47d 支持为用户生成不同组的证书
证书管理前端增加搜索功能
2025-08-26 17:29:50 +08:00

473 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dbdata
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"slices"
"strconv"
"sync"
"time"
"github.com/bjdgyc/anylink/base"
"software.sslmate.com/src/go-pkcs12"
)
// 客户端证书数据结构
type ClientCertData struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Username string `json:"username" xorm:"varchar(60) not null"`
Groupname string `json:"groupname" xorm:"varchar(60) not null"`
Status int `json:"status" xorm:"int default 0"`
Certificate string `json:"certificate" xorm:"text not null"`
PrivateKey string `json:"private_key" xorm:"text not null"`
SerialNumber string `json:"serial_number" xorm:"varchar(100) not null"`
NotAfter time.Time `json:"not_after" xorm:"datetime not null"`
CreatedAt time.Time `json:"created_at" xorm:"datetime created"`
}
var (
clientCACert *x509.Certificate
clientCAKey *rsa.PrivateKey
caMutex sync.Mutex
)
// 证书状态
const (
CertStatusActive = 0 // 有效
CertStatusDisabled = 1 // 禁用
CertStatusExpired = 2 // 过期
)
// 获取证书状态描述
func (c *ClientCertData) GetStatusText() string {
switch c.GetStatus() {
case CertStatusActive:
return "有效"
case CertStatusDisabled:
return "禁用"
case CertStatusExpired:
return "过期"
default:
return "未知"
}
}
// 获取证书状态
func (c *ClientCertData) GetStatus() int {
return c.Status
}
// 保存客户端证书
func (c *ClientCertData) Save() error {
if c.Id > 0 {
return Set(c) // 更新现有记录
}
return Add(c)
}
// 禁用证书
func (c *ClientCertData) Disable() error {
return c.UpdateStatus(CertStatusDisabled)
}
// 启用证书
func (c *ClientCertData) Enable() error {
return c.UpdateStatus(CertStatusActive)
}
// 删除证书记录
func (c *ClientCertData) Delete() error {
return Del(c)
}
// 切换证书状态
func (c *ClientCertData) ChangeStatus() error {
switch c.Status {
case CertStatusActive:
return c.Disable()
case CertStatusDisabled:
return c.Enable()
}
return fmt.Errorf("证书已过期,无法切换状态")
}
// 更新客户端证书状态
func (c *ClientCertData) UpdateStatus(status int) error {
c.Status = status
if err := Set(c); err != nil {
return fmt.Errorf("更新客户端证书状态失败: %v", err)
}
return nil
}
// 检查并更新证书状态为过期
func (c *ClientCertData) CheckAndUpdateStatus() error {
if c.Status != CertStatusExpired && time.Now().After(c.NotAfter) {
if err := c.UpdateStatus(CertStatusExpired); err != nil {
return fmt.Errorf("更新证书状态为过期失败: %v", err)
}
base.Info("检测到证书过期,已更新状态:", c.Username)
}
return nil
}
// 获取客户端证书列表
func GetClientCertList(pageSize, pageIndex int, username, groupname, status string) ([]ClientCertData, int64, error) {
var certs []ClientCertData
session := GetXdb().NewSession()
defer session.Close()
session = session.Where("1=1")
// 添加搜索条件
if username != "" {
session.And("username LIKE ?", "%"+username+"%")
}
if groupname != "" {
session.And("groupname LIKE ?", "%"+groupname+"%")
}
if status != "" {
if statusInt, err := strconv.Atoi(status); err == nil {
session.And("status = ?", statusInt)
}
}
total, err := FindAndCount(session, &certs, pageSize, pageIndex)
if err != nil {
return nil, 0, fmt.Errorf("获取客户端证书列表失败: %v", err)
}
return certs, total, nil
}
// 获取客户端证书
func GetClientCert(username, groupname string) (*ClientCertData, error) {
clientCert := &ClientCertData{}
has, err := GetXdb().Where("username = ? AND groupname = ?", username, groupname).Get(clientCert)
if err != nil {
return nil, err
}
if !has {
return nil, ErrNotFound
}
return clientCert, err
}
// 生成客户端 CA 证书
func GenerateClientCA() error {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "AnyLink Client CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), // 10年有效期
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return err
}
// 写入 CA 证书文件
certOut, err := os.OpenFile(base.Cfg.ClientCertCAFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
if err != nil {
return err
}
defer certOut.Close()
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
// 写入 CA 私钥文件
keyOut, err := os.OpenFile(base.Cfg.ClientCertCAKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer keyOut.Close()
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
}
// 生成客户端证书并保存到数据库
func GenerateClientCert(username, groupname string) (*ClientCertData, error) {
// 检查用户是否存在并验证组成员资格
user := &User{}
err := One("Username", username, user)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("用户不存在: %s", username)
}
return nil, fmt.Errorf("获取用户信息失败: %v", err)
}
// 检查用户是否属于指定组
if !slices.Contains(user.Groups, groupname) {
return nil, fmt.Errorf("用户 %s 不属于组 %s", username, groupname)
}
// 检查是否已存在证书记录
_, err = GetClientCert(username, groupname)
if err != nil {
if !errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("获取用户证书失败: %v", err)
}
} else {
// 用户已有证书记录,不允许重复生成
return nil, fmt.Errorf("用户 %s 已存在证书,所在组:%s请先删除现有证书", username, groupname)
}
// 确保客户端 CA 已加载
if err := LoadClientCA(); err != nil {
return nil, fmt.Errorf("无法加载客户端 CA: %v", err)
}
// 生成客户端私钥
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// 创建客户端证书模板
template := x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: username,
OrganizationalUnit: []string{groupname},
Organization: []string{"AnyLink VPN"},
Country: []string{"CN"},
Province: []string{"Beijing"},
Locality: []string{"Beijing"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365), // 1年有效期
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
DNSNames: []string{username},
BasicConstraintsValid: true,
IsCA: false,
}
// 签发客户端证书
certDER, err := x509.CreateCertificate(rand.Reader, &template, clientCACert, &clientKey.PublicKey, clientCAKey)
if err != nil {
return nil, err
}
// 编码证书和私钥为 PEM 格式
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey)})
// 保存到数据库
clientCertData := &ClientCertData{
Username: username,
Groupname: groupname,
Certificate: string(certPEM),
PrivateKey: string(keyPEM),
SerialNumber: template.SerialNumber.String(),
NotAfter: template.NotAfter,
CreatedAt: time.Now(),
Status: CertStatusActive, // 初始状态为有效
}
if err := clientCertData.Save(); err != nil {
return nil, fmt.Errorf("保存客户端证书失败: %v", err)
}
return clientCertData, nil
}
// 生成 PKCS#12 格式证书文件
func GenerateClientP12FromDB(username, groupname, password string) ([]byte, error) {
// 从数据库获取证书
clientCert, err := GetClientCert(username, groupname)
if err != nil {
return nil, err
}
// 检查并更新证书状态
if err := clientCert.CheckAndUpdateStatus(); err != nil {
base.Error("检查并更新证书状态失败:", err)
}
// 检查证书状态
if clientCert.GetStatus() != CertStatusActive {
return nil, fmt.Errorf("用户 %s 的证书状态为:%s", username, clientCert.GetStatusText())
}
// 确保客户端 CA 已加载
if err := LoadClientCA(); err != nil {
return nil, fmt.Errorf("无法加载客户端 CA: %v", err)
}
// 解析证书和私钥
certBlock, _ := pem.Decode([]byte(clientCert.Certificate))
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode([]byte(clientCert.PrivateKey))
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, err
}
// 打包为 .p12 格式
p12Data, err := pkcs12.Modern.Encode(key, cert, []*x509.Certificate{clientCACert}, password)
if err != nil {
return nil, err
}
return p12Data, nil
}
// 验证客户端证书
func ValidateClientCert(cert *x509.Certificate, userAgent string) bool {
// 获取用户和证书信息
user := &User{
Username: cert.Subject.CommonName,
}
err := One("Username", user.Username, user)
if err != nil {
if errors.Is(err, ErrNotFound) {
base.Error("证书验证失败:用户不存在", cert.Subject.CommonName)
} else {
base.Error("证书验证失败:查询用户失败:", err)
}
return false
}
// 检查用户状态是否启用
if user.Status != 1 {
base.Error("证书验证失败:用户已禁用:", user.Username)
return false
}
// 获取客户端证书记录
clientCertData, err := GetClientCert(user.Username, cert.Subject.OrganizationalUnit[0])
if err != nil {
base.Error("证书验证失败:获取客户端证书失败:", err)
return false
}
if clientCertData.Groupname != cert.Subject.OrganizationalUnit[0] {
base.Error("证书验证失败:证书组名与用户组名不匹配")
return false
}
// 检查证书状态
if clientCertData.GetStatus() != CertStatusActive {
base.Error("证书验证失败:", user.Username, "证书状态为", clientCertData.GetStatusText())
return false
}
// 检查证书是否过期
if time.Now().After(cert.NotAfter) {
base.Error("证书验证失败:证书已过期:", cert.NotAfter)
return false
}
// 验证证书指纹
storedCertBlock, _ := pem.Decode([]byte(clientCertData.Certificate))
storedCert, err := x509.ParseCertificate(storedCertBlock.Bytes)
if err != nil {
base.Error("证书验证失败:解析存储证书失败:", err)
return false
}
// 比较证书的完整内容
if !bytes.Equal(cert.Raw, storedCert.Raw) {
base.Error("证书验证失败:证书内容不匹配")
return false
}
// 验证证书链
verifyOptions := x509.VerifyOptions{
Roots: LoadClientCAPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
if _, err := cert.Verify(verifyOptions); err != nil {
base.Error("证书验证失败:证书链验证失败:", err)
return false
}
// 检查扩展密钥用途
hasClientAuth := slices.Contains(cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
if !hasClientAuth {
base.Error("证书验证失败:证书缺少客户端认证扩展")
return false
}
return true
}
// 加载客户端 CA 证书池
func LoadClientCAPool() *x509.CertPool {
if err := LoadClientCA(); err != nil {
return nil
}
caCertPool := x509.NewCertPool()
caCertPool.AddCert(clientCACert)
return caCertPool
}
// 加载客户端 CA 证书和私钥
func LoadClientCA() error {
caMutex.Lock()
defer caMutex.Unlock()
// 如果证书已经加载到内存中,则直接返回
if clientCACert != nil && clientCAKey != nil {
return nil
}
caCertPEM, readErr := os.ReadFile(base.Cfg.ClientCertCAFile)
if readErr != nil {
base.Warn("无法读取客户端 CA 证书,请初始化CA:", readErr)
return fmt.Errorf("无法读取客户端 CA 证书,请初始化CA: %w", readErr)
}
caKeyPEM, readErr := os.ReadFile(base.Cfg.ClientCertCAKeyFile)
if readErr != nil {
return fmt.Errorf("无法读取客户端 CA 私钥: %w", readErr)
}
caCertBlock, _ := pem.Decode(caCertPEM)
if caCertBlock == nil {
return errors.New("无法解析客户端 CA 证书 PEM 块")
}
var parseErr error
clientCACert, parseErr = x509.ParseCertificate(caCertBlock.Bytes)
if parseErr != nil {
return fmt.Errorf("无法解析客户端 CA 证书: %w", parseErr)
}
caKeyBlock, _ := pem.Decode(caKeyPEM)
if caKeyBlock == nil {
return errors.New("无法解析客户端 CA 私钥 PEM 块")
}
var parseKeyErr error
clientCAKey, parseKeyErr = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
if parseKeyErr != nil {
// 解析为PKCS8
pkcs8Key, pkcs8Err := x509.ParsePKCS8PrivateKey(caKeyBlock.Bytes)
if pkcs8Err != nil {
return fmt.Errorf("无法解析客户端 CA 私钥 (PKCS1 or PKCS8): %w", parseKeyErr)
}
var ok bool
clientCAKey, ok = pkcs8Key.(*rsa.PrivateKey)
if !ok {
return errors.New("解析私钥成功,但不是 RSA 类型")
}
}
return nil
}