[SSL证书]实现基本的自动申请证书流程

This commit is contained in:
GoEdgeLab
2020-11-26 16:39:06 +08:00
parent 29cbabebf5
commit c1c83990d6
18 changed files with 594 additions and 27 deletions

View File

@@ -0,0 +1,59 @@
package acme
import (
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/go-acme/lego/v4/challenge/dns01"
"strings"
)
type DNSProvider struct {
raw dnsclients.ProviderInterface
}
func NewDNSProvider(raw dnsclients.ProviderInterface) *DNSProvider {
return &DNSProvider{raw: raw}
}
func (this *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
// 设置记录
index := strings.Index(fqdn, "."+domain)
if index < 0 {
return errors.New("invalid fqdn value")
}
recordName := fqdn[:index]
record, err := this.raw.QueryRecord(domain, recordName, dnsclients.RecordTypeTXT)
if err != nil {
return errors.New("query DNS record failed: " + err.Error())
}
if record == nil {
err = this.raw.AddRecord(domain, &dnsclients.Record{
Id: "",
Name: recordName,
Type: dnsclients.RecordTypeTXT,
Value: value,
Route: this.raw.DefaultRoute(),
})
if err != nil {
return errors.New("create DNS record failed: " + err.Error())
}
} else {
err = this.raw.UpdateRecord(domain, record, &dnsclients.Record{
Name: recordName,
Type: dnsclients.RecordTypeTXT,
Value: value,
Route: this.raw.DefaultRoute(),
})
if err != nil {
return errors.New("update DNS record failed: " + err.Error())
}
}
return nil
}
func (this *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}

15
internal/acme/key.go Normal file
View File

@@ -0,0 +1,15 @@
package acme
import (
"crypto/x509"
"encoding/base64"
)
func ParsePrivateKeyFromBase64(base64String string) (interface{}, error) {
data, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return nil, err
}
return x509.ParsePKCS8PrivateKey(data)
}

94
internal/acme/request.go Normal file
View File

@@ -0,0 +1,94 @@
package acme
import (
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
acmelog "github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/registration"
"io/ioutil"
"log"
)
type Request struct {
debug bool
task *Task
}
func NewRequest(task *Task) *Request {
return &Request{
task: task,
}
}
func (this *Request) Debug() {
this.debug = true
}
func (this *Request) Run() (certData []byte, keyData []byte, err error) {
if !this.debug {
acmelog.Logger = log.New(ioutil.Discard, "", log.LstdFlags)
}
if this.task.User == nil {
err = errors.New("'user' must not be nil")
return
}
if this.task.DNSProvider == nil {
err = errors.New("'dnsProvider' must not be nil")
return
}
if len(this.task.DNSDomain) == 0 {
err = errors.New("'dnsDomain' must not be empty")
return
}
if len(this.task.Domains) == 0 {
err = errors.New("'domains' must not be empty")
return
}
config := lego.NewConfig(this.task.User)
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
return nil, nil, err
}
// 注册用户
resource := this.task.User.GetRegistration()
if resource != nil {
resource, err = client.Registration.QueryRegistration()
if err != nil {
return nil, nil, err
}
} else {
resource, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, nil, err
}
err = this.task.User.Register(resource)
if err != nil {
return nil, nil, err
}
}
err = client.Challenge.SetDNS01Provider(NewDNSProvider(this.task.DNSProvider))
if err != nil {
return nil, nil, err
}
// 申请证书
request := certificate.ObtainRequest{
Domains: this.task.Domains,
Bundle: true,
}
certResource, err := client.Certificate.Obtain(request)
if err != nil {
return nil, nil, err
}
return certResource.Certificate, certResource.PrivateKey, nil
}

View File

