diff --git a/internal/db/models/ssl_cert_dao.go b/internal/db/models/ssl_cert_dao.go index 4bcc6da3..e41702e4 100644 --- a/internal/db/models/ssl_cert_dao.go +++ b/internal/db/models/ssl_cert_dao.go @@ -113,6 +113,8 @@ func (this *SSLCertDAO) CreateCert(tx *dbs.Tx, adminId int64, userId int64, isOn } op.CommonNames = commonNamesJSON + op.OcspIsUpdated = false + err = this.Save(tx, op) if err != nil { return 0, err @@ -121,7 +123,18 @@ func (this *SSLCertDAO) CreateCert(tx *dbs.Tx, adminId int64, userId int64, isOn } // UpdateCert 修改证书 -func (this *SSLCertDAO) UpdateCert(tx *dbs.Tx, certId int64, isOn bool, name string, description string, serverName string, isCA bool, certData []byte, keyData []byte, timeBeginAt int64, timeEndAt int64, dnsNames []string, commonNames []string) error { +func (this *SSLCertDAO) UpdateCert(tx *dbs.Tx, + certId int64, + isOn bool, + name string, + description string, + serverName string, + isCA bool, + certData []byte, + keyData []byte, + timeBeginAt int64, + timeEndAt int64, + dnsNames []string, commonNames []string) error { if certId <= 0 { return errors.New("invalid certId") } @@ -156,6 +169,8 @@ func (this *SSLCertDAO) UpdateCert(tx *dbs.Tx, certId int64, isOn bool, name str } op.CommonNames = commonNamesJSON + op.OcspIsUpdated = false + err = this.Save(tx, op) if err != nil { return err @@ -194,6 +209,7 @@ func (this *SSLCertDAO) ComposeCertConfig(tx *dbs.Tx, certId int64, cacheMap *ut config.ServerName = cert.ServerName config.TimeBeginAt = int64(cert.TimeBeginAt) config.TimeEndAt = int64(cert.TimeEndAt) + config.OCSP = []byte(cert.Ocsp) if IsNotNull(cert.DnsNames) { dnsNames := []string{} @@ -356,6 +372,41 @@ func (this *SSLCertDAO) CheckUserCert(tx *dbs.Tx, certId int64, userId int64) er return nil } +// ListCertsToUpdateOCSP 查找需要更新OCSP的证书 +func (this *SSLCertDAO) ListCertsToUpdateOCSP(tx *dbs.Tx, size int64) (result []*SSLCert, err error) { + _, err = this.Query(tx). + State(SSLCertStateEnabled). + Attr("ocspIsUpdated", false). + Limit(size). + Slice(&result). + FindAll() + return +} + +// UpdateCertOSCP 修改OCSP +func (this *SSLCertDAO) UpdateCertOSCP(tx *dbs.Tx, certId int64, ocsp []byte, errString string) error { + if ocsp == nil { + ocsp = []byte{} + } + + // 限制长度 + if len(errString) > 300 { + errString = errString[:300] + } + + err := this.Query(tx). + Pk(certId). + Set("ocsp", ocsp). + Set("ocspError", errString). + Set("ocspIsUpdated", true). + UpdateQuickly() + if err != nil { + return err + } + + return this.NotifyUpdate(tx, certId) +} + // NotifyUpdate 通知更新 func (this *SSLCertDAO) NotifyUpdate(tx *dbs.Tx, certId int64) error { policyIds, err := SharedSSLPolicyDAO.FindAllEnabledPolicyIdsWithCertId(tx, certId) diff --git a/internal/db/models/ssl_cert_model.go b/internal/db/models/ssl_cert_model.go index 8fc6622c..a033660e 100644 --- a/internal/db/models/ssl_cert_model.go +++ b/internal/db/models/ssl_cert_model.go @@ -1,52 +1,58 @@ package models -// SSL证书 +// SSLCert SSL证书 type SSLCert struct { - Id uint32 `field:"id"` // ID - AdminId uint32 `field:"adminId"` // 管理员ID - UserId uint32 `field:"userId"` // 用户ID - State uint8 `field:"state"` // 状态 - CreatedAt uint64 `field:"createdAt"` // 创建时间 - UpdatedAt uint64 `field:"updatedAt"` // 修改时间 - IsOn uint8 `field:"isOn"` // 是否启用 - Name string `field:"name"` // 证书名 - Description string `field:"description"` // 描述 - CertData string `field:"certData"` // 证书内容 - KeyData string `field:"keyData"` // 密钥内容 - ServerName string `field:"serverName"` // 证书使用的主机名 - IsCA uint8 `field:"isCA"` // 是否为CA证书 - GroupIds string `field:"groupIds"` // 证书分组 - TimeBeginAt uint64 `field:"timeBeginAt"` // 开始时间 - TimeEndAt uint64 `field:"timeEndAt"` // 结束时间 - DnsNames string `field:"dnsNames"` // DNS名称列表 - CommonNames string `field:"commonNames"` // 发行单位列表 - IsACME uint8 `field:"isACME"` // 是否为ACME自动生成的 - AcmeTaskId uint64 `field:"acmeTaskId"` // ACME任务ID - NotifiedAt uint64 `field:"notifiedAt"` // 最后通知时间 + Id uint32 `field:"id"` // ID + AdminId uint32 `field:"adminId"` // 管理员ID + UserId uint32 `field:"userId"` // 用户ID + State uint8 `field:"state"` // 状态 + CreatedAt uint64 `field:"createdAt"` // 创建时间 + UpdatedAt uint64 `field:"updatedAt"` // 修改时间 + IsOn uint8 `field:"isOn"` // 是否启用 + Name string `field:"name"` // 证书名 + Description string `field:"description"` // 描述 + CertData string `field:"certData"` // 证书内容 + KeyData string `field:"keyData"` // 密钥内容 + ServerName string `field:"serverName"` // 证书使用的主机名 + IsCA uint8 `field:"isCA"` // 是否为CA证书 + GroupIds string `field:"groupIds"` // 证书分组 + TimeBeginAt uint64 `field:"timeBeginAt"` // 开始时间 + TimeEndAt uint64 `field:"timeEndAt"` // 结束时间 + DnsNames string `field:"dnsNames"` // DNS名称列表 + CommonNames string `field:"commonNames"` // 发行单位列表 + IsACME uint8 `field:"isACME"` // 是否为ACME自动生成的 + AcmeTaskId uint64 `field:"acmeTaskId"` // ACME任务ID + NotifiedAt uint64 `field:"notifiedAt"` // 最后通知时间 + Ocsp string `field:"ocsp"` // OCSP缓存 + OcspIsUpdated uint8 `field:"ocspIsUpdated"` // OCSP是否已更新 + OcspError string `field:"ocspError"` // OCSP更新错误 } type SSLCertOperator struct { - Id interface{} // ID - AdminId interface{} // 管理员ID - UserId interface{} // 用户ID - State interface{} // 状态 - CreatedAt interface{} // 创建时间 - UpdatedAt interface{} // 修改时间 - IsOn interface{} // 是否启用 - Name interface{} // 证书名 - Description interface{} // 描述 - CertData interface{} // 证书内容 - KeyData interface{} // 密钥内容 - ServerName interface{} // 证书使用的主机名 - IsCA interface{} // 是否为CA证书 - GroupIds interface{} // 证书分组 - TimeBeginAt interface{} // 开始时间 - TimeEndAt interface{} // 结束时间 - DnsNames interface{} // DNS名称列表 - CommonNames interface{} // 发行单位列表 - IsACME interface{} // 是否为ACME自动生成的 - AcmeTaskId interface{} // ACME任务ID - NotifiedAt interface{} // 最后通知时间 + Id interface{} // ID + AdminId interface{} // 管理员ID + UserId interface{} // 用户ID + State interface{} // 状态 + CreatedAt interface{} // 创建时间 + UpdatedAt interface{} // 修改时间 + IsOn interface{} // 是否启用 + Name interface{} // 证书名 + Description interface{} // 描述 + CertData interface{} // 证书内容 + KeyData interface{} // 密钥内容 + ServerName interface{} // 证书使用的主机名 + IsCA interface{} // 是否为CA证书 + GroupIds interface{} // 证书分组 + TimeBeginAt interface{} // 开始时间 + TimeEndAt interface{} // 结束时间 + DnsNames interface{} // DNS名称列表 + CommonNames interface{} // 发行单位列表 + IsACME interface{} // 是否为ACME自动生成的 + AcmeTaskId interface{} // ACME任务ID + NotifiedAt interface{} // 最后通知时间 + Ocsp interface{} // OCSP缓存 + OcspIsUpdated interface{} // OCSP是否已更新 + OcspError interface{} // OCSP更新错误 } func NewSSLCertOperator() *SSLCertOperator { diff --git a/internal/db/models/ssl_policy_dao.go b/internal/db/models/ssl_policy_dao.go index 5927005c..7cffbac1 100644 --- a/internal/db/models/ssl_policy_dao.go +++ b/internal/db/models/ssl_policy_dao.go @@ -167,6 +167,9 @@ func (this *SSLPolicyDAO) ComposePolicyConfig(tx *dbs.Tx, policyId int64, cacheM config.HSTS = hstsConfig } + // ocsp + config.OCSPIsOn = policy.OcspIsOn == 1 + if cacheMap != nil { cacheMap.Put(cacheKey, config) } @@ -196,7 +199,7 @@ func (this *SSLPolicyDAO) FindAllEnabledPolicyIdsWithCertId(tx *dbs.Tx, certId i } // CreatePolicy 创建Policy -func (this *SSLPolicyDAO) CreatePolicy(tx *dbs.Tx, adminId int64, userId int64, http2Enabled bool, minVersion string, certsJSON []byte, hstsJSON []byte, clientAuthType int32, clientCACertsJSON []byte, cipherSuitesIsOn bool, cipherSuites []string) (int64, error) { +func (this *SSLPolicyDAO) CreatePolicy(tx *dbs.Tx, adminId int64, userId int64, http2Enabled bool, minVersion string, certsJSON []byte, hstsJSON []byte, ocspIsOn bool, clientAuthType int32, clientCACertsJSON []byte, cipherSuitesIsOn bool, cipherSuites []string) (int64, error) { op := NewSSLPolicyOperator() op.State = SSLPolicyStateEnabled op.IsOn = true @@ -213,6 +216,8 @@ func (this *SSLPolicyDAO) CreatePolicy(tx *dbs.Tx, adminId int64, userId int64, op.Hsts = hstsJSON } + op.OcspIsOn = ocspIsOn + op.ClientAuthType = clientAuthType if len(clientCACertsJSON) > 0 { op.ClientCACerts = clientCACertsJSON @@ -234,7 +239,7 @@ func (this *SSLPolicyDAO) CreatePolicy(tx *dbs.Tx, adminId int64, userId int64, } // UpdatePolicy 修改Policy -func (this *SSLPolicyDAO) UpdatePolicy(tx *dbs.Tx, policyId int64, http2Enabled bool, minVersion string, certsJSON []byte, hstsJSON []byte, clientAuthType int32, clientCACertsJSON []byte, cipherSuitesIsOn bool, cipherSuites []string) error { +func (this *SSLPolicyDAO) UpdatePolicy(tx *dbs.Tx, policyId int64, http2Enabled bool, minVersion string, certsJSON []byte, hstsJSON []byte, ocspIsOn bool, clientAuthType int32, clientCACertsJSON []byte, cipherSuitesIsOn bool, cipherSuites []string) error { if policyId <= 0 { return errors.New("invalid policyId") } @@ -251,6 +256,8 @@ func (this *SSLPolicyDAO) UpdatePolicy(tx *dbs.Tx, policyId int64, http2Enabled op.Hsts = hstsJSON } + op.OcspIsOn = ocspIsOn + op.ClientAuthType = clientAuthType if len(clientCACertsJSON) > 0 { op.ClientCACerts = clientCACertsJSON diff --git a/internal/db/models/ssl_policy_model.go b/internal/db/models/ssl_policy_model.go index 7b3f0936..dd5496b7 100644 --- a/internal/db/models/ssl_policy_model.go +++ b/internal/db/models/ssl_policy_model.go @@ -1,6 +1,6 @@ package models -// +// SSLPolicy SSL配置策略 type SSLPolicy struct { Id uint32 `field:"id"` // ID AdminId uint32 `field:"adminId"` // 管理员ID @@ -14,6 +14,7 @@ type SSLPolicy struct { CipherSuites string `field:"cipherSuites"` // 加密算法套件 Hsts string `field:"hsts"` // HSTS设置 Http2Enabled uint8 `field:"http2Enabled"` // 是否启用HTTP/2 + OcspIsOn uint8 `field:"ocspIsOn"` // 是否启用OCSP State uint8 `field:"state"` // 状态 CreatedAt uint64 `field:"createdAt"` // 创建时间 } @@ -31,6 +32,7 @@ type SSLPolicyOperator struct { CipherSuites interface{} // 加密算法套件 Hsts interface{} // HSTS设置 Http2Enabled interface{} // 是否启用HTTP/2 + OcspIsOn interface{} // 是否启用OCSP State interface{} // 状态 CreatedAt interface{} // 创建时间 } diff --git a/internal/rpc/services/service_ssl_policy.go b/internal/rpc/services/service_ssl_policy.go index 6a3a28fe..b6032331 100644 --- a/internal/rpc/services/service_ssl_policy.go +++ b/internal/rpc/services/service_ssl_policy.go @@ -43,7 +43,7 @@ func (this *SSLPolicyService) CreateSSLPolicy(ctx context.Context, req *pb.Creat // TODO } - policyId, err := models.SharedSSLPolicyDAO.CreatePolicy(tx, adminId, userId, req.Http2Enabled, req.MinVersion, req.SslCertsJSON, req.HstsJSON, req.ClientAuthType, req.ClientCACertsJSON, req.CipherSuitesIsOn, req.CipherSuites) + policyId, err := models.SharedSSLPolicyDAO.CreatePolicy(tx, adminId, userId, req.Http2Enabled, req.MinVersion, req.SslCertsJSON, req.HstsJSON, req.OcspIsOn, req.ClientAuthType, req.ClientCACertsJSON, req.CipherSuitesIsOn, req.CipherSuites) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func (this *SSLPolicyService) UpdateSSLPolicy(ctx context.Context, req *pb.Updat } } - err = models.SharedSSLPolicyDAO.UpdatePolicy(tx, req.SslPolicyId, req.Http2Enabled, req.MinVersion, req.SslCertsJSON, req.HstsJSON, req.ClientAuthType, req.ClientCACertsJSON, req.CipherSuitesIsOn, req.CipherSuites) + err = models.SharedSSLPolicyDAO.UpdatePolicy(tx, req.SslPolicyId, req.Http2Enabled, req.MinVersion, req.SslCertsJSON, req.HstsJSON, req.OcspIsOn, req.ClientAuthType, req.ClientCACertsJSON, req.CipherSuitesIsOn, req.CipherSuites) if err != nil { return nil, err } diff --git a/internal/tasks/ssl_cert_update_ocsp_task.go b/internal/tasks/ssl_cert_update_ocsp_task.go new file mode 100644 index 00000000..d823f4b6 --- /dev/null +++ b/internal/tasks/ssl_cert_update_ocsp_task.go @@ -0,0 +1,176 @@ +// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package tasks + +import ( + "bytes" + "crypto" + "crypto/tls" + "crypto/x509" + "errors" + teaconst "github.com/TeaOSLab/EdgeAPI/internal/const" + "github.com/TeaOSLab/EdgeAPI/internal/db/models" + "github.com/TeaOSLab/EdgeAPI/internal/goman" + "github.com/TeaOSLab/EdgeAPI/internal/remotelogs" + "github.com/TeaOSLab/EdgeAPI/internal/utils" + "github.com/iwind/TeaGo/dbs" + "golang.org/x/crypto/ocsp" + "io/ioutil" + "net/http" + "time" +) + +func init() { + dbs.OnReadyDone(func() { + goman.New(func() { + NewSSLCertUpdateOCSPTask().Start() + }) + }) +} + +type SSLCertUpdateOCSPTask struct { + ticker *time.Ticker +} + +func NewSSLCertUpdateOCSPTask() *SSLCertUpdateOCSPTask { + return &SSLCertUpdateOCSPTask{ + ticker: time.NewTicker(5 * time.Minute), + } +} + +func (this *SSLCertUpdateOCSPTask) Start() { + for range this.ticker.C { + err := this.Loop() + if err != nil { + remotelogs.Error("SSLCertUpdateOCSPTask", err.Error()) + } + } +} + +func (this *SSLCertUpdateOCSPTask) Loop() error { + ok, err := models.SharedSysLockerDAO.Lock(nil, "ssl_cert_update_ocsp_task", 300-1) // 假设执行时间为1秒 + if err != nil { + return err + } + if !ok { + return nil + } + + var tx *dbs.Tx + certs, err := models.SharedSSLCertDAO.ListCertsToUpdateOCSP(tx, 10) + if err != nil { + return errors.New("list certs failed: " + err.Error()) + } + + for _, cert := range certs { + ocspData, err := this.UpdateCertOCSP(cert) + var errString = "" + if err != nil { + errString = err.Error() + } + err = models.SharedSSLCertDAO.UpdateCertOSCP(tx, int64(cert.Id), ocspData, errString) + if err != nil { + return errors.New("update ocsp failed: " + err.Error()) + } + } + + return nil +} + +func (this *SSLCertUpdateOCSPTask) UpdateCertOCSP(certOne *models.SSLCert) (ocspData []byte, err error) { + if certOne.IsCA == 1 || len(certOne.CertData) == 0 || len(certOne.KeyData) == 0 { + return + } + + keyPair, err := tls.X509KeyPair([]byte(certOne.CertData), []byte(certOne.KeyData)) + if err != nil { + return nil, errors.New("parse certificate failed: " + err.Error()) + } + if len(keyPair.Certificate) == 0 { + return nil, nil + } + + var certData = keyPair.Certificate[0] + cert, err := x509.ParseCertificate(certData) + if err != nil { + return nil, errors.New("parse certificate block failed: " + err.Error()) + } + + // 是否已过期 + var now = time.Now() + if cert.NotBefore.After(now) || cert.NotAfter.Before(now) { + return nil, nil + } + + if len(cert.IssuingCertificateURL) == 0 || len(cert.OCSPServer) == 0 { + return nil, nil + } + + if len(cert.DNSNames) == 0 { + return nil, nil + } + + var issuerURL = cert.IssuingCertificateURL[0] + var ocspServerURL = cert.OCSPServer[0] + + var httpClient = utils.SharedHttpClient(5 * time.Second) + issuerReq, err := http.NewRequest(http.MethodGet, issuerURL, nil) + if err != nil { + return nil, errors.New("request issuer certificate failed: " + err.Error()) + } + issuerReq.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version) + issuerResp, err := httpClient.Do(issuerReq) + if err != nil { + return nil, errors.New("request issuer certificate failed: " + err.Error()) + } + defer func() { + _ = issuerResp.Body.Close() + }() + + issuerData, err := ioutil.ReadAll(issuerResp.Body) + if err != nil { + return nil, errors.New("read issuer certificate failed: " + err.Error()) + } + issuerCert, err := x509.ParseCertificate(issuerData) + if err != nil { + return nil, errors.New("parse issuer certificate failed: " + err.Error()) + } + + buf, err := ocsp.CreateRequest(cert, issuerCert, &ocsp.RequestOptions{ + Hash: crypto.SHA1, + }) + if err != nil { + return nil, errors.New("create ocsp request failed: " + err.Error()) + } + ocspReq, err := http.NewRequest(http.MethodPost, ocspServerURL, bytes.NewBuffer(buf)) + if err != nil { + return nil, errors.New("request ocsp failed: " + err.Error()) + } + ocspReq.Header.Set("Content-Type", "application/ocsp-request") + ocspReq.Header.Set("Accept", "application/ocsp-response") + + ocspResp, err := httpClient.Do(ocspReq) + if err != nil { + return nil, errors.New("request ocsp failed: " + err.Error()) + } + + defer func() { + _ = ocspResp.Body.Close() + }() + + respData, err := ioutil.ReadAll(ocspResp.Body) + if err != nil { + return nil, errors.New("read ocsp failed: " + err.Error()) + } + + ocspResult, err := ocsp.ParseResponse(respData, issuerCert) + if err != nil { + return nil, errors.New("decode ocsp failed: " + err.Error()) + } + + // 只返回Good的ocsp + if ocspResult.Status == ocsp.Good { + return respData, nil + } + return nil, nil +} diff --git a/internal/tasks/ssl_cert_update_ocsp_task_test.go b/internal/tasks/ssl_cert_update_ocsp_task_test.go new file mode 100644 index 00000000..7cd35798 --- /dev/null +++ b/internal/tasks/ssl_cert_update_ocsp_task_test.go @@ -0,0 +1,19 @@ +// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package tasks_test + +import ( + "github.com/TeaOSLab/EdgeAPI/internal/tasks" + "github.com/iwind/TeaGo/dbs" + "testing" +) + +func TestSSLCertUpdateOCSPTask_Loop(t *testing.T) { + dbs.NotifyReady() + + var task = tasks.NewSSLCertUpdateOCSPTask() + err := task.Loop() + if err != nil { + t.Fatal(err) + } +}