Files
SamWaf/utils/ssl/ssl.go
T
lvshujun 38e242e5c7 Add dns check time
Add log output
Remove old env
2026-03-25 18:24:26 +08:00

525 lines
15 KiB
Go

package ssl
import (
"SamWaf/common/zlog"
"SamWaf/model"
"SamWaf/utils"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/go-acme/lego/v4/registration"
)
type MyUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *MyUser) GetEmail() string {
return u.Email
}
func (u MyUser) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
func RegistrationSSL(order model.SslOrder, savePath string, caServerAddress string, applyPlatform string, eab_kid string, eab_hmac_key string) (model.SslOrder, error) {
isIpSSL := utils.IsIP(order.ApplyDomain)
myUser := MyUser{
Email: order.ApplyEmail,
}
if order.ApplyKey == "" {
// Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return order, err
}
toPEMPrivate, err := privateKeyToPEM(privateKey)
if err != nil {
return order, err
} else {
order.ApplyKey = toPEMPrivate
}
myUser.key = privateKey
} else {
privateKey, err := pemToPrivateKey(order.ApplyKey)
if err != nil {
return order, err
} else {
myUser.key = privateKey
}
}
//order.ApplyKey = privateKey
config := lego.NewConfig(&myUser)
config.CADirURL = caServerAddress
config.Certificate.KeyType = certcrypto.RSA2048
if isIpSSL {
//是否是ip申请ssl证书
config.Certificate.DisableCommonName = true
}
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
if err != nil {
return order, err
}
// We specify an HTTP port of 5002 and an TLS port of 5001 on all interfaces
// because we aren't running as root and can't bind a listener to port 80 and 443
// (used later when we attempt to pass challenges). Keep in mind that you still
// need to proxy challenge traffic to port 5002 and 5001.
switch order.ApplyMethod {
case "http01":
provider, err := webroot.NewHTTPProvider(savePath)
if err != nil {
return order, err
}
err = client.Challenge.SetHTTP01Provider(provider)
case "dns01":
dnsProvider, err := GetDnsProvider(order.ApplyDns)
if err != nil {
zlog.Error(fmt.Sprintf("Failed to initialize DNS provider `%s`: %v", order.ApplyDns, err))
return order, err
}
err = setDNS01ProviderWithRetry(client, dnsProvider, order.SkipDNSVerify)
if err != nil {
zlog.Error(fmt.Sprintf("Failed to set DNS-01 provider: %v", err))
}
}
if err != nil {
return order, err
}
// New users will need to register
var reg *registration.Resource
if applyPlatform == "zerossl" {
// ZeroSSL 需要使用 EAB (External Account Binding) 方式注册
eabOptions := registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: eab_kid,
HmacEncoded: eab_hmac_key,
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(eabOptions)
if err != nil {
return order, err
}
} else {
// 其他平台使用原来的注册方式
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return order, err
}
}
myUser.Registration = reg
certificates := &certificate.Resource{}
if !isIpSSL {
// 诊断域名权威 NS
if order.ApplyMethod == "dns01" {
DiagnoseDomainNS(order.ApplyDomain)
}
request := certificate.ObtainRequest{
Domains: strings.Split(order.ApplyDomain, ","),
Bundle: true,
}
certificatesLocal, err := client.Certificate.Obtain(request)
if err != nil {
return order, err
}
certificates = certificatesLocal
} else {
// 1. 为证书生成新的私钥
certPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return order, fmt.Errorf("generate cert private key failed: %w", err)
}
// 2. 用新私钥创建 CSR
csrDER, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "",
},
IPAddresses: []net.IP{net.ParseIP(order.ApplyDomain)},
},
certPrivateKey, // 使用证书专用私钥
)
if err != nil {
return order, err
}
// 3. 解析 CSR
csr, err := x509.ParseCertificateRequest(csrDER)
if err != nil {
return order, err
}
// 4. 申请证书
certificatesLocal, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: true,
PrivateKey: certPrivateKey, // 使用证书专用私钥
Profile: "shortlived",
})
if err != nil {
return order, err
}
certificates = certificatesLocal
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
order.ResultPrivateKey = certificates.PrivateKey
order.ResultCertificate = certificates.Certificate
order.ResultCertStableURL = certificates.CertStableURL
order.ResultCertURL = certificates.CertURL
order.ResultCSR = certificates.CSR
// IP证书情况下,certificates.Domain可能为空,使用申请的IP地址
if isIpSSL && certificates.Domain == "" {
order.ResultDomain = order.ApplyDomain
} else {
order.ResultDomain = certificates.Domain
}
order.ResultIssuerCertificate = certificates.IssuerCertificate
block, _ := pem.Decode(order.ResultCertificate)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
order.ResultValidTo = cert.NotAfter
}
}
return order, nil
}
func ReNewSSL(order model.SslOrder, savePath string, caServerAddress string, applyPlatform string, eab_kid string, eab_hmac_key string) (model.SslOrder, error) {
// 判断是否是IP证书
isIpSSL := utils.IsIP(order.ApplyDomain)
myUser := MyUser{
Email: order.ApplyEmail,
}
if order.ApplyKey == "" {
// Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return order, err
}
toPEMPrivate, err := privateKeyToPEM(privateKey)
if err != nil {
return order, err
} else {
order.ApplyKey = toPEMPrivate
}
myUser.key = privateKey
} else {
privateKey, err := pemToPrivateKey(order.ApplyKey)
if err != nil {
return order, err
} else {
myUser.key = privateKey
}
}
//order.ApplyKey = privateKey
config := lego.NewConfig(&myUser)
config.CADirURL = caServerAddress
config.Certificate.KeyType = certcrypto.RSA2048
if isIpSSL {
//是否是ip申请ssl证书
config.Certificate.DisableCommonName = true
}
// A client facilitates communication with the CA server.
client, err := lego.NewClient(config)
if err != nil {
return order, err
}
// We specify an HTTP port of 5002 and an TLS port of 5001 on all interfaces
// because we aren't running as root and can't bind a listener to port 80 and 443
// (used later when we attempt to pass challenges). Keep in mind that you still
// need to proxy challenge traffic to port 5002 and 5001.
if order.ApplyMethod == "http01" {
provider, err := webroot.NewHTTPProvider(savePath)
if err != nil {
return order, err
}
err = client.Challenge.SetHTTP01Provider(provider)
} else if order.ApplyMethod == "dns01" {
dnsProvider, err := GetDnsProvider(order.ApplyDns)
if err != nil {
zlog.Error(fmt.Sprintf("Failed to initialize DNS provider `%s`: %v", order.ApplyDns, err))
return order, err
}
err = setDNS01ProviderWithRetry(client, dnsProvider, order.SkipDNSVerify)
if err != nil {
zlog.Error(fmt.Sprintf("Failed to set DNS-01 provider: %v", err))
}
}
if err != nil {
return order, err
}
// New users will need to register
var reg *registration.Resource
if applyPlatform == "zerossl" {
// ZeroSSL 需要使用 EAB (External Account Binding) 方式注册
eabOptions := registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: eab_kid,
HmacEncoded: eab_hmac_key,
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(eabOptions)
if err != nil {
return order, err
}
} else {
// 其他平台使用原来的注册方式
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return order, err
}
}
myUser.Registration = reg
certificates := &certificate.Resource{}
if !isIpSSL {
// 诊断域名权威 NS
if order.ApplyMethod == "dns01" {
DiagnoseDomainNS(order.ApplyDomain)
}
// 域名证书:使用续期方式
certRes := certificate.Resource{
Domain: order.ResultDomain,
CertURL: order.ResultCertURL,
CertStableURL: order.ResultCertStableURL,
PrivateKey: order.ResultPrivateKey,
Certificate: order.ResultCertificate,
IssuerCertificate: order.ResultIssuerCertificate,
CSR: order.ResultCSR,
}
//构造参数
certificatesLocal, err := client.Certificate.RenewWithOptions(certRes, &certificate.RenewOptions{
Bundle: true,
})
if err != nil {
return order, err
}
certificates = certificatesLocal
} else {
// IP证书:使用CSR方式重新申请
// 1. 为证书生成新的私钥
certPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return order, fmt.Errorf("generate cert private key failed: %w", err)
}
// 2. 用新私钥创建 CSR
csrDER, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "",
},
IPAddresses: []net.IP{net.ParseIP(order.ApplyDomain)},
},
certPrivateKey, // 使用证书专用私钥
)
if err != nil {
return order, err
}
// 3. 解析 CSR
csr, err := x509.ParseCertificateRequest(csrDER)
if err != nil {
return order, err
}
// 4. 申请新证书
certificatesLocal, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
CSR: csr,
Bundle: true,
PrivateKey: certPrivateKey, // 使用证书专用私钥
Profile: "shortlived",
})
if err != nil {
return order, err
}
certificates = certificatesLocal
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
order.ResultPrivateKey = certificates.PrivateKey
order.ResultCertificate = certificates.Certificate
order.ResultCertStableURL = certificates.CertStableURL
order.ResultCertURL = certificates.CertURL
order.ResultCSR = certificates.CSR
// IP证书情况下,certificates.Domain可能为空,使用申请的IP地址
if isIpSSL && certificates.Domain == "" {
order.ResultDomain = order.ApplyDomain
} else {
order.ResultDomain = certificates.Domain
}
order.ResultIssuerCertificate = certificates.IssuerCertificate
block, _ := pem.Decode(order.ResultCertificate)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
order.ResultValidTo = cert.NotAfter
}
}
return order, nil
}
// 将ECDSA私钥编码为PEM格式的字符串
func privateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) {
// 将私钥转换为DER格式字节
privBytes, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return "", err
}
// 将DER格式字节封装为PEM格式
pemKey := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: privBytes,
})
return string(pemKey), nil
}
// 将PEM格式字符串转换回ECDSA私钥
func pemToPrivateKey(pemStr string) (*ecdsa.PrivateKey, error) {
// 解码PEM字符串
block, _ := pem.Decode([]byte(pemStr))
if block == nil || block.Type != "EC PRIVATE KEY" {
return nil, fmt.Errorf("invalid PEM block")
}
// 解析DER格式的私钥
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privateKey, nil
}
func GetDnsProvider(dnsName string) (challenge.Provider, error) {
switch dnsName {
case "alidns":
return alidns.NewDNSProvider()
case "huaweicloud":
return huaweicloud.NewDNSProvider()
case "tencentcloud":
return tencentcloud.NewDNSProvider()
case "cloudflare":
return cloudflare.NewDNSProvider()
case "baiducloud":
return baiducloud.NewDNSProvider()
default:
return nil, fmt.Errorf("unrecognized DNS provider: %s", dnsName)
}
}
func setDNS01ProviderWithRetry(client *lego.Client, dnsProvider challenge.Provider, skipDNSVerify int64) error {
// 默认重试10次,每次间隔30秒
dnsPrecheckRetry := 10
dnsPrecheckRetryInterval := 30 * time.Second
// DNS 传播等待时间:300秒(3分钟)
propagationTimeout := 180 * time.Second
if skipDNSVerify == 1 {
// 跳过本地DNS传播校验:固定等待后直接通知CA进行验证
return client.Challenge.SetDNS01Provider(dnsProvider, dns01.PropagationWait(propagationTimeout, true))
}
// 默认执行本地DNS传播校验,并增加重试机制
return client.Challenge.SetDNS01Provider(dnsProvider,
dns01.WrapPreCheck(func(domain, fqdn, value string, check dns01.PreCheckFunc) (bool, error) {
var lastErr error
for i := 1; i <= dnsPrecheckRetry; i++ {
ok, err := check(fqdn, value)
if ok && err == nil {
return true, nil
}
lastErr = err
if i < dnsPrecheckRetry {
time.Sleep(dnsPrecheckRetryInterval)
}
}
if lastErr != nil {
return false, lastErr
}
return false, fmt.Errorf("dns propagation precheck failed after %d retries", dnsPrecheckRetry)
}),
)
}
// DiagnoseDomainNS 诊断域名的权威 NS 服务器
func DiagnoseDomainNS(domain string) error {
zlog.Info(fmt.Sprintf("Checking authoritative nameservers for domain: %s", domain))
// 查询域名的权威 NS 记录
nameServers, err := net.LookupNS(domain)
if err != nil {
zlog.Warn(fmt.Sprintf("Failed to lookup NS records for %s: %v", domain, err))
return err
}
// 检查是否是预期的 DNS 提供商
nsStr := strings.ToLower(fmt.Sprintf("%v", nameServers))
if strings.Contains(nsStr, "dnspod") || strings.Contains(nsStr, "tencent") {
zlog.Info(fmt.Sprintf("Domain %s is managed by Tencent Cloud DNSPod", domain))
} else if strings.Contains(nsStr, "alidns") || strings.Contains(nsStr, "alibaba") {
zlog.Warn(fmt.Sprintf("Domain %s appears to be managed by Alibaba Cloud (not Tencent Cloud)", domain))
} else if strings.Contains(nsStr, "huawei") {
zlog.Warn(fmt.Sprintf("Domain %s appears to be managed by Huawei Cloud (not Tencent Cloud)", domain))
} else if strings.Contains(nsStr, "cloudflare") {
zlog.Warn(fmt.Sprintf("Domain %s appears to be managed by Cloudflare (not Tencent Cloud)", domain))
} else {
zlog.Warn(fmt.Sprintf("Domain %s's authoritative NS is not recognized. Make sure NS records point to your DNS provider", domain))
}
return nil
}