Scert/backend/internal/httpapi/tls_certificate.go

141 lines
5.1 KiB
Go

package httpapi
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"reflect"
"strings"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
)
type ManagementCertificateState struct {
Hostname string
CertificatePath string
PrivateKeyPath string
NotAfter time.Time
HTTPSMode string
ManagementURL string
DNSVerified bool
Request certificate.SetupCertificateRequest
}
// ManagementCertificate 返回实际配置的管理证书,不依赖业务域名仓储。
func (s *Server) ManagementCertificate() (ManagementCertificateState, error) {
s.configMu.RLock()
defer s.configMu.RUnlock()
state := s.managementCertificateState()
pair, err := tls.LoadX509KeyPair(state.CertificatePath, state.PrivateKeyPath)
if err != nil {
return state, fmt.Errorf("读取管理证书失败:%w", err)
}
leaf, err := x509.ParseCertificate(pair.Certificate[0])
if err != nil {
return state, fmt.Errorf("解析管理证书失败:%w", err)
}
state.NotAfter = leaf.NotAfter
return state, nil
}
// 调用方持有配置锁;只返回续期需要的值,不暴露完整配置或通知凭据。
func (s *Server) managementCertificateState() ManagementCertificateState {
return ManagementCertificateState{
Hostname: strings.TrimSpace(s.config.Web.Hostname),
CertificatePath: s.config.Server.TLSCert,
PrivateKeyPath: s.config.Server.TLSKey,
HTTPSMode: s.config.Server.HTTPSMode,
ManagementURL: s.config.ManagementURL(),
DNSVerified: s.config.DNS.VerificationState == "verified",
Request: certificate.SetupCertificateRequest{
Domains: []string{strings.TrimSpace(s.config.Web.Hostname)}, CertificateType: "single",
ProviderID: s.config.DNS.Provider, CredentialRef: s.config.DNS.CredentialRef,
AccountEmail: s.config.ACME.AccountEmail, Environment: s.config.ACME.Environment,
CertificateRoot: s.config.Paths.Certificate, BackupRoot: s.config.Paths.Backup,
},
}
}
// ActivateTLSCertificate 先持久化完整证书路径,再切换不可变快照;失败不影响旧连接。
// 此方法仅供进程内部续期流程调用,不提供上传或任意路径的 HTTP 接口。
func (s *Server) ActivateTLSCertificate(certificatePath, privateKeyPath string) error {
return s.activateTLSCertificate(certificatePath, privateKeyPath, nil)
}
// ActivateManagementCertificate 拒绝覆盖签发期间发生的配置或证书变化。
func (s *Server) ActivateManagementCertificate(expected ManagementCertificateState, saved certificate.SavedResource) error {
return s.activateTLSCertificate(saved.FullChainPath, saved.PrivateKeyPath, &expected)
}
func (s *Server) activateTLSCertificate(certificatePath, privateKeyPath string, expected *ManagementCertificateState) error {
s.tlsMu.Lock()
defer s.tlsMu.Unlock()
s.configMu.Lock()
defer s.configMu.Unlock()
if expected != nil {
previous := *expected
previous.NotAfter = time.Time{}
if !reflect.DeepEqual(previous, s.managementCertificateState()) {
return errors.New("管理证书签发期间配置已变化,已拒绝覆盖")
}
}
if !s.config.SetupReady || strings.TrimSpace(s.configPath) == "" {
return errors.New("管理证书热更新需要已初始化的持久化配置")
}
certificatePath = strings.TrimSpace(certificatePath)
privateKeyPath = strings.TrimSpace(privateKeyPath)
pair, err := tls.LoadX509KeyPair(certificatePath, privateKeyPath)
if err != nil {
return fmt.Errorf("加载新的管理证书失败:%w", err)
}
leaf, err := x509.ParseCertificate(pair.Certificate[0])
if err != nil {
return fmt.Errorf("解析新的管理证书失败:%w", err)
}
now := time.Now()
if now.Before(leaf.NotBefore) || !now.Before(leaf.NotAfter) {
return errors.New("新的管理证书不在有效期内")
}
if expected != nil && !leaf.NotAfter.After(expected.NotAfter) {
return errors.New("新的管理证书未延长实际有效期")
}
host := strings.TrimSpace(s.config.Web.Hostname)
if host == "" {
host = strings.TrimSpace(s.config.Web.PublicIP)
}
if host == "" {
return errors.New("管理域名或公网 IP 未配置")
}
// 校验与切换使用同一份内存数据,避免重复读文件造成校验对象变化。
var chain []byte
for _, der := range pair.Certificate {
chain = append(chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...)
}
validator := certificate.LocalValidator{VerifyChain: true, Roots: s.tlsRoots}
if _, err := validator.ValidatePEM(context.Background(), chain, []string{host}); err != nil {
return fmt.Errorf("新的管理证书校验失败:%w", err)
}
pair.Leaf = leaf
candidate := s.config
candidate.Server.TLSCert = certificatePath
candidate.Server.TLSKey = privateKeyPath
if err := candidate.Save(s.configPath); err != nil {
return fmt.Errorf("保存管理证书配置失败:%w", err)
}
s.config = candidate
s.tlsCertificate.Store(&pair)
return nil
}
func (s *Server) getTLSCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
pair := s.tlsCertificate.Load()
if pair == nil {
return nil, errors.New("管理 HTTPS 证书尚未加载")
}
return pair, nil
}