certimate/internal/certapply/account.go

129 lines
3.4 KiB
Go

package certapply
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"strings"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"golang.org/x/sync/singleflight"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/internal/repository"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
var registrationSg singleflight.Group
type ACMEAccount = domain.ACMEAccount
func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccount, error) {
if config == nil {
return nil, errors.New("the acme config is nil")
}
if email == "" {
return nil, errors.New("the email is empty")
}
ctx := context.Background()
accountRepo := repository.NewACMEAccountRepository()
account, err := accountRepo.GetByCAAndEmail(ctx, string(config.CAProvider), config.CADirUrl, email)
if err != nil {
if !domain.IsRecordNotFoundError(err) {
return nil, fmt.Errorf("failed to get acme account record: %w", err)
}
}
// register new acme account if not exists
if account == nil {
if !register {
return nil, errors.New("the acme account does not exist")
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
keyPEM, err := xcert.ConvertECPrivateKeyToPEM(key)
if err != nil {
return nil, err
}
account = &ACMEAccount{
CA: string(config.CAProvider),
Email: email,
PrivateKey: keyPEM,
ACMEDirUrl: config.CADirUrl,
}
legoCfg := lego.NewConfig(account)
legoCfg.CADirURL = config.CADirUrl
legoClient, err := lego.NewClient(legoCfg)
if err != nil {
return nil, err
}
var regres *registration.Resource
var regerr error
if legoClient.GetExternalAccountRequired() {
if config.EABKid == "" {
return nil, errors.New("missing or invalid eab kid")
}
if config.EABHmacKey == "" {
return nil, errors.New("missing or invalid eab hmac key")
}
// patch, see https://github.com/go-acme/lego/issues/2634
keyId := strings.TrimSpace(config.EABKid)
keyEncoded := strings.TrimSpace(config.EABHmacKey)
keyEncoded = strings.ReplaceAll(strings.ReplaceAll(keyEncoded, "+", "-"), "/", "_")
keyEncoded = strings.TrimRight(keyEncoded, "=")
regres, regerr = legoClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: keyId,
HmacEncoded: keyEncoded,
})
} else {
regres, regerr = legoClient.Registration.Register(registration.RegisterOptions{
TermsOfServiceAgreed: true,
})
}
if regerr != nil {
return nil, fmt.Errorf("failed to register acme account: %w", regerr)
}
account.ACMEAccount = &regres.Body
account.ACMEAcctUrl = regres.URI
if _, err := accountRepo.Save(ctx, account); err != nil {
return nil, fmt.Errorf("failed to save acme account record: %w", err)
}
}
return account, nil
}
func NewACMEAccountWithSingleFlight(config *ACMEConfig, email string) (*ACMEAccount, error) {
if config == nil {
return nil, errors.New("the acme config is nil")
}
if email == "" {
return nil, errors.New("the email is empty")
}
resp, err, _ := registrationSg.Do(fmt.Sprintf("%s|%s|%s", string(config.CAProvider), config.CADirUrl, email), func() (any, error) {
return NewACMEAccount(config, email, true)
})
if err != nil {
return nil, err
}
return resp.(*ACMEAccount), nil
}