在工作中,有这么一个需求,通过后台管理域名的证书自动申请及续签的功能;
方案有两种:
一种是通过官方的golang.org/x/crypto/acme/autocert包来实现域名的申请及续签,这个不用多说。
另一种是通过lego包来实现,重点说明一下这中方案,只实现了http的挑战,需要在路由中设置挑战路径
我这个Echo的路由
// http域名挑战
s.e.GET("/.well-known/acme-challenge/:token", func(c echo.Context) error {
fmt.Println("域名挑战......")
token := c.Param("token")
exist, value := cert.NewMemoryProviderServer().GetKeyAuth("", token)
fmt.Printf("exist:%v, token:%v, value:%v", exist, token, value)
if !exist {
return errors.New("挑战失败")
}
c.Response().Writer.Write([]byte(value))
return nil
}).Name = "域名申请挑战"
接下来是对lego的封装,http-001挑战需要实现Present,CleanUp这两个方法:
var httpData *MemoryProviderServer
var once1 sync.Once
type MemoryProviderServer struct {
data map[string]string
lock sync.Mutex
}
func NewMemoryProviderServer() *MemoryProviderServer {
once1.Do(func() {
httpData = &MemoryProviderServer{
data: map[string]string{},
}
})
return httpData
}
func (s *MemoryProviderServer) Present(domain, token, keyAuth string) error {
s.lock.Lock()
fmt.Printf("保存token:%v,value:%v\n", token, keyAuth)
s.data[token] = keyAuth
s.lock.Unlock()
return nil
}
func (s *MemoryProviderServer) CleanUp(domain, token, keyAuth string) error {
s.lock.Lock()
delete(s.data, domain+token)
fmt.Printf("清空token:%v\n", token)
s.lock.Unlock()
return nil
}
func (s *MemoryProviderServer) GetKeyAuth(domain, token string) (exist bool, keyAuth string) {
s.lock.Lock()
fmt.Printf("domain data:%+v", s.data)
keyAuth, exist = s.data[token]
s.lock.Unlock()
return
}
接下来是lets encrypt账号注册,注同一个ip一定时间内注册账号是有限制的,所以需要将账号信息缓存起来,比如存redis,db等,这里是直接存的数据库,表结构如下:
type LetsEncrypt struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
Email string `gorm:"column:email;type:varchar(191);uniqueIndex:idx_email;not null;default:'';comment:email" json:"email"`
PrivateKey string `gorm:"column:private_key;type:text;comment:账号key" json:"privateKey"`
Registration string `gorm:"column:registration;type:text;comment:lets Encrypt注册信息" json:"registration"`
CreateAt int64 `gorm:"column:create_at;not null;autoCreateTime" json:"createAt"`
UpdateAt int64 `gorm:"column:update_at;not null;autoUpdateTime" json:"updateAt"`
}
缓存实现:
type DataCache interface {
SetKey(key crypto.PrivateKey) error
GetKey() crypto.PrivateKey
SetRegistration(reg *registration.Resource) error
GetRegistration() *registration.Resource
SetEmail(email string) error
}
type DBCache struct {
Email string
Key crypto.PrivateKey
Registration *registration.Resource
Data *models.LetsEncrypt
}
func (d *DBCache) SetKey(key crypto.PrivateKey) error {
d.Key = key
d.Data.PrivateKey = string(certcrypto.PEMEncode(d.Key))
return d.saveData()
}
func (d *DBCache) GetKey() crypto.PrivateKey {
return d.Key
}
func (d *DBCache) SetRegistration(reg *registration.Resource) error {
d.Registration = reg
marshal, err := json.Marshal(d.Registration)
if err != nil {
return err
}
d.Data.Registration = string(marshal)
return d.saveData()
}
func (d *DBCache) GetRegistration() *registration.Resource {
return d.Registration
}
func (d *DBCache) SetEmail(email string) error {
d.Email = email
// 查询数据库是否存在
var account models.LetsEncrypt
if err := database.GetGormDb().Where(models.LetsEncrypt{Email: d.Email}).FirstOrCreate(&account).Error; err != nil {
log.Error(err)
return err
}
d.Data = &account
// 解析key
if d.Data.PrivateKey != "" {
key, err := certcrypto.ParsePEMPrivateKey([]byte(d.Data.PrivateKey))
if err != nil {
log.Info("初始化key错误")
return err
}
d.Key = key
}
// 解析registration
if d.Data.Registration != "" {
var reg registration.Resource
if err := json.Unmarshal([]byte(d.Data.Registration), ®); err != nil {
log.Info("初始化registration错误")
return err
}
d.Registration = ®
}
return nil
}
func (d *DBCache) saveData() error {
return database.GetGormDb().Save(d.Data).Error
}
最后实现lego的封装
type user struct {
// email 邮箱
email string
// registration 表示已在ACME服务器上注册的帐户信息
registration *registration.Resource
// key 表示ECDSA私钥
key crypto.PrivateKey
}
func (u *user) GetEmail() string {
return u.email
}
func (u *user) GetRegistration() *registration.Resource {
return u.registration
}
func (u *user) GetPrivateKey() crypto.PrivateKey {
return u.key
}
type LetsEncrypt struct {
User *user
Client *lego.Client
Cache DataCache
Account string
}
var letsEncrypt *LetsEncrypt
var once sync.Once
var defaultEmail = "xxx@163.com"
var dao DataCache
func NewLetsEncrypt(email string) *LetsEncrypt {
once.Do(func() {
if email == "" {
email = defaultEmail
}
dao = &DBCache{}
if err := dao.SetEmail(email); err != nil {
log.Error(err)
return
}
letsEncrypt = &LetsEncrypt{
User: &user{
email: email,
},
Cache: dao,
}
letsEncrypt.newKey()
letsEncrypt.newRegistration()
})
return letsEncrypt
}
// 生成lets encrypt 注册账号private key
func (l *LetsEncrypt) newKey() {
if l.Cache.GetKey() != nil {
l.User.key = l.Cache.GetKey()
return
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Error(err)
}
l.User.key = key
log.Info("设置key缓存")
if err = l.Cache.SetKey(key); err != nil {
log.Error(err)
}
}
// 生成lets encrypt的注册信息
// newClient将会执行2次,第一次用于注册账号信息,第二次解决No key Id in jws的bug,/lego/client.go在45行
//
func (l *LetsEncrypt) newRegistration() {
var err error
l.newClient()
if l.Cache.GetRegistration() != nil {
l.User.registration = l.Cache.GetRegistration()
l.newClient()
return
}
reg, err := l.Client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Error(err)
return
}
l.User.registration = reg
l.newClient()
// todo 保存信息
if l.User.registration.Body.Status != "valid" {
err = errors.New("Returning registration status not valid ")
return
}
log.Info("保存registration")
if err = l.Cache.SetRegistration(reg); err != nil {
log.Error(err)
return
}
return
}
const (
// CALetsEncryptDebug(默认) Let's Encrypt 测试地址
CALetsEncryptDebug = "https://acme-staging-v02.api.letsencrypt.org/directory"
// CALetsEncrypt Let's Encrypt 正式地址
CALetsEncrypt = "https://acme-v02.api.letsencrypt.org/directory"
)
func (l *LetsEncrypt) newClient() {
var err error
cfg := lego.NewConfig(l.User)
cfg.CADirURL = CALetsEncrypt
cfg.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(cfg)
if err != nil {
log.Error(err)
return
}
l.Client = client
return
}
func (l *LetsEncrypt) SetProvider(typ challenge.Type) error {
var err error
switch typ {
case challenge.DNS01:
err = l.Client.Challenge.SetDNS01Provider(NewDNSProviderBestDNS())
case challenge.HTTP01:
err = l.Client.Challenge.SetHTTP01Provider(NewMemoryProviderServer())
default:
err = errors.New("Unknown Challenge types ")
}
return err
}
func (l *LetsEncrypt) Obtain(domains []string) ([]byte, []byte, error) {
if len(domains) == 0 {
return nil, nil, errors.New("the domain cannot be empty")
}
var (
err error
res *certificate.Resource
)
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
// 可以在privateKey参数中提供自己的私钥
}
times := 1
for times > 0 {
times--
if res, err = l.Client.Certificate.Obtain(request); err != nil {
log.Info(err)
if err = l.SetProvider(challenge.HTTP01); err != nil {
continue
//return nil, nil, err
}
}
break
}
fmt.Printf("cert:%v,key:%v", string(res.Certificate), string(res.PrivateKey))
return res.Certificate, res.PrivateKey, err
}
func (l *LetsEncrypt) Renew(ce, privateKey []byte) ([]byte, []byte, error) {
// 私钥重用的话 privateKey 不为空,反之则重新生成
var (
res *certificate.Resource
err error
)
if res, err = l.Client.Certificate.Renew(certificate.Resource{
PrivateKey: privateKey,
Certificate: ce,
}, true, true, ""); err != nil {
return nil, nil, err
}
return res.Certificate, res.PrivateKey, nil
}
最后使用,c,k就是公钥和私钥
certService := NewLetsEncrypt("")
obmains := []string{"xxx.com","xxx2.com"}
c, k, err := certService.Obtain(obmains);
完成!搞定
就可以把拿到的公钥和私钥去配置https了.