An account for many customers

This commit is contained in:
yoan
2024-10-23 16:25:21 +08:00
parent 8bec234fe8
commit 1dca6ecf8d
16 changed files with 286 additions and 3331 deletions

View File

@@ -12,6 +12,8 @@ import (
"strings"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/go-acme/lego/v4/certcrypto"
@@ -78,9 +80,37 @@ type ApplyOption struct {
}
type ApplyUser struct {
Ca string
Email string
Registration *registration.Resource
key crypto.PrivateKey
key string
}
func newApplyUser(ca, email string) (*ApplyUser, error) {
repo := getAcmeAccountRepository()
rs := &ApplyUser{
Ca: ca,
Email: email,
}
resp, err := repo.GetByCAAndEmail(ca, email)
if err != nil {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
keyStr, err := x509.PrivateKeyToPEM(privateKey)
if err != nil {
return nil, err
}
rs.key = keyStr
return rs, nil
}
rs.Registration = resp.Resource
rs.key = resp.Key
return rs, nil
}
func (u *ApplyUser) GetEmail() string {
@@ -92,6 +122,15 @@ func (u ApplyUser) GetRegistration() *registration.Resource {
}
func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey {
rs, _ := x509.ParsePrivateKeyFromPEM(u.key)
return rs
}
func (u *ApplyUser) hasRegistration() bool {
return u.Registration != nil
}
func (u *ApplyUser) getPrivateKeyString() string {
return u.key
}
@@ -182,21 +221,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}
}
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
// Some unified lego environment variables are configured here.
// link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(option.DisableFollowCNAME))
myUser := ApplyUser{
Email: option.Email,
key: privateKey,
myUser, err := newApplyUser(sslProvider.Provider, option.Email)
if err != nil {
return nil, err
}
config := lego.NewConfig(&myUser)
config := lego.NewConfig(myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
config.CADirURL = sslProviderUrls[sslProvider.Provider]
@@ -217,11 +251,13 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
client.Challenge.SetDNS01Provider(provider, challengeOptions...)
// New users will need to register
reg, err := getReg(client, sslProvider)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
if !myUser.hasRegistration() {
reg, err := getReg(client, sslProvider, myUser)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
}
myUser.Registration = reg
domains := strings.Split(option.Domain, ";")
request := certificate.ObtainRequest{
@@ -243,7 +279,16 @@ func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, erro
}, nil
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.Resource, error) {
type AcmeAccountRepository interface {
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
Save(ca, email, key string, resource *registration.Resource) error
}
func getAcmeAccountRepository() AcmeAccountRepository {
return repository.NewAcmeAccountRepository()
}
func getReg(client *lego.Client, sslProvider *SSLProviderConfig, user *ApplyUser) (*registration.Resource, error) {
var reg *registration.Resource
var err error
switch sslProvider.Provider {
@@ -271,6 +316,12 @@ func getReg(client *lego.Client, sslProvider *SSLProviderConfig) (*registration.
return nil, err
}
repo := getAcmeAccountRepository()
if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil {
return nil, fmt.Errorf("failed to save registration: %w", err)
}
return reg, nil
}

View File

@@ -0,0 +1,17 @@
package domain
import (
"time"
"github.com/go-acme/lego/v4/registration"
)
type AcmeAccount struct {
Id string
Ca string
Email string
Resource *registration.Resource
Key string
Created time.Time
Updated time.Time
}

View File

@@ -1,6 +1,7 @@
package x509
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"fmt"
@@ -46,3 +47,35 @@ func EqualCertificate(a, b *x509.Certificate) bool {
a.Issuer.SerialNumber == b.Issuer.SerialNumber &&
a.Subject.SerialNumber == b.Subject.SerialNumber
}
// 将 ECDSA 私钥转换为 PEM 格式的字符串。
func PrivateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) {
data, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return "", fmt.Errorf("failed to marshal EC private key: %w", err)
}
block := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: data,
}
return string(pem.EncodeToMemory(block)), nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。
func ParsePrivateKeyFromPEM(privateKeyPem string) (*ecdsa.PrivateKey, error) {
pemData := []byte(privateKeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return privateKey, nil
}

View File

@@ -0,0 +1,51 @@
package repository
import (
"github.com/go-acme/lego/v4/registration"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
)
type AcmeAccountRepository struct{}
func NewAcmeAccountRepository() *AcmeAccountRepository {
return &AcmeAccountRepository{}
}
func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) {
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("acme_accounts", "ca={:ca} && email={:email}", dbx.Params{"ca": ca, "email": email})
if err != nil {
return nil, err
}
resource := &registration.Resource{}
if err := resp.UnmarshalJSONField("resource", resource); err != nil {
return nil, err
}
return &domain.AcmeAccount{
Id: resp.GetString("id"),
Ca: resp.GetString("ca"),
Email: resp.GetString("email"),
Key: resp.GetString("key"),
Resource: resource,
Created: resp.GetTime("created"),
Updated: resp.GetTime("updated"),
}, nil
}
func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error {
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("acme_accounts")
if err != nil {
return err
}
record := models.NewRecord(collection)
record.Set("ca", ca)
record.Set("email", email)
record.Set("key", key)
record.Set("resource", resource)
return app.GetApp().Dao().Save(record)
}