@@ -0,0 +1,74 @@
package acme
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/go-acme/lego/v4/registration"
_ "github.com/go-sql-driver/mysql"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/maps"
"testing"
)
func TestNewRequest(t *testing.T) {
privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd")
if err != nil {
t.Fatal(err)
}
user := NewUser("19644627@qq.com", privateKey, func(resource *registration.Resource) error {
resourceJSON, err := json.Marshal(resource)
if err != nil {
return err
}
t.Log(string(resourceJSON))
return nil
})
regResource := []byte(`{"body":{"status":"valid","contact":["mailto:19644627@qq.com"]},"uri":"https://acme-v02.api.letsencrypt.org/acme/acct/103672877"}`)
err = user.SetRegistration(regResource)
if err != nil {
t.Fatal(err)
}
dnsProvider, err := testDNSPodProvider()
if err != nil {
t.Fatal(err)
}
req := NewRequest(&Task{
User: user,
DNSProvider: dnsProvider,
DNSDomain: "yun4s.cn",
Domains: []string{"yun4s.cn"},
})
certData, keyData, err := req.Run()
if err != nil {
t.Fatal(err)
}
t.Log("cert:", string(certData))
t.Log("key:", string(keyData))
}
func testDNSPodProvider() (dnsclients.ProviderInterface, error) {
db, err := dbs.Default()
if err != nil {
return nil, err
}
one, err := db.FindOne("SELECT * FROM edgeDNSProviders WHERE type='dnspod' ORDER BY id DESC")
if err != nil {
return nil, err
}
apiParams := maps.Map{}
err = json.Unmarshal([]byte(one.GetString("apiParams")), &apiParams)
if err != nil {
return nil, err
}
provider := &dnsclients.DNSPodProvider{}
err = provider.Auth(apiParams)
if err != nil {
return nil, err
}
return provider, nil
}

10
internal/acme/task.go Normal file
View File

@@ -0,0 +1,10 @@
package acme
import "github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
type Task struct {
User *User
DNSProvider dnsclients.ProviderInterface
DNSDomain string
Domains []string
}

49
internal/acme/user.go Normal file
View File

@@ -0,0 +1,49 @@
package acme
import (
"crypto"
"encoding/json"
"github.com/go-acme/lego/v4/registration"
)
type User struct {
email string
resource *registration.Resource
key crypto.PrivateKey
registerFunc func(resource *registration.Resource) error
}
func NewUser(email string, key crypto.PrivateKey, registerFunc func(resource *registration.Resource) error) *User {
return &User{
email: email,
key: key,
registerFunc: registerFunc,
}
}
func (this *User) GetEmail() string {
return this.email
}
func (this *User) GetRegistration() *registration.Resource {
return this.resource
}
func (this *User) SetRegistration(resourceData []byte) error {
resource := &registration.Resource{}
err := json.Unmarshal(resourceData, resource)
if err != nil {
return err
}
this.resource = resource
return nil
}
func (this *User) GetPrivateKey() crypto.PrivateKey {
return this.key
}
func (this *User) Register(resource *registration.Resource) error {
this.resource = resource
return this.registerFunc(resource)
}

View File

