mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-21 04:36:03 +08:00
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
package certificate
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
|
|
"github.com/certimate-go/certimate/internal/app"
|
|
"github.com/certimate-go/certimate/internal/certacme"
|
|
"github.com/certimate-go/certimate/internal/domain"
|
|
"github.com/certimate-go/certimate/internal/domain/dtos"
|
|
"github.com/certimate-go/certimate/internal/settings"
|
|
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
|
xcertpfx "github.com/certimate-go/certimate/pkg/utils/cert/pfx"
|
|
)
|
|
|
|
type CertificateService struct {
|
|
acmeAccountRepo acmeAccountRepository
|
|
certificateRepo certificateRepository
|
|
}
|
|
|
|
func NewCertificateService(acmeAccountRepo acmeAccountRepository, certificateRepo certificateRepository) *CertificateService {
|
|
return &CertificateService{
|
|
acmeAccountRepo: acmeAccountRepo,
|
|
certificateRepo: certificateRepo,
|
|
}
|
|
}
|
|
|
|
func (s *CertificateService) InitSchedule(ctx context.Context) error {
|
|
app.GetScheduler().MustAdd("cleanupCertificateExpired", "0 0 * * *", func() {
|
|
s.cleanupExpiredCertificates(context.Background())
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CertificateService) DownloadCertificate(ctx context.Context, req *dtos.CertificateDownloadReq) (*dtos.CertificateDownloadResp, error) {
|
|
certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
canonicalName := strings.Split(certificate.SubjectAltNames, ";")[0]
|
|
canonicalName = strings.ReplaceAll(canonicalName, "*", "_")
|
|
|
|
var buf bytes.Buffer
|
|
zipWriter := zip.NewWriter(&buf)
|
|
defer zipWriter.Close()
|
|
|
|
var zipBytes []byte
|
|
switch req.FileFormat {
|
|
case "", domain.CertificateFormatTypePEM:
|
|
{
|
|
serverCertPEM, issuerCertPEM, err := xcert.ExtractCertificatesFromPEM(certificate.Certificate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract certs: %w", err)
|
|
}
|
|
|
|
keyWriter, err := zipWriter.Create(fmt.Sprintf("%s.key", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = keyWriter.Write([]byte(certificate.PrivateKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
certWriter, err := zipWriter.Create(fmt.Sprintf("%s.crt", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = certWriter.Write([]byte(certificate.Certificate))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
serverCertWriter, err := zipWriter.Create(fmt.Sprintf("%s (server).pem", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = serverCertWriter.Write([]byte(serverCertPEM))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
intermediaCertWriter, err := zipWriter.Create(fmt.Sprintf("%s (intermedia).pem", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = intermediaCertWriter.Write([]byte(issuerCertPEM))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = zipWriter.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zipBytes = buf.Bytes()
|
|
}
|
|
|
|
case domain.CertificateFormatTypePFX:
|
|
{
|
|
pfxPassword := "certimate"
|
|
if req.PfxPassword != "" {
|
|
pfxPassword = req.PfxPassword
|
|
}
|
|
|
|
pfxEncoder, err := xcertpfx.ResolvePfxEncoder(req.PfxEncoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certPFX, err := xcert.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword, pfxEncoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certWriter, err := zipWriter.Create(fmt.Sprintf("%s.pfx", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = certWriter.Write(certPFX)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
readmeWriter, err := zipWriter.Create("README.txt")
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
readme := fmt.Sprintf("[PFX Password]\n%s\n", pfxPassword)
|
|
_, err = readmeWriter.Write([]byte(readme))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = zipWriter.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zipBytes = buf.Bytes()
|
|
}
|
|
|
|
case domain.CertificateFormatTypeJKS:
|
|
{
|
|
jksAlias := "certimate"
|
|
if req.JksAlias != "" {
|
|
jksAlias = req.JksAlias
|
|
}
|
|
|
|
jksKeypass := "certimate"
|
|
if req.JksKeypass != "" {
|
|
jksKeypass = req.JksKeypass
|
|
}
|
|
|
|
jksStorepass := "certimate"
|
|
if req.JksStorepass != "" {
|
|
jksStorepass = req.JksStorepass
|
|
}
|
|
|
|
certJKS, err := xcert.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksAlias, jksKeypass, jksStorepass)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certWriter, err := zipWriter.Create(fmt.Sprintf("%s.jks", canonicalName))
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
_, err = certWriter.Write(certJKS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
readmeWriter, err := zipWriter.Create("README.txt")
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
readme := fmt.Sprintf("[JKS Alias]\n%s\n\n[JKS Key Password]\n%s\n\n[JKS Store Password]\n%s\n", jksAlias, jksKeypass, jksStorepass)
|
|
_, err = readmeWriter.Write([]byte(readme))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = zipWriter.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zipBytes = buf.Bytes()
|
|
}
|
|
|
|
default:
|
|
return nil, domain.ErrInvalidParams
|
|
}
|
|
|
|
resp := &dtos.CertificateDownloadResp{
|
|
ZipBytes: zipBytes,
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *CertificateService) RevokeCertificate(ctx context.Context, req *dtos.CertificateRevokeReq) (*dtos.CertificateRevokeResp, error) {
|
|
certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if certificate.ACMEAccountUrl == "" || certificate.ACMECertificateUrl == "" {
|
|
return nil, fmt.Errorf("could not revoke a certificate which is not issued in Certimate")
|
|
}
|
|
if certificate.IsRevoked {
|
|
return nil, fmt.Errorf("could not revoke a certificate which is already revoked")
|
|
}
|
|
|
|
acmeAccount, err := s.acmeAccountRepo.GetByCAAndAcctUrl(ctx, certificate.CA, certificate.ACMEAccountUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to revoke certificate: could not find acme account: %w", err)
|
|
}
|
|
|
|
acmeClient, err := certacme.NewACMEClientWithAccount(acmeAccount)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to revoke certificate: could not initialize acme config: %w", err)
|
|
}
|
|
|
|
revokeReq := &certacme.RevokeCertificateRequest{
|
|
Certificate: certificate.Certificate,
|
|
}
|
|
_, err = acmeClient.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to revoke certificate: %w", err)
|
|
}
|
|
|
|
certificate.IsRevoked = true
|
|
certificate, err = s.certificateRepo.Save(ctx, certificate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dtos.CertificateRevokeResp{}, nil
|
|
}
|
|
|
|
func (s *CertificateService) cleanupExpiredCertificates(ctx context.Context) error {
|
|
globalSettingsForPersistence := settings.GetGlobalSettingsForPersistence()
|
|
if globalSettingsForPersistence.CertificatesRetentionMaxDays != 0 {
|
|
ret, err := s.certificateRepo.DeleteWithExprs(ctx,
|
|
dbx.NewExp(fmt.Sprintf("validityNotAfter<DATETIME('now', '-%d days')", globalSettingsForPersistence.CertificatesRetentionMaxDays)),
|
|
)
|
|
if err != nil {
|
|
app.GetLogger().Error("failed to delete expired certificates", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
if ret > 0 {
|
|
app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|