@@ -2,10 +2,15 @@ package models
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAPI/internal/acme"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/go-acme/lego/v4/registration"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/types"
)
@@ -131,7 +136,6 @@ func (this *ACMETaskDAO) CreateACMETask(adminId int64, userId int64, acmeUserId
op.AutoRenew = autoRenew
op.IsOn = true
op.State = ACMETaskStateEnabled
op.IsOk = false
_, err := this.Save(op)
if err != nil {
return 0, err
@@ -173,3 +177,179 @@ func (this *ACMETaskDAO) CheckACMETask(adminId int64, userId int64, acmeTaskId i
Pk(acmeTaskId).
Exist()
}
// 设置任务关联的证书
func (this *ACMETaskDAO) UpdateACMETaskCert(taskId int64, certId int64) error {
if taskId <= 0 {
return errors.New("invalid taskId")
}
op := NewACMETaskOperator()
op.Id = taskId
op.CertId = certId
_, err := this.Save(op)
return err
}
// 执行任务并记录日志
func (this *ACMETaskDAO) RunTask(taskId int64) (isOk bool, errMsg string, resultCertId int64) {
isOk, errMsg, resultCertId = this.runTaskWithoutLog(taskId)
// 记录日志
err := SharedACMETaskLogDAO.CreateACMELog(taskId, isOk, errMsg)
if err != nil {
logs.Error(err)
}
return
}
// 执行任务但并不记录日志
func (this *ACMETaskDAO) runTaskWithoutLog(taskId int64) (isOk bool, errMsg string, resultCertId int64) {
task, err := this.FindEnabledACMETask(taskId)
if err != nil {
errMsg = "查询任务信息时出错:" + err.Error()
return
}
if task == nil {
errMsg = "找不到要执行的任务"
return
}
if task.IsOn != 1 {
errMsg = "任务没有启用"
return
}
// ACME用户
user, err := SharedACMEUserDAO.FindEnabledACMEUser(int64(task.AcmeUserId))
if err != nil {
errMsg = "查询ACME用户时出错" + err.Error()
return
}
if user == nil {
errMsg = "找不到ACME用户"
return
}
privateKey, err := acme.ParsePrivateKeyFromBase64(user.PrivateKey)
if err != nil {
errMsg = "解析私钥时出错:" + err.Error()
return
}
remoteUser := acme.NewUser(user.Email, privateKey, func(resource *registration.Resource) error {
resourceJSON, err := json.Marshal(resource)
if err != nil {
return err
}
err = SharedACMEUserDAO.UpdateACMEUserRegistration(int64(user.Id), resourceJSON)
return err
})
if len(user.Registration) > 0 {
err = remoteUser.SetRegistration([]byte(user.Registration))
if err != nil {
errMsg = "设置注册信息时出错:" + err.Error()
return
}
}
// DNS服务商
dnsProvider, err := SharedDNSProviderDAO.FindEnabledDNSProvider(int64(task.DnsProviderId))
if err != nil {
errMsg = "查找DNS服务商账号信息时出错" + err.Error()
return
}
if dnsProvider == nil {
errMsg = "找不到DNS服务商账号"
return
}
providerInterface := dnsclients.FindProvider(dnsProvider.Type)
if providerInterface == nil {
errMsg = "暂不支持此类型的DNS服务商 '" + dnsProvider.Type + "'"
return
}
apiParams, err := dnsProvider.DecodeAPIParams()
if err != nil {
errMsg = "解析DNS服务商API参数时出错" + err.Error()
return
}
err = providerInterface.Auth(apiParams)
if err != nil {
errMsg = "校验DNS服务商API参数时出错" + err.Error()
return
}
acmeRequest := acme.NewRequest(&acme.Task{
User: remoteUser,
DNSProvider: providerInterface,
DNSDomain: task.DnsDomain,
Domains: task.DecodeDomains(),
})
certData, keyData, err := acmeRequest.Run()
if err != nil {
errMsg = "证书生成失败:" + err.Error()
return
}
// 分析证书
sslConfig := &sslconfigs.SSLCertConfig{
CertData: certData,
KeyData: keyData,
}
err = sslConfig.Init()
if err != nil {
errMsg = "证书生成成功,但是分析证书信息时发生错误:" + err.Error()
return
}
// 保存证书
resultCertId = int64(task.CertId)
if resultCertId > 0 {
cert, err := SharedSSLCertDAO.FindEnabledSSLCert(resultCertId)
if err != nil {
errMsg = "证书生成成功,但查询已绑定的证书时出错:" + err.Error()
return
}
if cert == nil {
errMsg = "证书已被管理员或用户删除"
// 禁用
err = SharedACMETaskDAO.DisableACMETask(taskId)
if err != nil {
errMsg = "禁用失效的ACME任务出错" + err.Error()
}
return
}
err = SharedSSLCertDAO.UpdateCert(resultCertId, cert.IsOn == 1, cert.Name, cert.Description, cert.ServerName, cert.IsCA == 1, certData, keyData, sslConfig.TimeBeginAt, sslConfig.TimeEndAt, sslConfig.DNSNames, sslConfig.CommonNames)
if err != nil {
errMsg = "证书生成成功,但是修改数据库中的证书信息时出错:" + err.Error()
return
}
} else {
resultCertId, err = SharedSSLCertDAO.CreateCert(int64(task.AdminId), int64(task.UserId), true, task.DnsDomain+"免费证书", "免费申请的证书", "", false, certData, keyData, sslConfig.TimeBeginAt, sslConfig.TimeEndAt, sslConfig.DNSNames, sslConfig.CommonNames)
if err != nil {
errMsg = "证书生成成功,但是保存到数据库失败:" + err.Error()
return
}
err = SharedSSLCertDAO.UpdateCertACME(resultCertId, int64(task.Id))
if err != nil {
errMsg = "证书生成成功修改证书ACME信息时出错" + err.Error()
return
}
// 设置成功
err = SharedACMETaskDAO.UpdateACMETaskCert(taskId, resultCertId)
if err != nil {
errMsg = "证书生成成功,设置任务关联的证书时出错:" + err.Error()
return
}
}
isOk = true
return
}

View File

@@ -26,3 +26,13 @@ func init() {
SharedACMETaskLogDAO = NewACMETaskLogDAO()
})
}
// 生成日志
func (this *ACMETaskLogDAO) CreateACMELog(taskId int64, isOk bool, errMsg string) error {
op := NewACMETaskLogOperator()
op.TaskId = taskId
op.Error = errMsg
op.IsOk = isOk
_, err := this.Save(op)
return err
}

View File

@@ -12,7 +12,6 @@ type ACMETask struct {
Domains string `field:"domains"` // 证书域名
CreatedAt uint64 `field:"createdAt"` // 创建时间
State uint8 `field:"state"` // 状态
IsOk uint8 `field:"isOk"` // 最后运行是否正常
CertId uint64 `field:"certId"` // 生成的证书ID
AutoRenew uint8 `field:"autoRenew"` // 是否自动更新
}
@@ -28,7 +27,6 @@ type ACMETaskOperator struct {
Domains interface{} // 证书域名
CreatedAt interface{} // 创建时间
State interface{} // 状态
IsOk interface{} // 最后运行是否正常
CertId interface{} // 生成的证书ID
AutoRenew interface{} // 是否自动更新
}

View File

@@ -97,7 +97,7 @@ func (this *ACMEUserDAO) CreateACMEUser(adminId int64, userId int64, email strin
return types.Int64(op.Id), nil
}
// 查找用户列表
// 修改用户信息
func (this *ACMEUserDAO) UpdateACMEUser(acmeUserId int64, description string) error {
if acmeUserId <= 0 {
return errors.New("invalid acmeUserId")
@@ -109,6 +109,18 @@ func (this *ACMEUserDAO) UpdateACMEUser(acmeUserId int64, description string) er
return err
}
// 修改用户ACME注册信息
func (this *ACMEUserDAO) UpdateACMEUserRegistration(acmeUserId int64, registrationJSON []byte) error {
if acmeUserId <= 0 {
return errors.New("invalid acmeUserId")
}
op := NewACMEUserOperator()
op.Id = acmeUserId
op.Registration = registrationJSON
_, err := this.Save(op)
return err
}
// 计算用户数量
func (this *ACMEUserDAO) CountACMEUsersWithAdminId(adminId int64, userId int64) (int64, error) {
query := this.Query()

View File

@@ -10,6 +10,7 @@ type ACMEUser struct {
CreatedAt uint64 `field:"createdAt"` // 创建时间
State uint8 `field:"state"` // 状态
Description string `field:"description"` // 备注介绍
Registration string `field:"registration"` // 注册信息
}
type ACMEUserOperator struct {
@@ -21,6 +22,7 @@ type ACMEUserOperator struct {
CreatedAt interface{} // 创建时间
State interface{} // 状态
Description interface{} // 备注介绍
Registration interface{} // 注册信息
}
func NewACMEUserOperator() *ACMEUserOperator {

View File

@@ -178,6 +178,7 @@ func (this *SSLCertDAO) ComposeCertConfig(certId int64) (*sslconfigs.SSLCertConf
config.Id = int64(cert.Id)
config.IsOn = cert.IsOn == 1
config.IsCA = cert.IsCA == 1
config.IsACME = cert.IsACME == 1
config.Name = cert.Name
config.Description = cert.Description
config.CertData = []byte(cert.CertData)
@@ -269,3 +270,16 @@ func (this *SSLCertDAO) ListCertIds(isCA bool, isAvailable bool, isExpired bool,
}
return result, nil
}
// 设置证书的ACME信息
func (this *SSLCertDAO) UpdateCertACME(certId int64, acmeTaskId int64) error {
if certId <= 0 {
return errors.New("invalid certId")
}
op := NewSSLCertOperator()
op.Id = certId
op.AcmeTaskId = acmeTaskId
op.IsACME = true
_, err := this.Save(op)
return err
}

View File

@@ -11,6 +11,8 @@ import (
// 阿里云服务商
type AliDNSProvider struct {
BaseProvider
accessKeyId string
accessKeySecret string
}
@@ -87,6 +89,20 @@ func (this *AliDNSProvider) GetRoutes(domain string) (routes []*Route, err error
return
}
// 查询单个记录
func (this *AliDNSProvider) QueryRecord(domain string, name string, recordType RecordType) (*Record, error) {
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
for _, record := range records {
if record.Name == name && record.Type == recordType {
return record, nil
}
}
return nil, err
}
// 设置记录
func (this *AliDNSProvider) AddRecord(domain string, newRecord *Record) error {
req := alidns.CreateAddDomainRecordRequest()

View File

@@ -0,0 +1,4 @@
package dnsclients
type BaseProvider struct {
}

View File

@@ -14,6 +14,8 @@ import (
// DNSPod服务商
type DNSPodProvider struct {
BaseProvider
apiId string
apiToken string
}
@@ -104,6 +106,20 @@ func (this *DNSPodProvider) GetRoutes(domain string) (routes []*Route, err error
return routes, nil
}
// 查询单个记录
func (this *DNSPodProvider) QueryRecord(domain string, name string, recordType RecordType) (*Record, error) {
records, err := this.GetRecords(domain)
if err != nil {
return nil, err
}
for _, record := range records {
if record.Name == name && record.Type == recordType {
return record, nil
}
}
return nil, err
}
// 设置记录
func (this *DNSPodProvider) AddRecord(domain string, newRecord *Record) error {
if newRecord == nil {

View File

@@ -7,12 +7,15 @@ type ProviderInterface interface {
// 认证
Auth(params maps.Map) error
// 获取域名列表
// 获取域名解析记录列表
GetRecords(domain string) (records []*Record, err error)
// 读取域名支持的线路数据
GetRoutes(domain string) (routes []*Route, err error)
// 查询单个记录
QueryRecord(domain string, name string, recordType RecordType) (*Record, error)
// 设置记录
AddRecord(domain string, newRecord *Record) error

View File

@@ -2,6 +2,7 @@ package errors
import (
"errors"
"github.com/iwind/TeaGo/Tea"
"path/filepath"
"runtime"
"strconv"
@@ -15,6 +16,11 @@ type errorObj struct {
}
func (this *errorObj) Error() string {
// 在非测试环境下,我们不提示详细的行数等信息
if !Tea.IsTesting() {
return this.err.Error()
}
s := this.err.Error() + "\n " + this.file
if len(this.funcName) > 0 {
s += ":" + this.funcName + "()"

View File

@@ -116,6 +116,8 @@ func (this *ACMETaskService) ListEnabledACMETasks(ctx context.Context, req *pb.L
Id: int64(cert.Id),
IsOn: cert.IsOn == 1,
Name: cert.Name,
TimeBeginAt: int64(cert.TimeBeginAt),
TimeEndAt: int64(cert.TimeEndAt),
}
}
@@ -125,7 +127,6 @@ func (this *ACMETaskService) ListEnabledACMETasks(ctx context.Context, req *pb.L
DnsDomain: task.DnsDomain,
Domains: task.DecodeDomains(),
CreatedAt: int64(task.CreatedAt),
IsOk: task.IsOk == 1,
AutoRenew: task.AutoRenew == 1,
AcmeUser: pbACMEUser,
DnsProvider: pbProvider,
@@ -208,9 +209,13 @@ func (this *ACMETaskService) RunACMETask(ctx context.Context, req *pb.RunACMETas
return nil, this.PermissionError()
}
// TODO
isOk, msg, certId := models.SharedACMETaskDAO.RunTask(req.AcmeTaskId)
return &pb.RunACMETaskResponse{}, nil
return &pb.RunACMETaskResponse{
IsOk: isOk,
Error: msg,
SslCertId: certId,
}, nil
}
// 查找单个任务信息